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>
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
// 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 },
|
||
);
|
||
}
|