phase: 179_git_clone_server_freeze
Build and Push Containers / build-and-push-app (push) Successful in 1m43s
Build and Push Containers / build-and-push-db (push) Successful in 13s

All gates green; working tree holds exactly the phase's changes for the harness to commit.

## Phase 179 — Final verification report

- **Verified (all 3 tasks already complete in `complete/`)**: repro + CONFIRMED verdict, `asyncio.to_thread` fix, dedicated E2E suite.
- `app/api/sync.py::_run_sync`: both git ops (`clone_or_pull`, `file_commit_dates`) now `await asyncio.to_thread(...)`; phase-134 boundary note + module docstring document worker-thread-runs-to-completion and the A3 DB-calls-stay-on-loop note.
- **Tests / lint / coverage** (full suite 100% green):
  - `uv run pytest tests/integration/test_sync_git_freeze.py -v` → **4 passed** (post-fix probe <1.5 s during 3 s stub)
  - `uv run pytest tests/e2e/test_sync_git_freeze.py -v --no-cov` → **2 passed in 25.65 s in isolation** (wall guard 120 s holds)
  - `uv run pytest --cov=app --cov-report=term-missing` → **4173 passed**, `app/` coverage **99%** (`app/api/sync.py` 100%)
  - `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors**
- **Completion criteria**: ① integration probe green + verdict (CONFIRMED, mechanism, measured 3.04 s→0.003 s) in docstring — **met**; ② off-loop git ops + boundary/A3 notes — **met**; ③ full suite green, coverage 99% >90%, ruff+pyright clean — **met**; ④ E2E green in isolation (responsive-poll + cancel-during-clone contracts) — **met**; ⑤ atomic commit + move to `complete/` — **harness-owned** (pipeline rule overrides per-phase self-commit; all changes staged-ready in the working tree).
- **Documentation**: full README (1526 l) + AGENTS.md review — phase-179 bullets present and accurate (verified against fixture constants, `.env.example` fully covered by the config reference, pages table matches `app/main.py` shell routes, quick-reference commands/artifacts exist); **consistent — no further changes needed**.
- **Notable**: task-03 pipeline sessions died on context overflow but their harness validation passed; this pass independently re-ran every gate to confirm. No live infra touched; no git index commands run.
- **Next pending phase**: `180_markdown_inline_links_images`.
This commit is contained in:
2026-10-14 13:06:39 -04:00
parent 2b57180998
commit 3d82a72b6f
31 changed files with 2326 additions and 28 deletions
+35 -4
View File
@@ -280,6 +280,33 @@ def _poll(client: TestClient, want: str, timeout: float = 5.0) -> dict:
raise AssertionError(f"sync did not reach {want!r} within {timeout}s")
def _wait_phase(client: TestClient, want: str, timeout: float = 5.0) -> dict:
"""Poll ``GET /api/sync/status`` until ``phase == want`` (the phase
machine, phase 98 task 01), failing loudly if the run leaves
``running`` first.
Phase 179 (task 02) is why this exists: the sync's git ops now run
OFF the event loop (``asyncio.to_thread``), so how far the run has
progressed by the time the client issues its NEXT request is
genuinely concurrent — pre-fix the spawned task reached the import
span before the trigger's own response went out, and a test that
asserted a mid-run phase silently depended on that. Tests wait for
the phase instead."""
deadline = time.monotonic() + timeout
while True:
body = client.get("/api/sync/status").json()
assert body["state"] == "running", (
f"run left state {body['state']!r} before phase {want!r}: {body}"
)
if body["phase"] == want:
return body
if time.monotonic() >= deadline:
raise AssertionError(
f"sync did not report phase {want!r} within {timeout}s: {body}"
)
time.sleep(0.02)
class FakeImportSources:
"""Records every ``import_sources`` call; returns a canned summary."""
@@ -626,8 +653,10 @@ def test_double_trigger_while_running_returns_409(
monkeypatch.setattr(sync_api, "clone_or_pull", fake_clone)
_stub_probe(monkeypatch)
# The in-flight run takes a while (asyncio.sleep) so the second POST
# lands while it is still running.
fake_import = FakeImportSources(ImportSummary(files=1, added=1), delay=0.5)
# lands while it is still running (phase 179: 1.5 s, not 0.5 s — the
# run now yields the loop at its git ops, so the window the second
# POST and the phase wait fall into is wider on purpose).
fake_import = FakeImportSources(ImportSummary(files=1, added=1), delay=1.5)
monkeypatch.setattr(sync_api, "import_sources", fake_import)
monkeypatch.setattr(sync_api, "regenerate_overview", FakeOverview(ok=True))
monkeypatch.setattr(sync_api, "generate_folder_summaries", FakeFolderSummaries())
@@ -639,7 +668,10 @@ def test_double_trigger_while_running_returns_409(
assert r.status_code == 409
assert r.json() == {"detail": "a sync is already running"}
body = sync_client.get("/api/sync/status").json()
# Phase 179 (task 02): WAIT for the import phase — the run's
# progress is concurrent now that the git ops left the loop, so the
# next request no longer inherits a run that already ran past them.
body = _wait_phase(sync_client, "import")
assert body["state"] == "running"
assert body["started_at"] is not None
assert body["finished_at"] is None
@@ -647,7 +679,6 @@ def test_double_trigger_while_running_returns_409(
# Phase 98 (task 01): the in-flight run (parked in the fake
# import's delay) reports the import phase — the summary span has
# not started, so its counters are 0/0.
assert body["phase"] == "import"
assert body["current_summary"] is None
assert body["summaries_done"] == 0 and body["summaries_total"] == 0