test(daemon): seeded concurrency torture lane for session/lease/lock invariants - #1439
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
Size Report
Startup median (7 runs, lower is better):
Top changed chunks: no changes in the largest emitted chunks. |
|
Review findings for
Residual scope: Checks: Integration Tests, typecheck, lint/format, layering, Fallow, and no-test-only-DI are green on this head. Coverage is red only for the independently reproducible inherited |
4fba061 to
72f48d6
Compare
|
@thymikee thanks — all four review points addressed in 1 (P1) — real lock plan, revert-sensitive. The lane no longer hand-writes 2 (P1) — #1430 envelope. The nightly now emits the standard envelope via 3 (P2) — determinism + contention proof. Replay now asserts equality on the full ordered scheduler trace ( 4 (P2) — locality. Residual scope. Agreed and now stated precisely in
|
|
Re-review at head f5abcf6: request changes. The split and imported lock-plan helper are improvements, but five blockers remain. First, the harness models lock grants and calls resolveRequestExecutionLockKeys directly; it does not exercise shouldLockSessionExecution through runLocked and withRequestExecutionLocks, so bypassing the production application path can still leave this lane green. Second, the claimed forced same-device test calls runTorture without pinnedDevice, so doOpen still randomly picks from DEVICE_POOL. Third, the sweep writes a passing envelope before the separate contention test runs; that later test can fail while the always-uploaded envelope still says pass. Fourth, issue 1430 remains open and this PR adds no shared health/freshness watcher, so the scheduled lane is not born monitored. Fifth, exact replay is only checked in the explicit TORTURE_SEED branch, not the normal CI/nightly sweep. All reported PR checks are green and local default/explicit replay validation passed, but they do not close these guardrail gaps. |
|
@thymikee thanks — pushed 1. Production application-path coupling ( 2. Forced same-device test wasn't actually pinned. Fixed — it now passes 3. Envelope could publish 5. Replay only checked under 4. #1430 shared health/freshness watcher. This one I want to confirm scope on. #1430 is a separate Track E issue whose deliverables are (a) the standard envelope — which #1416 "MUST ship with" and now does — and (b) "a scheduled health job that reads last-run/last-success for every |
|
Re-review at head 0890152: pinnedDevice contention, whole-lane result handling, and replay during the normal sweep are fixed. Two P1 blockers remain. First, the harness still manually mirrors shouldLockSessionExecution and resolveRequestExecutionLockKeys, then grants modeled scheduler locks; it never exercises createRequestExecutionScope().runLocked() through withRequestExecutionLocks. Breaking or bypassing the production lock application can therefore leave this lane green. Add a real-scope same-device contention guard or narrow the claim. Second, issue 1412 requires issue 1430 health/freshness monitoring to land with new scheduled lanes; this PR still only uploads an envelope and cannot detect a lane that misses two cadences. The envelope duration also excludes the later forced-contention guard. Current PR checks are green, but these architecture/observability gaps still block readiness. |
|
@thymikee pushed P1 #1 — real-scope serialization guard (closed). Added Envelope duration (fixed). P1 #2 — #1430 health/freshness monitoring. I've asked the repo owner to make the scope call rather than guess, because this is where I need direction: the cross-workflow "reads last-run/last-success for every |
|
Re-review at head 8e9001e: the real production lock application guard is fixed and Integration Tests exercise createRequestExecutionScope().runLocked through withRequestExecutionLocks. Two P1 blockers remain. First, default and forced sweeps include seed 0 and failures print TORTURE_SEED=0, but optionalIntFromEnv rejects values less than or equal to zero; the promised replay command therefore cannot replay a seed-0 failure. Allow non-negative replay seeds separately from positive client/op counts and add a seed-0 regression. Second, issue 1412 still requires issue 1430 health/freshness monitoring to land with this scheduled lane; this branch has only the envelope, not a cadence watcher. Current checks are green, but these contracts remain unfulfilled. |
|
@thymikee good catch — pushed P1 #1 — seed 0 unreplayable (fixed). P1 #2 — #1430 monitoring. This is a deliberate scope call I've put to the repo owner rather than deciding unilaterally, and it's the one open item. My read: #1416's stated contract ("new lanes MUST ship with the standard envelope") is satisfied — the lane emits the envelope and carries a |
|
Re-reviewed the latest delta. The seed-0 replay blocker is fixed: One required blocker remains: #1430 cadence health/freshness is still not implemented on this branch. The scheduled lane emits an envelope, but nothing detects or reports two missed/failed cadences, so the lane can silently go dark. No new regression was found in this one-file delta. |
|
@thymikee the maintainer asked me to land the watcher here, so #1430's cadence health/freshness monitoring is now in this PR — pushed New
Structure / testability: all decision logic is pure in New lanes need zero wiring: emitting the standard envelope + a |
|
The #1430 watcher plumbing is now present, but the two-cadence rule is still violated. Current tests cover two failures and an old success, but not the required zero-run/one-failure grace. Carry enough first-observed/registration timing to distinguish a newborn lane, require two completed failed cadences (or equivalent elapsed evidence), and add both grace regressions. All current checks are green; there is still no live scheduled/API alert run. |
|
Re: the two-cadence rule violation — fixed in
Added both grace regressions in
The existing dark/failing tests now register the lane well past the grace window so they still assert unhealthy. On the live alert run: the watcher is |
|
The model-level zero-run/one-failure grace is fixed, but the production anchor still makes the generic #1430 contract wrong. The new tests inject a recent timestamp and miss this mapping. Derive or durably record the schedule-introduction/first-observed time and test the production mapping. All head checks are green; no live scheduled/API issue-write run exists yet. |
|
Re: the production anchor still being wrong ( New pure mapping
So adding a schedule to an old workflow now anchors on the recent introduction commit, not the ancient file age. The watcher workflow checks out with Tested the production mapping directly (not just injected timestamps):
Model suite 16/16; |
|
Moving off workflow The tests only inject a timestamp and never exercise this Git mapping, so add a real old-workflow/later-schedule history regression. Also replace raw Checks are green; no live scheduled/API issue-write evidence exists. |
|
All three points addressed in 1. Semantic transition, not literal pickaxe / author time. Replaced the 2. Real git-history regression. New
3. Model + run suites 21/21; |
|
The implementation findings are fixed: schedule activation is now semantic, newest-transition, first-parent, committer-time based; raw process execution is gone; and real Git-history fixtures cover old workflow activation, comments, remove/re-add, and absent paths. One required regression gap remains: every fixture is linear and sets author/committer dates equal, so removing All head checks are green; no live scheduled/API issue-write run exists yet. |
|
Done — added the merge-commit regression in New
It asserts Model + run suites 22/22; |
|
Re-reviewed the latest delta: the merge-history regression now genuinely distinguishes feature commit time, merge author time, and default-branch merge committer time, so removing Residual validation risk: Scheduled Lane Health has not yet executed from the default branch or exercised the API issue-write route. |
|
Re: the residual validation risk on Scheduled Lane Health — the "runs from the default branch" half is inherently impossible pre-merge (the API only surfaces
Gates green: model + run suites 25/25, format/lint/typecheck/fallow clean. That leaves only the true end-to-end "scheduled run on |
|
Re-reviewed the post-readiness delta: clean. It adds focused fetch-stub coverage for opening a new alert, pinging the matching existing issue, ignoring pull requests/nonmatching titles, and preserving auth/payload contracts. Schedule-activation semantics and merge-history regressions are unchanged; all head checks, including No test-only DI seams and Fallow, are green. Residual risk: the real default-branch/API issue-write route has not executed yet. |
|
Thanks for the re-review. Agreed on the residual — the real default-branch/API issue-write route can only fire once this is on |
|
Reviewed against #1416 including the deterministic-scheduler amendment. Approve with changes — the torture lane itself is the strongest implementation in this wave and needs no rework: the cooperative-fiber scheduler genuinely makes a seed determine the full interleaving (fibers only yield through scheduler-owned promises; the un-instrumented microtask windows were checked and can't race because every other fiber is parked on a scheduler-owned gate), Changes before merge:
🤖 Review by Claude Code |
|
All three addressed in P1 — #1430 watcher removed. Deleted P2 — torture lane is no longer an accidental PR run. Moved it to P3 — no-shrinking gap filed as #1442 (follow-up to #1416), noting Local gates green (format/lint/typecheck/fallow); torture lane 4/4 from the new path. The |
…op the bundled lane-health work (#1414) - Strip scripts/scheduled-lane/* and scheduled-lane-health.yml: that watcher is #1430's own deliverable and collides with PR #1439's implementation of the same lane. What this lane owes (a per-run envelope) moves into scripts/fuzz/envelope.ts. - Rebase onto #1437 and rebuild the generator layer on fast-check: cases come from arbitraries sharing SELECTOR_VALUE_HAZARDS with the property suite, and counterexamples are shrunk, so a failure names a minimal input plus fast-check's seed/path instead of a 20k-char random string. - Route harness.test.ts into the serialized subprocess-stub project. - Drop the AGENT_DEVICE_FUZZ_STARTUP_DELAY_MS test seam: the ready handshake is now proven by a case budget far below real worker startup. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
P2 — After moving the lane under |
|
Re P2 doc consistency — fixed in |
|
Re-reviewed |
|
Re-reviewed after the rework: the #1430 unbundle (66a2dd9) and the nightly-only move both land cleanly — diff is down to 1,855 all-torture-scoped lines, checks green. One remaining item before merge: Rebase onto main and adopt the shared envelope. This branch's merge-base (e8b779c) predates the #1441 merge, which established After that: good to go from my side; the deterministic-scheduler core needed no changes in either review pass. 🤖 Review by Claude Code |
…invariants Refs #1416 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- derive each op's lock plan from production resolveRequestExecutionLockKeys via a fake device-inventory provider, so reverting the router's same-device serialization trips the overlap invariant (verified) - assert exact replay: full scheduler trace, terminal outcome, contention - assert real same-device lock contention in the sweep + a forced 2-client case - split harness into bindings/invariants/envelope modules (all <500 LOC) - emit #1430 scheduled-lane envelope (schema/SHA/hash/seed range/duration/result) and upload it from the nightly workflow Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…lay + forced-device contention; whole-lane envelope Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…e envelope duration Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…D must accept 0) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Discovers schedule: workflows from .github/workflows/, reads recent scheduled runs via the GitHub API, and opens/pings a tracking issue when a lane misses or fails two consecutive cadences. Pure model unit-tested and gated on PRs; API I/O + issue open/ping run nightly. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…w age Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…h runCmdSync Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…itter time Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… fetch Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Strip the scheduled-lane-health watcher (scripts + workflow + PR gate) — it is #1430's deliverable and collides with PR #1438's workflow of the same filename; keep only this lane's #1430 envelope writer. Move the torture lane under test/integration/nightly/ so it is out of the test:integration:node glob, and run it via an explicit, disclosed PR step plus the nightly sweep. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Rebase onto main (post-#1441) and replace the lane-local LaneEnvelope dialect with the shared scripts/lib/lane-envelope.ts builder, so the #1430 health watcher parses one schema: commitSha->commit, sourceHash-> configHash, seedRange/runs moved into the typed data payload, and the sweep encoded as seed "<start>-<end>". Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
b39bc09 to
551b68e
Compare
|
Done in Adopted the shared envelope. The lane's local
{ "schemaVersion": 1, "lane": "concurrency-torture", "commit": "…", "configHash": "…",
"seed": "0-4999", "startedAt": "…", "finishedAt": "…", "durationMs": …, "result": "pass",
"data": { "issue": 1416, "seedRange": { "start": 0, "end": 5000 }, "runs": 5000 } }Re "not reimplementing recently-merged work": I also swept the post-#1441 tree for other torture-adjacent helpers — there's no shared seeded PRNG or deterministic scheduler to consume (the lane's Updated the nightly workflow comment + |
|
Re-reviewed |
…s, never hangs (#1414) (#1438) * test: nightly parser fuzz lane with typed-AppError invariant (#1414) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): run envelope, artifact promotion, and harness self-check tests (#1414) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): shared scheduled-lane envelope on every terminal path, watchdog after ready (#1414) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): envelope for malformed options; add scheduled-lane health consumer (#1414, #1430) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(lanes): actions:read scope, terminal error envelope, first-due grace (#1414, #1430) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(lanes): anchor first-run grace to schedule registration, use exec helper in tests (#1414, #1430) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(lanes): portable POSIX pickaxe pattern for schedule registration (#1414, #1430) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(fuzz): fast-check generators over the shared hazard list, drop the bundled lane-health work (#1414) - Strip scripts/scheduled-lane/* and scheduled-lane-health.yml: that watcher is #1430's own deliverable and collides with PR #1439's implementation of the same lane. What this lane owes (a per-run envelope) moves into scripts/fuzz/envelope.ts. - Rebase onto #1437 and rebuild the generator layer on fast-check: cases come from arbitraries sharing SELECTOR_VALUE_HAZARDS with the property suite, and counterexamples are shrunk, so a failure names a minimal input plus fast-check's seed/path instead of a 20k-char random string. - Route harness.test.ts into the serialized subprocess-stub project. - Drop the AGENT_DEVICE_FUZZ_STARTUP_DELAY_MS test seam: the ready handshake is now proven by a case budget far below real worker startup. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): replay the regression corpus through the worker watchdog (#1414) A promoted hang case used to wedge the unit job until the CI timeout, because corpus replay called checkCase in-process. It now goes through the same worker-backed watchdog the nightly lane uses, so such a case fails against a 5s per-case budget; the file moves to the serialized subprocess-stub project with the rest of the worker-driven fuzz tests. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): let the watchdog outlive vitest's default case timeout (#1414) A wedged parser was surfacing as a bare 'Test timed out in 5000ms' instead of the named hang: failure that says which input wedged, because the file's vitest timeout was shorter than the watchdog budget times the number of replayed cases. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): complete drift provenance in the lane envelope (#1414) configHash now covers every input that decides what a seed generates (generate.ts and the shared property arbitraries, not just the arbitraries/targets/invariant), and tool records fast-check's installed version. A generation-loop edit or a fast-check upgrade previously changed the case set while the envelope looked unchanged. A test recomputes the hash with each input omitted so a future omission fails. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Summary
Adds a deterministic, seeded concurrency torture lane (#1416, umbrella #1412 Track A) that turns accidental ordering failures in session/lease/lock handling into a reproducible test asset. N logical clients drive randomized-but-seeded interleavings of
open/mutate/close/takeover/killagainst the realSessionStore+LeaseRegistry(plus an in-memory device-claim model). After every run it asserts: no leaked leases or claims, no cross-session state bleed, every lock released after owner death, and the session store stays consistent — and it pins the router's same-device serialization under 100+ interleavings.Why a scheduler, not just a seed (issue review amendment)
So all concurrency is routed through an instrumented dispatcher — the single source of ordering. Every client runs as a fiber that only advances when the scheduler resumes it, and mutual exclusion is granted by the scheduler too:
Determinism contract that makes replay exact: fibers yield only through scheduler-owned promises; everything between yields is synchronous (the real
SessionStore/LeaseRegistrynever await); no wall clock, no timers, no real I/O. A seed therefore fully determines execution order, so failures replay bit-for-bit.What is real vs modeled (the amendment's "say which and why")
SessionStore(session map/consistency) andLeaseRegistry(allocation, per-device exclusivity, release, scope checks).createRequestExecutionScopebuilds it: gate on the production decisionshouldLockSessionExecution(command)(daemon-command-registry.ts), then resolve keys viaresolveRequestExecutionLockKeys(request-binding.ts) driven with a fake inventory through the productionwithDeviceInventoryProviderseam. Only the mutex grant is scheduler-modeled, becausewithKeyedLock's native microtask hand-off can't be reproduced from a seed. Reverting either production decision trips the overlap invariant, so the lane is coupled to production lock resolution, not a duplicate of it.real-scope-serialization.tsguard drives concurrent same-device opens through the actualcreateRequestExecutionScope().runLocked()→withRequestExecutionLocks→withKeyedLockand asserts no overlap, so a regression in the production lock application path turns the lane red.device-claims.tsis a filesystem/OS process lock) and process "kill" — both out of scope for a scheduling-torture lane (test: seeded concurrency torture lane for session/lease/lock invariants #1416: no real devices / wall-clock stress).InMemoryClaimRegistrypreserves the invariants (one live claim per device key, owner-token-gated clear, dead-owner reclaimable).Value demonstrated
Beyond passing 100+ interleavings (default 128, verified up to 2000 seeds locally), the lane bites: temporarily granting each same-device critical section a non-shared lock key makes it fail deterministically and print the seed + replay command, e.g.
Files
test/integration/nightly/concurrency-torture.test.ts—node --testentry: sweepsTORTURE_RUNSseeds (default 128) or replays one viaTORTURE_SEED; prints the seed on any failure and self-checks replay determinism for every swept seed.test/integration/nightly/concurrency-torture/{prng,deterministic-scheduler,claim-registry,harness,bindings,invariants,real-scope-serialization,envelope}.ts— seeded splitmix32 PRNG, the dispatcher, the claim model, the world/invariants, the production lock-plan bindings, the real-scope guard, and the obs: scheduled-lane health, freshness telemetry, and standard artifact envelope #1430 envelope writer..github/workflows/concurrency-torture-nightly.yml— nightly (0 5 * * *) + manual sweep over a large seed range; offline, no devices. Emits/uploads the standard obs: scheduled-lane health, freshness telemetry, and standard artifact envelope #1430 envelope..github/workflows/ci.yml— explicitRun seeded concurrency torture lane (fast PR sweep)step in the Integration job.docs/agents/testing.md— documents the lane, theTORTURE_SEED=<n>replay flag, and the real-vs-modeled boundary.package.json—test:concurrency-torturescript.CI route (nightly lane, disclosed fast PR sweep)
The lane lives under
test/integration/nightly/, deliberately out of thetest:integration:nodeglob (node --test test/integration/*.test.ts, non-recursive) so it is not an accidental PR-time run. Both workflows point at it explicitly:Run seeded concurrency torture lane (fast PR sweep)step in the Integration job runs the fast default sweep (128 seeds, sub-second).Concurrency Torture Nightlysweeps a much larger seed range and uploads theconcurrency-torture-envelopeartifact.Seed replay (documented)
Scope notes
schedule:trigger).fc.scheduler()isn't a drop-in because this harness also arbitrates contended-mutex winners) — filed as follow-up Concurrency torture lane: minimize failing interleavings (shrinking) #1442.Validation
pnpm format:check,pnpm lint,pnpm typecheck, andpnpm check:falloware green; the torture lane passes from its new path. Note: the lane requires Node ≥22.18/24 to strip.ts(andpnpm build/check:affectedneed it too), matching CI's Node — a local Node 22.14 needs--experimental-strip-types.Closes #1416
Link to Devin session: https://app.devin.ai/sessions/b043907331814d8fa203bdbe2f529191
Requested by: @thymikee