Files
callstack__agent-device/scripts/fuzz/generate.ts
devin-ai-integration[bot] 006c4cadc9 test: nightly parser fuzz lane — parser input fails as typed AppErrors, 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>
2026-07-28 11:29:25 +02:00

104 lines
3.7 KiB
TypeScript

// The generating fuzz run for one target (#1414).
//
// fast-check drives the loop so a violation is reported SHRUNK: the first failing input is
// typically a long mutated string, while the minimized one names the actual parser branch. The
// run stays reproducible — `--seed` is fast-check's seed, and the reported `path` replays the exact
// case, shrink steps included.
import fc from 'fast-check';
import { arbitraryForTarget } from './arbitraries.ts';
import { type CaseRunner, createCaseRunner } from './execute.ts';
import type { FuzzFailure } from './invariant.ts';
import type { FuzzTarget } from './target-types.ts';
export type GeneratedRun = {
/** Cases actually executed, seeds and shrink candidates included. */
cases: number;
failure: FuzzFailure | null;
/** fast-check's replay coordinates for the counterexample, when there is one. */
replay?: { seed: number; path: string };
};
export type GenerateOptions = { iterations: number; seed: number; caseTimeoutMs: number };
/** The first seed that already violates the invariant, or `null` when they all hold. */
async function checkSeeds(target: FuzzTarget, runner: CaseRunner): Promise<FuzzFailure | null> {
for (const seed of target.seeds) {
const failure = await runner.run(seed);
if (failure) return failure;
}
return null;
}
/**
* Re-runs the shrunk counterexample, so the reported failure — the one written as an artifact and
* promoted into the corpus — describes the minimized input rather than the original random one.
*/
async function describeCounterexample(
target: FuzzTarget,
runner: CaseRunner,
input: string,
): Promise<FuzzFailure> {
const failure = await runner.run(input);
return (
failure ?? {
target: target.name,
input,
kind: 'hang',
detail: 'counterexample no longer reproduces outside the shrink run',
}
);
}
/**
* Fuzzes one target. Seeds run verbatim first: they are the known-good shapes, and a lane that
* only ever ran generated cases could pass while the plain grammar is broken.
*/
export async function generateAndCheck(
target: FuzzTarget,
options: GenerateOptions,
): Promise<GeneratedRun> {
const runner = await createCaseRunner(target, options.caseTimeoutMs);
try {
const seedFailure = await checkSeeds(target, runner);
if (seedFailure) return { cases: target.seeds.length, failure: seedFailure };
return await checkGenerated(target, runner, options);
} finally {
await runner.close();
}
}
/** The generated half of a run: fast-check picks the inputs and shrinks any counterexample. */
async function checkGenerated(
target: FuzzTarget,
runner: CaseRunner,
options: GenerateOptions,
): Promise<GeneratedRun> {
let cases = target.seeds.length;
const details = await fc.check(
fc.asyncProperty(arbitraryForTarget(target), async (input) => {
cases += 1;
return (await runner.run(input)) === null;
}),
{ numRuns: Math.max(options.iterations - target.seeds.length, 1), seed: options.seed },
);
if (!details.failed) return { cases, failure: null };
return {
cases,
failure: await describeCounterexample(target, runner, counterexampleOf(details)),
replay: replayOf(details),
};
}
type CheckDetails = { counterexample: [string] | null; counterexamplePath: string | null };
/** The shrunk input fast-check settled on; `''` when it reported a failure without one. */
function counterexampleOf(details: CheckDetails): string {
return details.counterexample?.[0] ?? '';
}
/** Coordinates that replay the counterexample, shrink steps included. */
function replayOf(details: CheckDetails & { seed: number }): { seed: number; path: string } {
return { seed: details.seed, path: details.counterexamplePath ?? '' };
}