mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
b9509fe006
* refactor: extract the .ad script codec into packages/ad-script Moves the mutually-coupled .ad read/write codec (script.ts, script-utils.ts, script-formatting.ts, open-script.ts) plus the target-v1 annotation SERDE slice of target-identity.ts into a new private leaf package, @agent-device/ad-script, exporting only `.`. This is option 1 from the P5 scoping dossier on #1478: the codec is shared by the daemon's session-script publication writer, the future replay engine, the CLI's `replay export`, and Maestro's failure-label formatting, so it can no longer live in root src/ once packages/ad-replay lands (R11 forbids a package reaching into root src), and a second export subpath or writer-half duplication are both ruled out by existing gates/tests. target-identity.ts keeps only the record/replay-shared classification core (classifyTargetBindingMatch, local-identity/ancestry-prefix matching), importing its shared types from the new package. Every real consumer (re-derived by grep, not the dossier's list alone) is rewired to @agent-device/ad-script. Refs #1478 Co-Authored-By: Claude <noreply@anthropic.com> * refactor: trim the ad-script façade to real consumers, lock the one-export boundary - packages/ad-script/src/index.ts: drop parseReplaySeriesFlags, formatTargetAnnotationCommentLine, parseTargetAnnotationCommentLine, TargetAnnotationLineParseResult, and TargetRect from the public façade — none has a consumer outside the package (re-swept every remaining export by grep; everything else kept has at least one real external importer). The functions/types stay exported from their declaring internal modules for the package's own internal use (script.ts, script-formatting.ts). - scripts/layering/package-boundaries.test.ts: add the parallel R11 assertions "the real tree parses, declares, and passes R11" already makes for maestro/provider-webdriver/provider-limrun/xml — ad-script exports exactly `.`, depends on exactly contracts+kernel, and is declared in root package.json — plus ad-script entries in the deep-resolution rejection coverage. Verified the lock catches a regression: temporarily added a fake `./codec` export to packages/ad-script/package.json and confirmed both the export-key-list assertion and the deep-resolution-rejection assertion fail; removed the plant and reconfirmed green. Refs #1478 Co-Authored-By: Claude <noreply@anthropic.com> * fix: remove polynomial-redos ambiguity from the target-v1 annotation line regex CodeQL js/polynomial-redos flagged TARGET_ANNOTATION_LINE_RE (packages/ad-script/src/internal/target-annotation-serde.ts): the payload group's `\s+(.*)` let `\s+` and the unconstrained `.*` both match whitespace, so a run of separator whitespace that ultimately fails to complete the match has many `\s+`/`.*` splits to backtrack through before concluding failure. Anchor the payload group on `\S` (the exact complement of `\s`), so the mandatory `\s+` separator and the payload's first character can never overlap — the split point becomes unique and no backtracking is possible. Behavior-preserving: the only caller (parseTargetAnnotationCommentLine) always matches against an already-.trim()-ed line, whose last character (whenever the tag matches at all) is never whitespace — so a payload section `\S.*` would reject (content that is entirely whitespace) can never reach this regex through the real call path. Verified against the frozen replay-compat corpus and the full serde/parser test suites, unmodified. Added a regression test with the exact adversarial shape CodeQL/the reviewer cited (many tab pairs after the version digits), asserting sub-second parse. Refs #1478 Co-Authored-By: Claude <noreply@anthropic.com> * test(ad-script): pin the annotation-line pattern's linear rejection directly The entry-point adversarial case matched greedily even with the retired regex (trim strips edge whitespace and per-line input carries no newline), so it proved nothing about the pattern. The regression surface is the pattern itself: an interior tab run with an x-newline tail fails the match, which the retired form re-split quadratically (3.7s at 100k tabs) and the \S anchor rejects in one attempt. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
// Parser fuzz targets (#1414).
|
|
//
|
|
// Every target takes one string case and calls a real parser. The harness owns the
|
|
// invariant (typed AppError with a non-empty hint, no hang); a target only says how a
|
|
// case string reaches its parser, and supplies the seed inputs the mutator chews on.
|
|
//
|
|
// Keep targets pure and synchronous: the hang watchdog (scripts/fuzz/worker.ts) can only
|
|
// attribute a stall to a case if the case runs to completion in one tick.
|
|
|
|
import { parseArgs } from '../../src/cli/parser/args.ts';
|
|
import { parseSelectorChain } from '../../src/selectors/parse.ts';
|
|
import { parseReplayScriptDetailed } from '@agent-device/ad-script';
|
|
import { readCliBatchStepsJson } from '../../src/cli/batch-steps.ts';
|
|
import { inspectMaestroFlow } from '@agent-device/maestro';
|
|
import type { FuzzTarget } from './target-types.ts';
|
|
|
|
// argv is carried as one string so a case (and its corpus entry, artifact, and repro
|
|
// command) stays a single copy-pasteable value. Splitting on spaces is deliberate: the
|
|
// fuzzer wants odd tokens, not a faithful shell grammar.
|
|
function toArgv(input: string): string[] {
|
|
return input.split(' ').filter((token) => token.length > 0);
|
|
}
|
|
|
|
export const FUZZ_TARGETS: readonly FuzzTarget[] = [
|
|
{
|
|
name: 'cli-args',
|
|
description: 'parseArgs (strict flags)',
|
|
run: (input) => void parseArgs(toArgv(input), { strictFlags: true }),
|
|
seeds: [
|
|
'click --selector text=Login',
|
|
'open com.example.app --platform ios --json',
|
|
'snapshot --depth 3 --format text',
|
|
'fill @e1 --text hello --submit',
|
|
'batch --steps [] --timeout 1000',
|
|
'devices --platform android --json --debug',
|
|
'wait --selector role=button --timeout-ms 500',
|
|
'test replays -- --raw --passthrough',
|
|
'click --selector',
|
|
'--json',
|
|
'',
|
|
],
|
|
},
|
|
{
|
|
name: 'selector',
|
|
description: 'parseSelectorChain',
|
|
run: (input) => void parseSelectorChain(input),
|
|
seeds: [
|
|
'text=Login',
|
|
'label="Sign in" && role=button',
|
|
'text=Save || label=Done',
|
|
'id=com.example:id/button[2]',
|
|
'role=button and is=enabled',
|
|
'text~=partial',
|
|
'label="quoted \\"inner\\" value"',
|
|
'@e1',
|
|
'text=🚀',
|
|
'text=',
|
|
'',
|
|
],
|
|
},
|
|
{
|
|
name: 'replay-script',
|
|
description: 'parseReplayScriptDetailed (.ad scripts)',
|
|
run: (input) => void parseReplayScriptDetailed(input),
|
|
seeds: [
|
|
'open com.example.app\nclick text=Login\nclose\n',
|
|
'# context platform=ios target=mobile\nopen com.example.app\n',
|
|
'# context timeoutMs=1000 retries=2\nsnapshot\n',
|
|
'# target-v1 role=button label=Login\nclick @e1\n',
|
|
'fill @e1 --text "hello world"\nassert text=Welcome\n',
|
|
'swipe 10 20 30 40\nwait 250\n',
|
|
'env FOO=bar\nclick text=${FOO}\n',
|
|
'screenshot --quality low\n',
|
|
'# target-v1 role=button\n\nclick @e1\n',
|
|
'',
|
|
],
|
|
},
|
|
{
|
|
name: 'batch-steps',
|
|
description: 'readCliBatchStepsJson (batch --steps)',
|
|
run: (input) => void readCliBatchStepsJson(input),
|
|
seeds: [
|
|
'[{"command":"snapshot","input":{}}]',
|
|
'[{"command":"click","input":{"selector":"text=Login"}}]',
|
|
'[{"command":"open","positionals":["com.example.app"],"flags":{"json":true}}]',
|
|
'[{"command":"snapshot","input":{}},{"command":"close","input":{}}]',
|
|
'[{"command":"wait","input":{"timeoutMs":250}}]',
|
|
'[]',
|
|
'{}',
|
|
'not json',
|
|
'',
|
|
],
|
|
},
|
|
{
|
|
name: 'maestro',
|
|
description: 'parseMaestroProgram (Maestro compat)',
|
|
run: (input) => void inspectMaestroFlow(input, 'fuzz.yaml'),
|
|
seeds: [
|
|
'appId: com.example.app\n---\n- launchApp\n- tapOn: "Login"\n',
|
|
'appId: com.example.app\n---\n- tapOn:\n id: "login"\n',
|
|
'appId: com.example.app\n---\n- inputText: "hello"\n- assertVisible: "Welcome"\n',
|
|
'appId: com.example.app\n---\n- swipe:\n direction: UP\n',
|
|
'appId: com.example.app\n---\n- runFlow: other.yaml\n',
|
|
'appId: com.example.app\n---\n- repeat:\n times: 2\n commands:\n - back\n',
|
|
'- launchApp\n',
|
|
'appId: com.example.app\n---\n',
|
|
'',
|
|
],
|
|
},
|
|
];
|