fix(e2e): per-run databases + specs rewritten against the engine - #13
Merged
Conversation
…the engine The red `e2e` job was two independent defects, not one. What actually failed the job: all four Playwright specs predated the engine rewrite and asserted on the retired LangGraph chat flow. Every failure in the CI log is `getByText(/Clarifying question #1/)` -> "element(s) not found". The engine runs the clarify Q&A loop inside the worker (product_owner auto-answers clarifying_pm), so the browser goes straight from "Start" to the PRD gate and those chat turns never exist. The `database is locked` tracebacks in the same log were real, but they were server-side noise rather than the failing assertion. The specs are rewritten rather than retired -- the engine's flow is fully drivable through the UI, so retiring the job would have discarded real coverage. clarify-gate.spec.ts covers the gate opening with the PRD rendered, the rejection loop re-running the phase, and reload rehydration from the engine snapshot; full-run.spec.ts approves both gates and drives all six phases to completion. The lock defect is separately real and also fixed. backend/main.py passed a constant `data/web.db` into every start_run, and each start_run boots its own state-server PROCESS. The store's single-writer guarantee is an asyncio.Lock, which serialises nothing across processes, so two live runs put two OS writers on one SQLite file. Each run now takes a unique sibling of APPFORGE_WEB_DB, which is what makes the README's load-bearing "single SQLite writer" claim actually true rather than true-only-while-one-run-is-alive. Evidence, A/B in both directions. Engine level, 4 concurrent runs: shared DB -> 4/4 runs failed with 2 workers killed by `database is locked`; per-run DBs -> 4/4 done, 13/13 tasks each, 0 lock deaths. Browser level, same suite: without the fix it passes but with 3 lock-induced worker crashes; with it, 0. The contention also launders lock errors into task retries that can fail a run outright, so the unfixed build was a live flake source for the new suite. Backend suite 155 -> 156, coverage 86.99%. Verified with data/ absent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last known functional defect blocking the v1.0.0 release. It turned out to be two independent problems, and the handoff misattributed which one was red.
What actually failed the
e2ejobNot
database is locked. All four Playwright specs were last touched 2026-06-02 and assert on the retired LangGraph chat flow — every failure in the CI log isgetByText(/Clarifying question #1/)→ "element(s) not found".The engine runs the clarify Q&A loop inside the worker (
product_ownerauto-answersclarifying_pm,agent_adapter.py::_run_clarify_loop), so the browser goes straight from "Start" to the PRD gate and those chat turns never exist. Thedatabase is lockedtracebacks in the same log were real, but they were concurrent server-side noise, not the failing assertion.Specs rewritten, not retired
The handoff flagged retiring the job as a user-facing call. It isn't needed — the engine's flow is fully drivable through the UI, so retiring would have discarded real coverage.
clarify-gate.spec.tsfull-run.spec.tsThe lock defect, confirmed and fixed
backend/main.pypassed a constantdata/web.dbinto everystart_run, and eachstart_runboots its own state-server process. The store's single-writer guarantee is anasyncio.Lock— it serialises nothing across processes, so two live runs put two OS writers on one SQLite file.Each run now takes a unique sibling of
APPFORGE_WEB_DB. That is what makes the README's load-bearing "single SQLite writer" claim actually true, rather than true-only-while-one-run-is-alive — which is why this was preferred over serialising runs.Evidence (A/B, both directions)
Engine level, 4 concurrent runs, gates driven:
database is lockedBrowser level, this same suite: without the fix it passes but logs 3 lock-induced worker crashes; with it, 0. Contention also launders lock errors into task retries that can fail a run outright, so the unfixed build was a live flake source for the new suite.
Also ruled out, as the handoff advised:
busy_timeoutis not involved — the failing path is a cross-process write conflict, not a timeout.Verification
All run with
data/absent (mv data data.bak), since a stale localdata/is exactly how the7bfa00dbug reachedmain.uv run pytest tests/— 156 passed (was 155), coverage 86.99% (gate 70%)cd e2e && npx playwright test— 4 passed in ~1.1m, 0 lock errorscd frontend && npm test— 28 passeduv run ruff check backend/ tests/·uv run black --check backend/ tests/— cleanNotes / deliberate choices
data/is gitignored scratch and a per-project database is useful for post-hoc inspection, so deleting was the more destructive option. They do accumulate over a long local session — logged as a known gap indocs/STATUS.md.PlanViewerreadsadr/tasks/design_specfrom the store and only the reload (project_state) path populates them. Reloading shows it. Logged, not touched.