test: nightly parser fuzz lane — parser input fails as typed AppErrors, never hangs (#1414) - #1438
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. |
|
Runtime verification — parser fuzz lane ✅Tested end-to-end from the shell on this branch (Node 22, no UI). Failure detection actually works (the key claim)Temporarily injected faults into The printed Green runs, determinism, scale
Corpus, replay lane, workflow
Replay-script quoted-token fix (before/after)Repo tree clean ( |
d70a045 to
cf3c5d0
Compare
Review: changes requestedThis is directionally solid—the target registry, deterministic mutator, worker cursor, and replay-script regression are good foundations—but it is not ready for the #1412/#1414 contract yet.
Readiness evidence is also incomplete: GitHub has no |
|
@thymikee addressed all three in [P1] Scheduled-lane envelope. #1430 is not merged, so there is nothing to consume yet — I implemented the equivalent here rather than blocking. { "schemaVersion": 1, "lane": "parser-fuzz", "result": "pass",
"startedAt": "…", "finishedAt": "…", "durationMs": 2229,
"provenance": { "commitSha": …, "ref": …, "workflowRunId": …, "workflowRunAttempt": …,
"nodeVersion": "v22.14.0", "tool": "scripts/fuzz/run.ts", "corpusEntries": 8 },
"config": { "mode": "generate", "seed": 1, "iterations": 300, "caseTimeoutMs": 2000, "targets": [...] },
"targetRuns": [ { "target": "cli-args", "cases": 300, "failures": 0, "durationMs": 490 }, ... ],
"failures": [ /* incl. artifact path */ ], "reproCommands": [ ... ] }The upload step is now [P1] Artifact → corpus promotion. Correct — the replay path returned before promotion could run. Tested end-to-end in [P2] Tests of the invariant/watchdog itself. Added Writing that test found a real watchdog bug: worker startup (type stripping + parser imports) counted against the per-case budget, so under load a fast untyped throw could be misreported as a On the readiness evidence: a Locally green: |
|
Re-review at head 5a382f6: artifact-to-corpus promotion and the tests-of-tests are fixed, but three P1 blockers remain. First, issue 1430 makes the shared standard artifact envelope and scheduled health/freshness integration a birth requirement for this new lane; the PR still uses a lane-specific envelope with no shared monitoring integration. Second, if the self-check fails, the fuzz step never runs, so no envelope is written; the always-run upload warns and the summary then fails while trying to read the missing file. Every terminal path needs a valid envelope and summary. Third, the watchdog interval starts before the worker reports ready, so slow worker startup can still be misclassified as a hung parser case. Start case timing only after ready and add a delayed-startup regression test. All PR checks are green, but no Replay Nightly run exists for this head, so the scheduled route itself is unvalidated. |
|
@thymikee all three addressed in 1. Shared envelope (#1430). #1430 has no branch or PR yet, so instead of a second lane-specific shape I extracted the standard writer to 2. Every terminal path has an envelope. Self-check and fuzz now write to separate artifact subdirs ( 3. Watchdog arms only after Local: On the unvalidated scheduled route: |
|
Re-review at head 0f4d99a: the artifact promotion, tests-of-tests, self-check failure handling, and worker-ready watchdog issues are fixed. Two blockers remain. First, umbrella issue 1412 explicitly requires issue 1430 health/freshness monitoring to land before or with this scheduled lane; this PR adds an envelope producer but no derived scheduled-lane health consumer, dark-lane alerting, or freshness exposure, and no Replay Nightly run exists for this head. Second, malformed workflow-dispatch options throw in readFuzzOptions before run() enters its envelope-writing catch, so the claim that every config/crash terminal path produces an envelope is still false. Move option parsing under the guarded outcome path and cover malformed input. Ordinary PR checks are green, but the scheduled route is not yet ship-ready. |
|
Both addressed in 1. Envelope on the option-parse path. Option parsing now happens inside the guarded path: 2. Health/freshness consumer (#1430). #1430 still has no branch, so rather than ship the lane without its watcher I built the consumer here:
Run against the live API just now, which is itself the dark-lane evidence you asked for — it immediately caught a real outage rather than a synthetic one:
Blocked on you for the scheduled-route run. I tried to dispatch it and my token can't: Local gates: |
|
Re-review at head dd700b6: malformed fuzz options are now guarded and tested, but the new scheduled-health path has three P1 blockers. First, scheduled-lane-health.yml omits actions: read while calling the Actions workflow-runs API; with an explicit permissions map the omitted scope is none, so collection will 403. Second, health.ts has no top-level terminal error path, so an API failure exits before writing its snapshot or own required error envelope. Third, a readable workflow with zero scheduled runs is marked dark immediately, contradicting the two-cadence grace and falsely alerting for newly born lanes before their first due run. Add the permission, make terminal failures emit an error envelope, and model first-due/two-cadence grace with dispatched-route evidence. Ordinary PR checks are green, but neither scheduled route has run on this head. |
|
Re-review at 1. 2. Terminal error path — 3. First-due grace — the zero-run case now goes through Live dry-run against the real API on this head (classification unchanged, and the new registration lookup doesn't perturb it): Local gates: format/typecheck/lint green, fallow Still blocked on the dispatched-route evidence, and I need you for it. Both attempts get
Otherwise, from an account with dispatch rights: |
|
Latest delta fixes One P1 remains: first-run grace is anchored to workflow Also replace the new raw No live scheduled/dispatch evidence exists yet; iOS Smoke is currently red on an unrelated pre-existing timing assertion, so this head is not fully green. |
|
Fixed in P1 — grace anchored to the wrong date. You're right, and That's the commit that changed the number of Process-execution rule. Replaced with Local: format/typecheck/lint green, fallow On the red check: all 22 checks on this PR are green ( Still no dispatched-route evidence: |
|
The prior raw-process and temporary-directory cleanup findings are fixed, and the schedule-introduction anchor is the right design. One portability blocker remains: Current PR checks are green, but there is still no schedule/workflow_dispatch execution of either new route. |
|
Good catch, and a nasty class of bug: fixed in
Added the transition test you asked for, against a real scratch git repo rather than a fixture: commit 1 adds a workflow with only
Documented local dry-run rerun on this head: Gates: format/typecheck/lint green, fallow That leaves only the live scheduled/dispatch execution, which I still can't produce — |
|
Re-reviewed the latest delta: the POSIX schedule regex works on macOS, and the scratch-repo regression proves an old workflow’s later schedule-introduction commit drives pending→dark classification. Process execution and temporary cleanup follow repository rules. All head checks are green; no new code finding. Residual validation risk: neither new scheduled route has executed via |
|
Thanks — agreed on the residual risk, and I want to be precise about why it can't be retired from my side, because one half of it can't be retired from anyone's side pre-merge. Just retried both routes at head Two different walls:
What that means for the merge decision, honestly stated: the fuzz harness itself is proven by the unit lane (self-check targets exercise the real worker/watchdog path in CI on every run, and 22/22 head checks are green), so what remains unproven is specifically the workflow wiring — step ordering, artifact upload, summary rendering — for the two scheduled routes. My suggestion stands: if you want that verified before merge rather than after, approving the split into a standalone
If you'd rather not merge anything unvalidated, the alternative is to land the health lane in a follow-up PR after the fuzz lane, which at least reduces the unproven surface to one workflow. Your call — tell me which and I'll set it up. |
|
Reviewed against #1414. Approve with changes — the lane's core is right (real production parsers verified by signature:
🤖 Review by Claude Code |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…tests (#1414) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…tchdog after ready (#1414) 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>
…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>
6549f9d to
bca12c8
Compare
|
Re: the four changes — all addressed in
Gates: |
|
P1 — The checked-in regression corpus currently bypasses 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>
|
Re: corpus replay bypassing the watchdog / shared envelope / live nightly. Corpus watchdog — fixed in Verified both directions: green, it runs 9 tests in 1.6s; with a Shared envelope. Live 50k nightly. Still can't produce one pre-merge: my token gets |
|
The corpus-watchdog P1 is fixed at |
…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>
Runtime re-verification of the fast-check rewrite ✅Shell-only testing on Failure detection + shrinking (the key claim)Injected faults into
Green runs, determinism, scale timings
Vitest lanes and adversarial CLI
One real nit came out of this and is fixed in Repo tree clean ( |
…elope main landed scripts/lib/lane-envelope.ts, which #1430 requires this lane to use: scripts/fuzz/envelope.ts is now only the mapping onto it, with the lane's own facts under `data` and a `stage` field carrying the crash case the shared two-value `result` cannot express. Conflicts were in package.json (format globs: keep scripts/fuzz alongside the new scripts/mutation + scripts/lib) and docs/agents/testing.md (both lanes' sections).
|
Merged
{ "schemaVersion": 1, "lane": "parser-fuzz", "commit": "…", "tool": { "node": "v22.14.0", "harness": "scripts/fuzz/run.ts" },
"configHash": "sha256:f605b3c5b8b4e14a", "seed": "1", "result": "pass",
"data": { "stage": "complete", "config": { "mode": "generate", … }, "targetRuns": […], "failures": [], "reproCommands": [] } }Two mapping decisions worth a look:
Green after the merge: |
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>
|
P2 — The shared-envelope migration resolves the #1430 sequencing blocker, but its drift provenance is incomplete. |
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>
|
Fixed in -tool: { node, harness }
+tool: { node, 'fast-check': fastCheckVersion(), harness } // read from the installed package
-configHash: sha256(arbitraries.ts, targets.ts, invariant.ts)
+configHash: sha256(arbitraries.ts, generate.ts, targets.ts, invariant.ts,
+ src/__tests__/test-utils/property-arbitraries.ts)
To stop the list silently going stale again, PR body updated too: the envelope paragraph now says the shared module is on Green after this: |
…invariants (#1439) * test(daemon): seeded concurrency torture lane for session/lease/lock invariants Refs #1416 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): drive torture lane through real lock plan + review fixes - 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> * test(daemon): pass claim data as plain view accessors (fallow) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): gate lock plan on shouldLockSessionExecution; sweep replay + forced-device contention; whole-lane envelope Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): add real-scope runLocked serialization guard; whole-lane envelope duration Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): allow seed 0 replay; add seed-0 regression (TORTURE_SEED must accept 0) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * obs(#1430): add scheduled-lane freshness/cadence health watcher 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> * test(daemon): give scheduled-lane watcher a two-cadence newborn grace Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): anchor lane grace on schedule-introduction, not workflow age Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): derive schedule-activation semantically via git, through runCmdSync Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): add merge-commit regression pinning first-parent + committer time Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): pin scheduled-lane-health issue-write route via stubbed fetch Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): unbundle #1430 watcher; make torture lane nightly-only 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> * docs: fix stale torture-lane paths after nightly/ move Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): adopt shared lane-envelope for torture lane 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> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Re-reviewed |
|
Thanks. On the residual: I can't produce it — gh workflow run replays-nightly.yml --ref devin/1785159046-parser-fuzz-lane -f fuzz-iterations=50000Only the |
Summary
Closes #1414 (Track A of #1412). Parsers are the front door for agent-authored input; nothing asserted they fail well. This adds a fuzz lane over the five parser entry points and enforces exactly one invariant:
Targets (
scripts/fuzz/targets.ts):parseArgs(strict flags),parseSelectorChain,parseReplayScriptDetailed,readCliBatchStepsJson,parseMaestroProgram. Adding a parser to the lane is one entry in that array.It already caught a real bug.
readQuotedReplayTokenscans for the closing quote and then hands the literal toJSON.parse, so a.adline likefill @e1 --text "hello wor\ld"(invalid escape, raw tab, or\^@) escaped as a bareSyntaxError. Now a typedINVALID_ARGSwith a hint naming the escaping rule; three of those inputs are pinned in the corpus and insrc/replay/__tests__/script.test.ts.Cases come from fast-check, sharing #1437's hazard vocabulary.
scripts/fuzz/arbitraries.tsbuilds each target's inputs fromSELECTOR_VALUE_HAZARDS(exported fromsrc/__tests__/test-utils/property-arbitraries.ts) plus a fuzz-only rejection list, over structured bases borrowed fromselectorChainArb/replayScriptArb/fc.json()— so a hazard added for the property suite reaches the fuzz lane too. fast-check owns the loop, which buys shrinking: an injectedthrow new ErrorinparseSelectorChainreports"\u202evisible=false"(14 chars) plus--seed 1 (path 30:0:0:0:0:1), not the 20k-char string that first tripped it.Hang detection is external, because it has to be. No in-process timer can interrupt a synchronous parser loop, so cases run in a worker thread over a one-case request/response protocol; when a case outlives
--case-timeout-msthe parent terminates the wedged thread, reports the exact input, and starts a fresh worker so one hang can't cascade into every later case. The worker postsreadyfirst, so startup (thread spawn, type stripping, parser imports) is never charged to a case budget.The harness is tested against itself.
scripts/fuzz/self-check-targets.tsholds three broken-on-purpose targets (bareError,AppErrorwith a blank hint,for(;;){}) that onlyregistry.tsresolves, so a normal run never sees them.pnpm fuzz:parsers --self-check(nightly step) andscripts/fuzz/harness.test.tsrequire each failure kind to be reported — otherwise a regressed classifier or watchdog would leave every test green.Envelope, promotion, lanes.
<artifact-dir>/run-envelope.jsonon obs: scheduled-lane health, freshness telemetry, and standard artifact envelope #1430's shared contract (scripts/lib/lane-envelope.ts, now onmainand imported here); the nightly uploads it withif: always()and echoes it into the step summary, so a green scheduled run is auditable.scripts/fuzz/envelope.tsis only the mapping: lane facts underdata(mode, per-target cases/failures/durations, failures, repro commands) plusdata.stage: 'error'for a run that could not complete itself, since the sharedresultispass | fail.configHashhashes everything that decides what a seed generates (arbitraries,generate.ts, targets, invariant, and test: property-based testing foundation (fast-check) over parse/print and geometry kernels #1437's shared hazards) andtoolrecords fast-check's installed version, so a generator edit or dependency upgrade cannot change the case set while the provenance looks unchanged.repro:andpromote: … --append-corpus, and--input-file --append-corpusappends the downloaded artifact toscripts/fuzz/corpus/regressions.json(sorted, deduped).scripts/fuzz/corpus-replay.test.tsreplays the whole corpus plus every target's seeds through that same worker watchdog (runCases(target, cases, 5_000)), so a promoted hang case fails against its per-case budget instead of wedging the unit job. It andharness.test.tslive in the serializedsubprocess-stubproject; the corpus file runs in ~1.5s.Parser Fuzz Laneridesreplays-nightly.yml(device-free, ubuntu, 50k cases/target, seeded bygithub.run_number).Verification, locally:
--self-checkand the fuzz tests stable across repeat runs.throw new Error(...)/for(;;){}injected into aparseSelectorChainbranch are reported asuntyped-throw/hang, shrunk, with working repro commands; the same injection makes the corpus replay test fail in 5.00s rather than hang (all reverted).pnpm format:check,pnpm lint,pnpm typecheck,pnpm check:fallow --base origin/main, and the affected unit projects green.Link to Devin session: https://app.devin.ai/sessions/45070ada3381444c89ac3bb95b79a611
Requested by: @thymikee