Skip to content

Close the two finished-session races the E2E stories surfaced - #1528

Merged
brillout merged 1 commit into
mainfrom
claude/backend-e2e-mock-tests-t7ci4f
Aug 10, 2026
Merged

Close the two finished-session races the E2E stories surfaced#1528
brillout merged 1 commit into
mainfrom
claude/backend-e2e-mock-tests-t7ci4f

Conversation

@brillout

Copy link
Copy Markdown
Member

What

The two product findings documented in #1527 (src/e2e/spec.md) are now fixes, and the stories that found them act inside the old race windows on purpose — they are the regression tests.

1. Acting on a session the moment it flips done no longer races teardown

The child writes status: done and exits; the daemon's teardown then archives the history, commits the bookkeeping to the run branch, and retires the worktree. A Push / Open-PR / Remove / Delete / Resume fired inside that window ran its own git against the same checkout: the loser reported "could not commit the work this session left uncommitted", and a teardown that lost stranded a worktree it should have removed.

Fix: every mutator of one run's checkout serializes on a per-run lock (src/run-locks.ts, keyed by resolved checkout path):

  • tearDownWorktree and continueWorkspace (daemon-runtime),
  • the commit step of sendPushBranch / sendOpenPullRequest,
  • sendRemoveWorktree / sendDeleteSession.

Both actors live in the daemon process by design (RPCs are served in-process; only the daemon writes to the project checkout), so an in-process lock is the whole fix. Whichever side wins commits the entire pending state (add -A); the other finds a clean tree — or no checkout, which commitSessionWork already reads as "the branch is authoritative".

2. The live event feed follows the journal into the archive

Teardown copies events.jsonl verbatim into the archive and removes the worktree. When fs.watch missed the final appends, the 1s poll found the file gone and the tail never delivered anything again — including the end the transcript needs.

Fix: tailRunEvents (events-tail.ts) re-resolves the journal's home when the tailed file disappears after having existed, and retargets the same tailer, carrying its read offset — the copy is content-identical, so exactly the missed lines arrive, once, with no replay. Details that matter:

Test changes

  • The publish story now clicks Push the instant the row flips done and asserts the push succeeds and teardown still retires the worktree; the resume story fires its continuation at done the same way.
  • The harness's close() drains in-flight teardowns by acquiring each run's lock before deleting fixture repos — the stranded-worktree noise in stress runs turned out to be the harness deleting repos under mid-flight teardowns, not a product fault.
  • New unit tests: 4 for withRunLock (ordering, isolation, failure hygiene, key normalization), 3 for tailRunEvents (missed lines exactly once across the move; no replay of a fully-consumed journal; idling while the resolver has no better answer).

Verification

  • Full suite: 1713 tests, 0 failures (7 new).
  • Stress: lifecycle story file 15×, each other story file 6× — all green with zero "keeping worktree" warnings (previously 1–2 per run).
  • Raw-git probe confirmed sibling-worktree commits don't contend on their own — the serialization is only needed against teardown, which is exactly what the lock scopes.
  • dist/ ships run-locks.js; src/e2e stays excluded from the publish build. Changeset included (patch).

Generated by Claude Code

Both product findings from #1527 are now fixes, and the stories that
found them act inside the old race windows on purpose, as regression
tests.

1) Acting on a session the moment its meta flips done raced teardown.
The child writes `status: done` and exits; teardown then archives the
history, commits the bookkeeping to the run branch, and retires the
worktree. A Push/Open-PR/Remove/Delete/Resume fired inside that window
ran its own git against the same checkout: the loser reported "could
not commit the work this session left uncommitted", and a teardown
that lost stranded a worktree it should have removed. Every mutator of
one run's checkout now serializes on a per-run lock (run-locks.ts) —
teardown, the handoff RPCs' commit step, the worktree removal RPCs,
and continuation reuse. Both actors live in the daemon process by
design, so the in-process lock is the whole fix.

2) A live event tail whose journal was retired went silent without the
final events. Teardown copies events.jsonl verbatim into the archive
and removes the worktree; when fs.watch missed the last appends, the
poll backstop found the file gone and delivered nothing again —
including the run's `end`. tailRunEvents (events-tail.ts) re-resolves
the journal's home when the tailed file disappears and retargets the
same tailer, carrying its read offset, so exactly the missed lines
arrive, once. The first pull after a retarget adopts the copy's mtime
so the #567 same-length-rewrite check cannot misread the move as a
truncation and replay the feed. Wired into the dashboard's onEvents
(with a guard against relocating onto the project-root journal, which
after a Delete is another run's feed), the device relay tail, and the
E2E harness — which drops its private archive-swap workaround for the
product seam.

The publish and resume stories now fire at the instant the run flips
done; the harness's close() drains in-flight teardowns by acquiring
each run's lock before deleting the fixture repos, which is what the
stranded-worktree noise in stress runs turned out to be.

Verified: full suite 1713 tests green; the two race stories plus every
other story file stressed 6-15x each with zero failures and zero
stranded-worktree warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKihCTfcazhDgt4dhUmfM9
@brillout
brillout merged commit 0ae46f8 into main Aug 10, 2026
1 check passed
@brillout
brillout deleted the claude/backend-e2e-mock-tests-t7ci4f branch August 10, 2026 15:49
brillout pushed a commit that referenced this pull request Aug 10, 2026
sdd.md scopes spec.md files to business logic and explicitly excludes
test code, and specs are plain-language product docs — the e2e spec.md
was both misplaced and written as technical harness documentation.

- src/e2e/spec.md becomes src/e2e/README.md (developer doc, where
  telefunction names, env vars, and file references belong); the five
  source-comment references follow the rename.
- The two product behaviors #1528 shipped move to the specs that own
  them, in spec language: acting on a session the instant it finishes
  is safe (product spec, teardown) and a live stream follows the log
  into the archive without dropping the ending (dashboard-rpc spec).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKihCTfcazhDgt4dhUmfM9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants