mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
006c4cadc9
* 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>
104 lines
3.7 KiB
TypeScript
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 ?? '' };
|
|
}
|