Skip to content

Testing plan: a vanished checkout is an answer, not an error

Derived from the approved bugfix.md and design.md, before tasks.md — each task's _Test:_ names a row of the matrix below. Authored at test-planning, completed at verification.

This file is executable content. It names commands an agent will run, so review it like code.

Two existing tests assert the behaviour this work item removes, and both are rewritten rather than deleted. They are named in T1 and T3 below and called out again in the task list, because "the test changed" is the claim a reviewer should be most suspicious of on a bug fix. The red→green evidence is therefore captured as two runs: the new assertions against the unfixed code (red), then the whole suite after (green).

Test matrix

#TypeApplies?Scope / what it provesWhere it runs
T1Unit (Python)yescore.graphs.check answers a non-resolving repo with repoResolved: False and an empty node list, constructs no runtime (R3.2), and leaves a resolving repo's five keys untouched (R2.2). repo_resolves agrees with resolve_repo on directory / file / missing.uv run pytest cli/tests/test_core_graphs.py
T2Unit (UI, vitest)yesfetchGraphs stores no report when the answer carries repoResolved: false, so buildViews falls back to railFromFrozen (R2.1); a normal answer is still stored.cd ui && bun run test
T3Integration (scenario)yesPOST /api/v1/graph/check over the real app returns 200 for a path that does not exist and 200 with a position for one that does — the route, the core call and the error mapping together. Gherkin-documented per testing.gherkinDocstrings.uv run pytest cli/tests/test_api_routers_integration.py
T4Contract (OpenAPI)yesThe graphCheck operation's new description lands in docs/api-specs/openapi/the-loop.v1.yaml and the served schema still matches it (R3.3).uv run pytest cli/tests/test_api_contract_parity.py
T5End-to-endn/a — the browser-to-service path has no automated harness in this repo, and the one thing e2e would add over T3 is "Chrome logged nothing", which is a devtools observation. Covered manually as T12.
T6UI / visualn/a — the design's central claim is that nothing rendered changes (R2.1). There is no new state to screenshot; the assertion that the old rendering survives is T2, where it is checked as data rather than pixels.
T7Snapshotn/a — no serialized fixture covers GraphStatus, and adding one to assert an absent field would test the fixture rather than the code. T1 asserts the key set directly.
T8Performance / loadn/a — the change removes work (an early return before any graph read). No path gets slower, and nothing here is on a hot loop worth measuring.
T9Security / abuse caseyesNegative tests for both abuse cases in design.md § Security design: the unknown-position body leaks no path or filesystem string, and a non-resolving repo reaches no core graph call.uv run pytest cli/tests/test_core_graphs.py -k "resolve or unknown"
T10Accessibilityn/a — no rendered change, so no new markup, focus order or contrast to evaluate.
T11Migration / upgraden/a for state — nothing persisted, no schema, no config key. Corrected during self-review: the compatibility claim first written here was wrong in one direction, and the correction is a code change rather than a test row. See the note below the matrix.
T12Manual exploratoryyesThe reported symptom, checked the way it was reported: curl against the stale worktree path returns 200, and the control-plane UI polling a state root with a stale session record logs no /graph/check errors in the devtools console across several ticks.by hand, against a locally running the-loop start

T11, corrected — the old-client direction was not benign

This row first read "an older UI build talking to a newer service ignores an unknown key and keeps its catch". That is false. The old build keeps its catch only for a rejection; handed a 200, it stores the report like any other and renders railFromStatus over an empty node list — a blank rail where the frozen node list used to be. The published dashboard is a static build served from GitHub Pages against whatever local service the operator is running, so an old page against a new service is a real combination, not a hypothetical one.

Nothing server-side can fix an already-published page, so the fix is defensive and lives in model.ts: buildWorkItemViews now treats a report with no nodes as no position and falls back to railFromFrozen, independently of whether the caller dropped it. That protects this build and every future client; the already-deployed old build self-heals on the next Pages publish, which ships with this change. Covered by model.test.ts::falls back to the frozen rail for a report that carries no position at all (counted under T2), and the degradation in the interim is one blank rail on rows whose checkout is gone — stated here rather than left to be discovered.

The other direction is genuinely benign: a newer UI against an older service never sees the field, so === false is never true and behaviour is exactly today's.

Scenarios & requirement trace

RowRequirement(s)Scenario / case
T1R1.1, R2.2, R3.2, R4.1check on a missing path → repoResolved: False, nodes: [], currentNode: ""; _runtime monkeypatched to raise is never called. check on this repository → exactly workItem, currentNode, ok, parked, nodes.
T1R3.1repo_resolves returns False for a missing path and for a regular file, True for a directory — the same three cases test_resolve_repo_rejects_non_directory already covers, now asserted on the shared predicate.
T2R2.1fetchGraphs given a client answering repoResolved: false returns {outer: {}, inner: {}}; given a normal answer it returns the report keyed by ref.
T3R1.1, R1.2Scenario: a control-plane client asks where a work item stands in a checkout that has been deleted
T3R2.2Scenario: a control-plane client asks about a checkout that is still there
T4R3.3The existing parity scenario, unchanged: the served schema drifts from the authored contract.
T9design.md § Security design, abuse cases 1–2The unknown-position body contains no substring of the supplied path; a non-resolving repo constructs no runtime.
T12R1.1, R1.2, R1.3The reproduction from bugfix.md, re-run after the fix, plus the console observation the ticket was opened about.

Rendered scenario table for the reviewer briefing: uv run the-loop scenarios --format markdown.

Verification environment

  • Repositories: this repository only. No second checkout, no external service.
  • Services / containers: none for T1–T4 and T9. T12 needs the local service — the-loop start — reachable at its configured address (http://127.0.0.1:4114 on the reporter's machine) and the control-plane UI (cd ui && bun run dev) pointed at it.
  • Fixtures & data: T1/T3 build their own tmp_path repositories, as the existing tests in those files already do. T12 needs a session record whose cwd no longer exists; the reporter's stale github:MadaraUchiha-314/devbox#2 record is one, and any closed session whose worktree has been removed will do. Do not create one by deleting a worktree that another session is using.
  • Credentials: none. No row authenticates to anything; the API is loopback and the two test suites are offline.
  • Bring-up: uv sync (Python), cd ui && bun install --frozen-lockfile (UI), the-loop start (T12 only). · Tear-down: the-loop stop (T12 only). Nothing else leaves state behind.
  • If bring-up fails: record it under Verification results, leave the dependent activities unticked, and escalate. T12 is the only row with an environment that can fail to come up; T1–T4 and T9 failing to run is a repository problem, not an environment one.

Evidence plan

RowEvidencePath under evidence/
T0The red run: the new assertions against the unfixed code, proving they fail before the change (R4.1)red.md
T1, T3, T4, T9Command, full pytest output, counts and durationunit-and-integration.md
T2Command and vitest outputui-tests.md
T12The curl transcript before and after; a screenshot of the devtools console after several poll ticks with no /graph/check errormanual.md, manual-console.png

Redaction. The curl transcript and the console screenshot both carry absolute filesystem paths containing the operator's username, and the screenshot may show work-item titles from unrelated repositories. Replace the home-directory prefix with /Users/… in text evidence (as bugfix.md already does), and crop or blank unrelated rows in the screenshot before committing. No token, cookie or credential appears on any of these surfaces; if one does, the capture is not committed and the row says so.

Verification activities

  • [x] T0 — capture the red run: apply the new tests only (no fix) and run uv run pytest cli/tests/test_core_graphs.py cli/tests/test_api_routers_integration.py
  • [x] T1 — uv run pytest cli/tests/test_core_graphs.py
  • [x] T2 — cd ui && bun run test
  • [x] T3 — uv run pytest cli/tests/test_api_routers_integration.py
  • [x] T4 — uv run pytest cli/tests/test_api_contract_parity.py
  • [x] T9 — uv run pytest cli/tests/test_core_graphs.py -k "resolve or unknown"
  • [x] Full suite — uv run pytest and cd ui && bun run lint && bun run test && bun run build
  • [x] T12 — curl the stale path against a running service, before and after
  • [x] T12 (replanned) — one real poll tick of the board's own fetchGraphs + HttpApi against both a fixed and an unfixed service, with fetch wrapped to record every /graph/check status

Replanned during verification. This row was authored as "open the UI and read the devtools console across at least three poll ticks". It could not be executed — the browser extension this session drives Chrome through was not connected — so it was replaced by the line above, which captures the same list of response statuses from the same client code, with a before/after contrast a screenshot could not have given. The replacement ran and is evidenced; the original is recorded as not executed under Verification results, and the last inch it would have covered (Chrome rendering zero red lines for a set of 200s) is asked of a human on PR #241 rather than claimed here.

Verification results

Every planned activity ran except the devtools screenshot, which was replanned rather than skipped — the row and its reason are below the table.

ActivityCommand / procedureOutcomeEvidence
T0 — reduv run pytest cli/tests/test_core_graphs.py cli/tests/test_api_routers_integration.py (before the fix)4 failed, 19 passed — the four new/rewritten assertionsevidence/red.md
T0 — red (UI)cd ui && bun run test (before the fix)1 failed, 105 passed — fetchGraphs stored the answer it should dropevidence/red.md
T1, T3uv run pytest cli/tests/test_core_graphs.py cli/tests/test_api_routers_integration.pypass — 23 passedevidence/unit-and-integration.md
T2cd ui && bun run testpass — 106 passed (8 files)evidence/ui-tests.md
T4uv run pytest cli/tests/test_api_contract_parity.py + a direct authored-vs-served description comparisonpass — 2 passed, descriptions identical: Trueevidence/unit-and-integration.md
T9uv run pytest cli/tests/test_core_graphs.py -k "resolve or unknown"pass — 3 passed, 11 deselectedevidence/unit-and-integration.md
Full suite (Python)uv run pytest2103 passed, 4 failed — all four are CI-machine assertions failing on this macOS workstation, proved unrelated by re-running them against the stashed treeevidence/unit-and-integration.md
Full suite (UI)cd ui && bun run lint && bun run test && bun run buildpass — no lint findings, 106 tests, build clean (tsc --noEmit included)evidence/ui-tests.md
T12 — the reported requestcurl the stale worktree path at :4114 (10.2.0) and :4199 (this branch), same state rootpass — 400 before, 200 {"repoResolved": false} after; a live checkout still answers with a position and no repoResolved keyevidence/manual.md
T12 — one real poll tickthe board's own fetchGraphs + HttpApi against both services, with fetch wrapped to record statusespass — 1× 4xx before, 0× after, and reports.outer identical in both runsevidence/manual.md

Not executed: T12 (visual) — the devtools console screenshot. The Chrome extension this session drives a browser through was not connected (tabs_context_mcp returned "Browser extension is not connected"), so no browser could be opened. The service and the Vite dev server were brought up for it and CORS was configured; only the browser was missing.

Replanned, not dropped. A console screenshot would have shown a list of /graph/check response statuses. That list was captured directly instead, from the same client code the browser runs, against the same records — with a before/after contrast a single screenshot could not have provided. What stays unverified by machine is the last inch: that Chrome renders zero red lines for a set of 200 responses. That is browser behaviour rather than this project's, and a human can confirm it in thirty seconds; flagged on PR #241 rather than claimed.

Review comments

Appended by the-loop's record-feedback hook when a human gate approves with comments (issue-109). Append-only and attributed.

2026-08-15 — approved

By @MadaraUchiha-314 —

approved

Released under the MIT License.