chore(agent): phase roadmap from TODO.md (179 git clone server freeze)

TODO L3 (a long git clone seems to freeze the server) converted to phase 179:
deterministic in-process repro + verdict, the asyncio.to_thread fix for the
sync's git ops, and the dedicated Playwright E2E suite. TODO.md cleared
(items now live in .agents/phases/).

LLM-Generated: true
This commit is contained in:
2026-10-12 09:27:58 -04:00
parent 149e753952
commit dad54a2854
5 changed files with 128 additions and 2 deletions
@@ -0,0 +1,41 @@
# Phase 179 — Git clone server freeze: investigate the owner's suspicion, then fix it
**Source:** `TODO.md` L3 — "A long git clone seems to freeze the server? Investigate to find out if this is true."
**Story:** n/a (TODO-derived, owner roadmap confirmation 2026-10-11)
**Context:** `app/api/sync.py` — `start_sync` (L421–438, `asyncio.create_task(_run_sync())` L438), `_run_sync` (L482, **async**, runs on the app's single uvicorn event loop), `await check_models(llm)` (L533 — already async HTTP probes), the short-lived `SessionLocal()` + `effective_sources(db)` (L538–540), the per-source `_check_cancelled()` boundary (L565), **`root = clone_or_pull(…)` (L577 — synchronous)**, **`file_commit_dates(root)` (L589 — synchronous)**, `sync_status` (L353–354 — "the UI polls this every 2 s"), `cancel_sync` (L442–459); `scripts/git_sync.py` — `clone_or_pull` (L96), `file_commit_dates` (L270), `run_git` (L320 — `subprocess.run` L342, **blocking**); `app/api/health.py` — `GET /api/health` (L13, mounted at `/api/health` via `app/main.py` L211); `scripts/import_docs.py` — the standalone script path (a SEPARATE process, never on the app's event loop — NOT a freeze vector); `app/api/doc_drafts.py` — `push_doc_draft` (a sync `def` → FastAPI threadpool — NOT a freeze vector); `app/api/sync.py` L233 (`from scripts.git_sync import GitSyncError, clone_or_pull, file_commit_dates` — the names to monkeypatch); `tests/integration/test_sync_api.py` — the `sync_client` context-managed `TestClient` pattern (L176–179 — one app event loop across requests) + the env-fallback `BOR_GIT_SOURCES` driving (L64–65 docstring); `tests/e2e/test_sync_cancel.py` — the dedicated module-app E2E pattern (dedicated port, `file://` fixture repo via the `_git` helper, the `_wall_guard` module fixture); `tests/e2e/slow_llm.py` (not needed here — the mock LLM import is fast; the fixture's slowness is in the CLONE, not the import).
## Objective
Verify the owner's suspicion with a deterministic reproduction (a long `git clone` during a sync blocks the app's event loop, stalling the entire HTTP surface — including the 2 s sync-status poll the UI relies on), record the verdict, and — the suspicion being confirmed — make the sync's git operations run OFF the event loop so the server stays fully responsive for the whole duration of a long clone (and the phase-134 cooperative cancel keeps its contract).
## Design (shared by all tasks — the executor reads this, not the chat)
- **D1 — the repro is in-process and deterministic (task 01).** A new `tests/integration/test_sync_git_freeze.py` follows `test_sync_api.py`'s `sync_client` pattern (context-managed `TestClient` — one app event loop across requests). `app.api.sync.clone_or_pull` and `app.api.sync.file_commit_dates` are monkeypatched (the module-level names imported at L233) with stubs that record a `time.monotonic()` start/end and sleep 3 s. The probe test measures the wall-clock latency of a `GET /api/sync/status` issued while the stub is in its sleep. The probe asserts the **PRE-fix** behavior (the probe is blocked — latency ≥ 2.5 s of the 3 s stub) → the test is GREEN before the fix and IS the demonstration of the freeze; the verdict (**CONFIRMED** / not) + mechanism + the measured latency are recorded in the test module docstring. No app/ code is touched in task 01.
- **D2 — the fix is `asyncio.to_thread`, nothing else (task 02).** Inside `_run_sync`: `root = await asyncio.to_thread(clone_or_pull, clone_url_for(row), sources_root / repo_name(row.url), row.branch)` and `doc_dates_by_root[str(root)] = await asyncio.to_thread(file_commit_dates, root)`. The module docstring and the phase-134 boundary comment (L565–576) update the semantics: a cancel registered while a git op is in flight no longer parks the EVENT LOOP — the git **worker thread** runs to completion (Python threads are not interruptible), the loop stays free (every other request, status poll included, is served), and the run still stops at the NEXT `_check_cancelled()` boundary (the phase-134 contract, unchanged). The task-01 probe flips to the post-fix assertion (probe latency < 1.5 s during the 3 s stub clone). The short-lived `SessionLocal()` / `effective_sources` DB calls stay on the loop (A3 — millisecond-scale, out of scope, noted in the docstring). `check_models` is already async HTTP — untouched.
- **D3 — the E2E pins the real pipeline (task 03).** `tests/e2e/test_sync_git_freeze.py` (the phase's dedicated Playwright suite, AGENTS rule 4): the dedicated module app on a dedicated port (the `test_sync_cancel` pattern — the session app is never started in the isolated run), mock LLM, a dedicated `BOR_SOURCES_DIR`, `BOR_GIT_SOURCES=file://<big repo>`, the shared E2E Postgres truncated before AND after every test. The `big_git_repo` fixture is a GENERATED local repo — N commits × ~1 MiB of near-incompressible (seeded pseudo-random) `.md` files — sized at authoring so a FRESH `git clone` of it takes ≥ 5 s on the test host (the pinned constants + the calibration note live in the module; `git` on PATH is the documented prerequisite, phase 28). Contract 1: while the run is in the clone phase, `GET /api/sync/status` polls (via `page.request`, ~2 s apart like the UI) each complete in < 1.5 s and the Sources page keeps updating with no network-error state; the run lands `success` and the sentinel file is in the KB. Contract 2 (phase 134 under to_thread): a cancel registered during the clone — the checkout dir is deleted first so the run re-clones (a long git phase again) — the status endpoint stays responsive (< 1.5 s) while the in-flight git thread finishes, and the run lands the `cancelled` terminal (error null, the UI's "Sync cancelled" neutral state). The `_wall_guard` module fixture pins the budget (the `test_sync_cancel` pattern — cut the fixture size if it drifts, never the guard). `E2E_REAL_LLM=1` is skipped (mock-only — the clone, not the import, is the timing fixture).
- **NOT touched:** `scripts/import_docs.py` (standalone process), `scripts/git_sync.py` (the git layer is unchanged — only its SCHEDULING changes), `app/core/docs_push.py` / `app/api/doc_drafts.py` (sync `def` → threadpool), `check_models`, the async LLM import path, the frontend, any other endpoint.
## Dependencies
— (none: the app is fully functional at the head of `complete/`; this phase investigates + fixes the in-process sync path)
## Tasks
1. `01_repro_verdict.md` — the deterministic in-process repro (3 s stubbed clone, status-poll latency probe) that demonstrates the event-loop freeze and records the verdict in the test module docstring.
2. `02_offloop_git_ops.md` — the fix: `clone_or_pull` / `file_commit_dates` off the event loop via `asyncio.to_thread`, the docstring/boundary-note update, the probe flipped to the post-fix assertion.
3. `03_e2e_freeze_contract.md` — `tests/e2e/test_sync_git_freeze.py` (the phase's dedicated Playwright suite, AGENTS rule 4): a real long `file://` clone, the responsive-status-poll contract, the phase-134 cancel-during-clone contract, the full gates, the atomic commit.
## Testing & Quality
- **Integration:** `tests/integration/test_sync_git_freeze.py` — the probe: pre-fix (task 01) the status poll issued during the 3 s stubbed clone is blocked (≥ 2.5 s); post-fix (task 02) it completes in < 1.5 s; the run reaches a terminal state in both (fixture cleanliness). `test_sync_api.py` + `test_import_docs_git.py` stay green (no behavior change beyond loop-freeness).
- **E2E (AGENTS rule 4, task 03):** `tests/e2e/test_sync_git_freeze.py`, run **in isolation** — the clone-phase status polls responsive (< 1.5 s each, ~2 s apart), the Sources page updating, the `success` terminal + sentinel in the KB; the cancel-during-clone → `cancelled` terminal with the status endpoint responsive while the git thread finishes.
- **Coverage:** **>90%** on `app/` (`uv run pytest --cov=app --cov-report=term-missing` — the only `app/` change is two `await asyncio.to_thread(…)` lines + docstrings in `app/api/sync.py`).
- **Lint/types:** `uv run ruff check . && uv run pyright`.
## Completion Criteria
- [ ] `uv run pytest tests/integration/test_sync_git_freeze.py -v` green with the post-fix assertion (probe < 1.5 s during the 3 s stub clone) and the verdict ("CONFIRMED" + mechanism + measured latency) recorded in the module docstring.
- [ ] `app/api/sync.py::_run_sync` runs the per-source git ops off the event loop (`asyncio.to_thread`); the phase-134 boundary note documents the worker-thread-runs-to-completion semantics; the DB calls stay on the loop (the A3 note).
- [ ] `uv run pytest` green, `app/` coverage >90%, ruff + pyright clean.
- [ ] `uv run pytest tests/e2e/test_sync_git_freeze.py -v --no-cov` green **in isolation** (the responsive-poll contract + the cancel-during-clone contract, wall-time guard holds).
- [ ] One atomic `--no-gpg-sign` Conventional Commit (e.g. `fix(sync): run the sync's git clone/pull off the event loop so long clones no longer freeze the server`); the phase dir moves to `.agents/phases/complete/`.
## Locked decisions (owner-confirmed 2026-10-11, roadmap confirmation)
- **A1 — scope: investigate + fix.** The TODO item is an investigation ("find out if this is true"); the owner confirmed the phase goes on to the minimal fix. If the repro in task 01 had landed NOT-confirmed, task 02 would document the no-fix decision instead — the code-path analysis (the synchronous `subprocess.run` inside the async `_run_sync`) and the repro together confirm the freeze.
- **A2 — fix mechanism: `asyncio.to_thread`.** No new packages; the single-event-loop uvicorn is unchanged; a cancel at the git phase leaves the in-flight git worker thread running to completion (Python threads are not interruptible) while the event loop is free — the phase-134 cooperative-cancel contract (stop at the next boundary, `cancelled` terminal, no corruption) is preserved.
- **A3 — out of scope: the sync's short-lived DB calls.** `SessionLocal()` / `effective_sources` (L538–540) are millisecond-scale SQLAlchemy calls — not the reported symptom; they stay on the loop and are noted in the docstring as a known, bounded, non-fix.
- **A4 — the E2E slow clone is a generated local `file://` repo.** N commits × ~1 MiB seeded pseudo-random `.md` files, sized at authoring so a fresh clone takes ≥ 5 s on the test host; deterministic, no network, `git` on PATH is the documented prerequisite (phase 28).
@@ -0,0 +1,27 @@
# Task 01 — Reproduce the freeze in-process and record the verdict
**Phase:** `179_git_clone_server_freeze` · **Source:** `TODO.md:3` — "A long git clone seems to freeze the server? Investigate to find out if this is true."
**Story:** n/a
## Objective
A deterministic, in-process reproduction that proves (or disproves) the owner's suspicion: while a sync's git clone is in flight, the app's event loop is blocked and the whole HTTP surface — including `GET /api/sync/status`, the endpoint the UI polls every 2 s — is stalled. The verdict (CONFIRMED / not, the mechanism, the measured latency) is recorded in the test module docstring.
## Work
1. `tests/integration/test_sync_git_freeze.py` — a NEW file following `tests/integration/test_sync_api.py`'s house patterns:
- The `sync_client` fixture (L176–179 pattern): a **context-managed** `TestClient(fastapi_app)` — one app event loop across requests (the docstring L64–65 explains why the context manager is required). Admin login via the house helper (the `_login` pattern, `test_sync_api.py` L260–265).
- The git source via the **env fallback**: drive `BOR_GIT_SOURCES=file://<tmp repo>` the way `test_sync_api.py` does (a fresh `Settings` on the resolver's module; the dev `.env` never leaks in). The tmp repo is a real one-commit git repo built with the house `_git` helper pattern (`tests/e2e/test_sync_cancel.py` L156–195 shape: `git init -q`, `add -A`, `-c user.email=… -c user.name=… -c commit.gpgsign=false commit -qm`); the stub never actually clones it, so the repo's content is irrelevant — one small `.md` file suffices.
- **The stubs (the timing fixture):** `monkeypatch.setattr("app.api.sync.clone_or_pull", …)` and `monkeypatch.setattr("app.api.sync.file_commit_dates", …)` — the module-level names imported at `app/api/sync.py` L233 (patching the `scripts.git_sync` originals would not affect the runner). The `clone_or_pull` stub: on entry sets a `threading.Event` (`clone_started`) and records `time.monotonic()`, sleeps **3.0 s** (simulating a long clone), records the end, and returns the tmp repo's `Path`; the `file_commit_dates` stub returns `{}`. Both are plain sync functions — the point is that PRE-fix they run ON the event loop.
- **The probe test** `test_long_git_clone_freezes_the_event_loop`: `POST /api/sync` (admin) → wait for `clone_started` (bounded wait, ~10 s) → `t0 = time.monotonic()`, `client.get("/api/sync/status")`, `elapsed = time.monotonic() - t0` → **assert `elapsed >= 2.5`** (the probe request, issued into the middle of the 3 s stubbed clone, is BLOCKED — it cannot be served until the stubbed clone returns and the event loop is free again). That assertion IS the freeze demonstration and it is GREEN pre-fix. Then poll `GET /api/sync/status` to the terminal state (the stub returns, so the run completes: `success` with the import, or a clean terminal — whichever the stubbed root yields; pin whichever the suite shows and document it) so the fixture is left clean for the next test.
- **The module docstring (the verdict record):** the owner's TODO item quoted (L3), the code path (`POST /api/sync` → `asyncio.create_task(_run_sync())` — `app/api/sync.py` L438/L482 → the synchronous `clone_or_pull` L577 → `run_git`'s blocking `subprocess.run` — `scripts/git_sync.py` L342 — with no `to_thread`/`create_subprocess_exec` anywhere in `app/` or `scripts/`), the measured probe latency (e.g. "the status probe took ≈3.0 s of the 3.0 s stubbed clone — the event loop was stalled for the full git op"), and the verdict line: **CONFIRMED** (or NOT — if the probe came back fast, stop and flag it in the docstring + the phase report rather than proceeding to a fix).
- `GET /api/health` (`app/api/health.py` L13, mounted at `/api/health`) may be used as a SECOND probe in the same test (the same assertion — it is blocked too: any endpoint on the loop stalls); keep the status probe primary (it is the endpoint the UI's 2 s poll depends on).
2. Verify by running: `uv run pytest tests/integration/test_sync_git_freeze.py -v` — green (the probe is green with the PRE-fix assertion: it documents the freeze, it is not a failing test), and `uv run pytest` — the full suite green (no other test is affected: the stubs live inside this one test's monkeypatch).
## Testing & Quality
- Integration: the probe family above (the blocked-latency assertion, the terminal-state assertion, the admin/anonymous gating falls out of the house `sync_client` pattern — add the anonymous-403 pins if the house pattern does not already carry them in this file).
- Coverage: **>90%** on `app/` (this task is test-only — `app/` code is untouched, so the suite's coverage must simply stay green).
## Completion Criteria
- [ ] `uv run pytest tests/integration/test_sync_git_freeze.py -v` green with the pre-fix assertion (`elapsed >= 2.5` for the status probe issued mid-clone).
- [ ] The module docstring records: the TODO item, the code path, the measured latency, and the verdict (**CONFIRMED** — expected, per the code-path analysis).
- [ ] `uv run pytest` green.
- [ ] No `app/` code change in this task.
@@ -0,0 +1,28 @@
# Task 02 — The fix: the sync's git operations run off the event loop
**Phase:** `179_git_clone_server_freeze` · **Source:** `TODO.md:3` — "A long git clone seems to freeze the server? Investigate to find out if this is true."
**Story:** n/a
## Objective
With the freeze confirmed (task 01's verdict), make the server stay fully responsive for the whole duration of a long `git clone` / `git pull` (and the `file_commit_dates` walk): the sync's per-source git operations run in a worker thread via `asyncio.to_thread`, the phase-134 cooperative-cancel contract is preserved, and the task-01 probe flips to pin the post-fix behavior.
## Work
1. `app/api/sync.py` — `_run_sync` (L482), the per-source git block (L566–589):
- L577: `root = clone_or_pull(…)` → `root = await asyncio.to_thread(clone_or_pull, clone_url_for(row), sources_root / repo_name(row.url), row.branch)` (the three-argument call, phase 121/135, unchanged — only its scheduling changes).
- L589: `doc_dates_by_root[str(root)] = file_commit_dates(root)` → `doc_dates_by_root[str(root)] = await asyncio.to_thread(file_commit_dates, root)`.
- `import asyncio` if not already imported at module level (check first — the module already uses `asyncio.create_task` at L438, so `asyncio` is imported; verify, do not duplicate).
- **The phase-134 boundary note (L565–576)** — update the comment: a cancel registered while a git op is in flight no longer parks the EVENT LOOP; the git **worker thread** runs its blocking `subprocess` to completion (Python threads are not interruptible — `_task.cancel()` unblocks the run at the `to_thread` await point / the run stops at the NEXT `_check_cancelled()` boundary), while every other request — status polls included — is served immediately. The observable contract is unchanged (phase 134: stop at the next boundary, `cancelled` terminal, no corruption).
- **The module docstring** — add the freeze note: pre-phase-179 the synchronous `clone_or_pull` / `file_commit_dates` (their `subprocess.run` calls — `scripts/git_sync.py` L342) ran ON the app's event loop, stalling the whole HTTP surface (including the 2 s `sync_status` poll) for the duration of a long clone (the TODO L3 suspicion — confirmed by `tests/integration/test_sync_git_freeze.py`); phase 179 runs them off the loop via `asyncio.to_thread`. Include the **A3 note**: the short-lived `SessionLocal()` / `effective_sources` DB calls (L538–540) remain on the loop — millisecond-scale, a known bounded non-fix, deliberately out of scope.
- `check_models` (L533) is untouched (already async HTTP probes). The kind-local branch (L590+) is untouched (no subprocess in it).
2. `tests/integration/test_sync_git_freeze.py` — flip the task-01 probe to the post-fix assertion: the status probe issued into the middle of the 3 s stubbed clone now completes in **< 1.5 s** (the event loop is free while the stubbed clone runs in the worker thread); keep the terminal-state assertion. Update the module docstring: the verdict section now reads CONFIRMED (task 01) → FIXED (phase 179, `asyncio.to_thread`), with the post-fix measured latency recorded (the probe ≈ instant, e.g. "the status probe took ≈0.01 s while the 3.0 s stubbed clone ran in the worker thread").
3. Verify by running: `uv run pytest tests/integration/test_sync_git_freeze.py tests/integration/test_sync_api.py tests/integration/test_import_docs_git.py -v` — all green (the flipped probe green; the sync families show no behavior change beyond loop-freeness), then `uv run pytest` — the full suite green.
## Testing & Quality
- Integration: the flipped probe (post-fix: status probe < 1.5 s during the 3 s stubbed clone) + the `test_sync_api.py` / `test_import_docs_git.py` regression set.
- Coverage: **>90%** on `app/` (the only `app/` change is two `await asyncio.to_thread(…)` lines + docstring/comment in `app/api/sync.py` — the suite's coverage must stay green).
## Completion Criteria
- [ ] `uv run pytest tests/integration/test_sync_git_freeze.py -v` green with the POST-fix assertion (probe < 1.5 s during the 3 s stub clone) and the docstring verdict updated (CONFIRMED → FIXED, post-fix latency recorded).
- [ ] `app/api/sync.py::_run_sync` awaits `asyncio.to_thread` for BOTH git ops; the phase-134 boundary note + the module docstring document the worker-thread semantics and the A3 DB note; `check_models` untouched.
- [ ] `uv run pytest tests/integration/test_sync_git_freeze.py tests/integration/test_sync_api.py tests/integration/test_import_docs_git.py -v` green; `uv run pytest` green.
- [ ] No behavior change in completed work beyond loop-freeness (the sync pipeline's sequence, errors, and cancel terminals are unchanged).
@@ -0,0 +1,32 @@
# Task 03 — E2E: the status poll stays responsive through a real long clone (the phase's dedicated Playwright suite)
**Phase:** `179_git_clone_server_freeze` · **Source:** `TODO.md:3` — "A long git clone seems to freeze the server? Investigate to find out if this is true."
**Story:** n/a
## Objective
Pin the fix against the REAL pipeline (the owner's exact suspicion, end to end): a sync whose first action is a genuinely long `git clone` (a generated local `file://` repo) must keep the whole HTTP surface — the 2 s `GET /api/sync/status` poll and the Sources page — responsive for the entire duration of the clone, the run must land `success`, and the phase-134 cancel-during-clone contract must hold under the off-loop scheduling. Then run the full gates and commit the phase.
## Work
1. `tests/e2e/test_sync_git_freeze.py` — a NEW module, mock-only (`E2E_REAL_LLM=1` skipped — the CLONE, not the import, is the timing fixture), following `tests/e2e/test_sync_cancel.py`'s house pattern point for point:
- **Isolation:** a per-module app on a **dedicated port** (the conftest session app is never started in this isolated run), the mock LLM on the conftest port, a dedicated `BOR_SOURCES_DIR` for the fixture repo's checkout, `BOR_GIT_SOURCES=file://<big repo>` (the env-fallback row — the `test_sync_cancel` L256 shape), the shared E2E Postgres truncated before AND after every test.
- **The `big_git_repo` fixture (the timing fixture — A4):** a module-scoped helper (the `_git` pattern, `test_sync_cancel.py` L156–195) that generates N commits, each adding one ~1 MiB `notes/note-<k>.md` whose body is **seeded pseudo-random** text (deterministic — a fixed-seed generator, e.g. `random.Random(k)` over a word list — so the pack is near-incompressible and the clone time is stable; plain repeated text would compress away and the clone would be fast). **Calibrate at authoring time**: pick N (start ~200) so a FRESH `git clone file://<repo>` takes **≥ 5 s** on the test host (measure it during authoring, record the measured clone time in the module docstring, and pin N + the file size as module constants with the calibration note). The last file carries the sentinel marker (the `test_sync_cancel` sentinel shape).
- **Test 1 — `test_status_polls_responsive_during_long_clone`:** admin login (the house helper) → trigger the sync (the UI's sync button, or the API via `page.request` — the `test_sync_cancel` L100-ms `page.request` poll shape) → while the run reports `running` and is still in the clone phase (the status's `current_file` is null / no file positions — the import has not started), poll `GET /api/sync/status` via `page.request` at ~2 s intervals (the UI's poll cadence): **every poll must complete in < 1.5 s** (the UI's 2 s poll budget — pre-fix the polls issued during the clone would stall for the whole clone and this is the owner's frozen server, observable as hung status polls / a stalled Sources page) → the Sources page keeps updating (no network-error state, the status label keeps advancing) → the run lands the `success` terminal → the sentinel file is in the KB (the house drill-down/`GET /api/docs` assertion shape).
- **Test 2 — `test_cancel_during_clone_reaches_terminal`:** first DELETE the fixture repo's checkout dir under `BOR_SOURCES_DIR` (the run re-clones — a long git phase again), trigger a second sync, and while it is in the clone phase register the cancel (the UI's Cancel control or `POST /api/sync/cancel` via `page.request`) → **the status endpoint stays responsive** (the `page.request` polls during the in-flight git worker thread all complete in < 1.5 s — the A2 observable: the git thread runs to completion in the background while the loop serves requests) → the run lands the `cancelled` terminal (the phase-134 contract: `error: null`, the in-flight positions cleared, the UI's neutral "Sync cancelled" state — no error banner, the main button retry-ready).
- **The `_wall_guard` module fixture** (the `test_sync_cancel` pattern): budget the module (two syncs, the first a full clone + mock import of N files, the second a re-clone + cancel) — budget it at the measured clone time + import span + setup, pin the guard; **if the suite drifts, cut N, not the guard.**
- The module docstring: the owner's TODO item quoted, the fix (task 02's `asyncio.to_thread`), the fixture calibration (N, the measured clone time), the timing paragraph, the run-in-isolation command (`uv run pytest tests/e2e/test_sync_git_freeze.py -v --no-cov`), the environment prerequisites (DB up: `podman compose up -d db`; `git` on PATH — the phase-28 documented prerequisite), the test→observable mapping (the house shape).
2. **The full gates** (the phase's final pass):
- `uv run pytest tests/e2e/test_sync_git_freeze.py -v --no-cov` — green **in isolation**.
- `uv run pytest --cov=app --cov-report=term-missing` — the full suite green, `app/` coverage **>90%**.
- `uv run ruff check . && uv run pyright` — clean.
- The regression spot-check in isolation: `uv run pytest tests/e2e/test_sync_cancel.py -v --no-cov` (the phase-134 cancel E2E — the contract this phase must not have broken; run it AFTER the new suite so the DB truncate ordering is clean).
3. **Commit + move:** one atomic `--no-gpg-sign` Conventional Commit covering `app/api/sync.py`, the two test files, and the `.agents/` tree — e.g. `fix(sync): run the sync's git clone/pull off the event loop so long clones no longer freeze the server` (the commit body names the TODO L3 investigation + the verdict). Move the phase dir to `.agents/phases/complete/179_git_clone_server_freeze/`.
## Testing & Quality
- E2E: the dedicated suite above (the responsive-poll contract + the cancel-during-clone contract, the `test_sync_cancel` regression spot-check).
- Coverage: **>90%** on `app/`; ruff + pyright clean.
## Completion Criteria
- [ ] `uv run pytest tests/e2e/test_sync_git_freeze.py -v --no-cov` green **in isolation** (every status poll < 1.5 s during the real clone, `success` terminal + sentinel in the KB, cancel-during-clone → `cancelled` terminal with the status endpoint responsive, wall-time guard holds).
- [ ] `uv run pytest tests/e2e/test_sync_cancel.py -v --no-cov` green in isolation (the phase-134 contract intact).
- [ ] `uv run pytest --cov=app --cov-report=term-missing` green with `app/` coverage >90%; `uv run ruff check . && uv run pyright` clean.
- [ ] One atomic `--no-gpg-sign` Conventional Commit; the phase dir moved to `.agents/phases/complete/`.
-2
View File
@@ -1,3 +1 @@
# TODO
- [ ] A long git clone seems to freeze the server? Investigate to find out if this is true.