mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
80b4769230
* test(fuzz): structured CLI/Maestro generators that reach command validation and assert error codes (#1781 B2) * test(fuzz): pin the rediscovered #1433 excess-positional case and keep numeric flag samples inside their range * style: apply oxfmt to the new fuzz modules * perf(fuzz): derive the CLI validation surface lazily so unrelated harness paths keep their startup * test(fuzz): resolve validation generators in the run path so corpus replay keeps its small module graph * test(fuzz): weight the CLI budget toward command validation, pin the finite classes as seeds, guard lazy surface derivation * docs(testing): describe the validation lane's layer split, seed-pinned classes, and PR-time gates * refactor(fuzz): split the validation generator into CLI and Maestro modules, mirrored in tests * refactor(fuzz): collapse the flag-shaped mutation classes and seed literals, derive class coverage from declarations * fix(fuzz): hash every case-generation module in configHash, guarded by an import-closure test * test(fuzz): assert CLI command and flag-key coverage against the registry, and close the six gaps it found
65 lines
2.5 KiB
TypeScript
65 lines
2.5 KiB
TypeScript
// The invariants the parser fuzz lane enforces (#1414, validation targets #1781 B2).
|
|
//
|
|
// Parsers are the front door for agent-authored input. For classic targets the contract is not
|
|
// "parses correctly" (nobody can say what a mutated string should mean) but "fails well":
|
|
//
|
|
// 1. a rejection is an `AppError` — never a bare Error, TypeError, string, or undefined;
|
|
// 2. the normalized error carries a non-empty `hint`, so the caller is told what to do;
|
|
// 3. the case terminates — enforced by the harness watchdog, not by this module, because
|
|
// synchronous parsers cannot be interrupted from inside their own tick.
|
|
//
|
|
// A validation target (`target.check`) additionally knows what each case SHOULD do, because its
|
|
// generator constructed the case with a planted violation or none: it judges silent acceptances
|
|
// and wrong error codes too (validation-case.ts).
|
|
|
|
import { AppError, normalizeError } from '@agent-device/kernel/errors';
|
|
import type { FuzzFailure, FuzzTarget } from './target-types.ts';
|
|
|
|
// Re-exported because every existing consumer imports the failure type from the invariant it
|
|
// belongs to; the declaration moved to target-types.ts only to keep targets cycle-free.
|
|
export type { FuzzFailure };
|
|
|
|
/**
|
|
* Runs one case and returns the invariant violation it produced, or `null`.
|
|
* For classic targets, accepting the parse is a pass: they judge rejections, not results.
|
|
* A validation target owns its whole judgment via `check`.
|
|
*/
|
|
export function checkCase(target: FuzzTarget, input: string): FuzzFailure | null {
|
|
if (target.check) return target.check(input);
|
|
try {
|
|
target.run(input);
|
|
return null;
|
|
} catch (error) {
|
|
if (!(error instanceof AppError)) {
|
|
return {
|
|
target: target.name,
|
|
input,
|
|
kind: 'untyped-throw',
|
|
detail: describeThrown(error),
|
|
};
|
|
}
|
|
const hint = normalizeError(error).hint;
|
|
if (typeof hint !== 'string' || hint.trim().length === 0) {
|
|
return {
|
|
target: target.name,
|
|
input,
|
|
kind: 'empty-hint',
|
|
detail: `AppError ${error.code} has no hint: ${error.message}`,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function describeThrown(error: unknown): string {
|
|
if (error instanceof Error) {
|
|
const stackLine = error.stack?.split('\n')[1]?.trim();
|
|
return `${error.name}: ${error.message}${stackLine ? ` (at ${stackLine})` : ''}`;
|
|
}
|
|
return `non-Error throw: ${typeof error} ${String(error)}`;
|
|
}
|
|
|
|
export function describeFailure(failure: FuzzFailure): string {
|
|
return `[${failure.target}] ${failure.kind}: ${failure.detail}`;
|
|
}
|