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>
84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
// Unit-lane replay of the parser fuzz regression corpus (#1414).
|
|
//
|
|
// The nightly lane finds cases; this replays every case it ever found so a regression fails in
|
|
// seconds on a PR instead of a night later. Cases go through the same worker-backed watchdog the
|
|
// nightly lane uses: a promoted hang case must fail this test against its per-case budget, not
|
|
// wedge the unit job until the CI timeout.
|
|
|
|
import fc from 'fast-check';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { arbitraryForTarget } from './arbitraries.ts';
|
|
import { readCorpus } from './corpus.ts';
|
|
import { runCases } from './execute.ts';
|
|
import { describeFailure } from './invariant.ts';
|
|
import { getFuzzTarget } from './registry.ts';
|
|
import { FUZZ_TARGETS } from './targets.ts';
|
|
|
|
/**
|
|
* Generous enough that a loaded CI runner never reports a healthy parser as hung, small enough
|
|
* that a genuinely wedged case fails the file in seconds.
|
|
*/
|
|
const CASE_TIMEOUT_MS = 5_000;
|
|
|
|
/**
|
|
* Vitest must outlast the watchdog for every case a test replays, or a wedged parser is reported as
|
|
* a bare `Test timed out` instead of the named `hang:` failure that says which input wedged.
|
|
*/
|
|
const timeoutFor = (cases: number) => cases * CASE_TIMEOUT_MS + 10_000;
|
|
|
|
describe('parser fuzz regression corpus', () => {
|
|
const corpus = readCorpus();
|
|
|
|
it('is non-empty and free of duplicates', () => {
|
|
expect(corpus.length).toBeGreaterThan(0);
|
|
const keys = corpus.map((entry) => `${entry.target}\u0000${entry.input}`);
|
|
expect(new Set(keys).size).toBe(keys.length);
|
|
});
|
|
|
|
it('names only real parser targets and explains every entry', () => {
|
|
const parserTargets = new Set<string>(FUZZ_TARGETS.map((target) => target.name));
|
|
for (const entry of corpus) {
|
|
expect(parserTargets).toContain(entry.target);
|
|
expect(entry.note.trim()).not.toBe('');
|
|
}
|
|
});
|
|
|
|
// One worker per target rather than per case: startup is the only real cost here, and the
|
|
// watchdog budget is per case either way.
|
|
it.each(FUZZ_TARGETS.map((target) => [target.name, target] as const))(
|
|
'%s corpus cases and seeds hold the invariant, under the watchdog',
|
|
async (name, target) => {
|
|
const cases = [
|
|
...target.seeds,
|
|
...corpus.filter((entry) => entry.target === name).map((entry) => entry.input),
|
|
];
|
|
const failures = await runCases(target, cases, CASE_TIMEOUT_MS);
|
|
expect(failures.map(describeFailure)).toEqual([]);
|
|
},
|
|
timeoutFor(corpus.length + Math.max(...FUZZ_TARGETS.map((t) => t.seeds.length))),
|
|
);
|
|
});
|
|
|
|
describe('fuzz case generation', () => {
|
|
const target = getFuzzTarget('selector');
|
|
|
|
it('is deterministic for a seed, so a reported counterexample replays', () => {
|
|
const sample = (seed: number) => fc.sample(arbitraryForTarget(target), { numRuns: 32, seed });
|
|
expect(sample(7)).toEqual(sample(7));
|
|
expect(sample(7)).not.toEqual(sample(8));
|
|
});
|
|
|
|
const GENERATED_CASES = 64;
|
|
|
|
it(
|
|
'generates strings the target can be fed directly',
|
|
async () => {
|
|
const inputs = fc.sample(arbitraryForTarget(target), { numRuns: GENERATED_CASES, seed: 1 });
|
|
expect(inputs.every((input) => typeof input === 'string')).toBe(true);
|
|
const failures = await runCases(target, inputs, CASE_TIMEOUT_MS);
|
|
expect(failures.map(describeFailure)).toEqual([]);
|
|
},
|
|
timeoutFor(GENERATED_CASES),
|
|
);
|
|
});
|