Files
callstack__agent-device/scripts/fuzz/arbitraries.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

115 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Case generators for the parser fuzz lane (#1414).
//
// fast-check rather than a bespoke PRNG, for two reasons the lane depends on: a reported
// counterexample is the SHRUNK input (a hand-rolled mutator reports the 20k-character random one),
// and the hazard vocabulary is the one #1437's property suite already curates — a hazard added
// there for a round-trip property reaches the fuzzer without being retyped here.
//
// Cases stay near the grammar's edge on purpose: a well-formed base with hostile chunks spliced in
// reaches deep parser branches that uniformly random noise never gets past the first token of.
import fc from 'fast-check';
import {
replayScriptArb,
SELECTOR_VALUE_HAZARDS,
selectorChainArb,
} from '../../src/__tests__/test-utils/property-arbitraries.ts';
import type { FuzzTarget, FuzzTargetName } from './target-types.ts';
/**
* Hazards a valid-input property cannot use — they exist to be *rejected*: structural JSON/YAML
* punctuation, delimiter lookalikes, prototype-pollution keys, numeric edges, bidi/zero-width
* controls. The shared list above carries the ones valid inputs must also survive.
*/
const REJECTION_HAZARDS = [
'`',
'==',
'&&',
'--',
'---',
'#',
':',
',',
'{',
'}',
'[',
']',
'(',
')',
'${',
'${}',
'@',
'~=',
'*',
'\r\n',
'\u0000',
'\u200b',
'\u202e',
'\ufeff',
'-0',
'NaN',
'Infinity',
'1e999',
'9007199254740993',
'null',
'undefined',
'__proto__',
'constructor',
] as const;
const hazardArb: fc.Arbitrary<string> = fc.constantFrom(
...SELECTOR_VALUE_HAZARDS,
...REJECTION_HAZARDS,
);
/** A base string with 1–4 hazards spliced in at shrinkable positions. */
function corrupted(base: fc.Arbitrary<string>): fc.Arbitrary<string> {
return fc
.tuple(
base,
fc.array(fc.tuple(hazardArb, fc.nat({ max: 4096 })), { minLength: 1, maxLength: 4 }),
)
.map(([text, edits]) =>
edits.reduce((current, [chunk, at]) => {
const index = at % (current.length + 1);
return current.slice(0, index) + chunk + current.slice(index);
}, text),
);
}
/** A long run of one hazard — regex-backtracking and quadratic-scan bait. */
const repeatedHazardArb: fc.Arbitrary<string> = fc
.tuple(hazardArb, fc.integer({ min: 50, max: 400 }))
.map(([chunk, times]) => chunk.repeat(times));
/** Input that is not trying to look like the grammar at all. */
const noiseArb: fc.Arbitrary<string> = fc.oneof(
fc.string({ maxLength: 40 }),
fc.string({ unit: 'binary', maxLength: 40 }),
fc.array(hazardArb, { maxLength: 8 }).map((parts) => parts.join('')),
repeatedHazardArb,
);
/**
* Bases borrowed from the property suite, which generates *valid* inputs: corrupting a
* grammar-correct script or selector chain is how the fuzzer reaches branches past the first
* rejection. Targets without one generate from their own seed list.
*/
const STRUCTURED_BASES: Partial<Record<FuzzTargetName, fc.Arbitrary<string>>> = {
selector: selectorChainArb.map((chain) => chain.expression),
'replay-script': replayScriptArb,
'batch-steps': fc.json({ maxDepth: 3 }),
};
/** The case distribution for one target: mostly near-miss, some valid, some pure noise. */
export function arbitraryForTarget(target: FuzzTarget): fc.Arbitrary<string> {
const seeded = fc.constantFrom(...target.seeds);
const structured = STRUCTURED_BASES[target.name];
const base = structured === undefined ? seeded : fc.oneof(seeded, structured);
return fc.oneof(
{ weight: 6, arbitrary: corrupted(base) },
{ weight: 2, arbitrary: base },
{ weight: 2, arbitrary: noiseArb },
);
}