Skip to content

Commit ade81ad

Browse files
thymikeeclaude
andcommitted
refactor(test-kit): drop command-registry and daemon-contracts so selectors can depend on test-kit
The replay-script generator is derived from the command catalog and is used only by the root round-trip property and the fuzz target, so it becomes a root fixture. The legacy daemon fixture serves one root test and moves beside it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent e8f24cd commit ade81ad

12 files changed

Lines changed: 237 additions & 250 deletions

File tree

‎docs/agents/testing.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Before creating fixtures, look in `@agent-device/test-kit` and
7070
`packages/daemon/src/__tests__/support/`. Shared `DeviceInfo`, session, snapshot, store,
7171
runtime-fact, and mocked-binary values belong in a sibling fixture module, not in repeated test
7272
literals; a fixture one package uses is a `*.fixtures.ts` beside its module. The packages test-kit
73-
builds on (kernel, contracts, capture-kit, host-kit, command-registry, daemon-contracts) cannot
73+
builds on (kernel, contracts, capture-kit, host-kit) cannot
7474
declare it; their tests keep helpers local. Never import test-kit in the `vi.mock` factory of a
7575
module its entry loads (`host-kit/process`, `command`, `file`): spy it in place, as
7676
`pinOwnProcessStartTime` does. Fixtures a package still exports (R83's legacy count) stay

‎packages/ad-script/src/internal/__tests__/script.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,7 @@ test('formatDivergenceActionLabel categorically drops fill/type text but keeps t
894894
// The property test asserting "serializing a parsed script is a fixed point
895895
// for generated scripts" stays at `src/commands/replay/ad-script-round-trip.test.ts`:
896896
// its script generator (`replayScriptArb`) is derived from the root command
897-
// catalog and selector grammar (`@agent-device/test-kit`),
897+
// catalog and selector grammar (`src/commands/replay/replay-script.fixtures.ts`),
898898
// which this package cannot import without an R11 package→root-src escape
899899
// (#1478 P5 scoping dossier §5d). It exercises this package's exports via
900900
// the `@agent-device/ad-script` specifier instead of duplicating the codec.

‎packages/test-kit/package.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
"description": "Private shared test helpers (temp dirs, device/session/snapshot fixtures, mocked processes, loopback servers) for the root and workspace-package test trees. One entry, named exports; test-only, never bundled, never importing root `src/`, `platform-*`, `provider-*` or the daemon.",
77
"dependencies": {
88
"@agent-device/capture-kit": "workspace:*",
9-
"@agent-device/command-registry": "workspace:*",
109
"@agent-device/contracts": "workspace:*",
11-
"@agent-device/daemon-contracts": "workspace:*",
1210
"@agent-device/host-kit": "workspace:*",
1311
"@agent-device/kernel": "workspace:*"
1412
},

‎packages/test-kit/src/index.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export {
4444
withMockedPlatform,
4545
} from './host-execution.ts';
4646
export { pinOwnProcessStartTime } from './host-process-mock.ts';
47-
export { spawnLegacyDaemonFixture } from './legacy-daemon-fixture.ts';
4847
export {
4948
closeLoopbackServer,
5049
listenOnLoopback,
@@ -69,7 +68,6 @@ export {
6968
PROPERTY_RUNS_SMALL,
7069
rawSnapshotNodesArb,
7170
refArb,
72-
replayScriptArb,
7371
} from './property-arbitraries.ts';
7472
export {
7573
createUnavailableRuntimeFactsForTest,
Lines changed: 0 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { SCROLL_DIRECTIONS, SWIPE_PRESETS } from '@agent-device/contracts/scroll-gesture';
21
import type { RawSnapshotNode } from '@agent-device/kernel/snapshot';
32
import fc from 'fast-check';
4-
import { INTERNAL_COMMANDS, PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
53

64
/**
75
* Shared fast-check generators for the pure parse/print and geometry kernels
@@ -74,232 +72,3 @@ export const rawSnapshotNodesArb: fc.Arbitrary<RawSnapshotNode[]> = fc
7472
{ maxLength: 12 },
7573
)
7674
.map((nodes) => nodes.map((node, index) => ({ ...node, index })));
77-
78-
// ---------------------------------------------------------------------------
79-
// `.ad` script lines
80-
// ---------------------------------------------------------------------------
81-
82-
const scriptTextArb: fc.Arbitrary<string> = fc.oneof(
83-
fc.constantFrom('Submit', 'Log in', 'a "quoted" label', "it's fine", 'tab\tseparated', 'Ünïcøde'),
84-
fc.string({ minLength: 1, maxLength: 10 }),
85-
);
86-
87-
const scriptTargetArb: fc.Arbitrary<string> = fc.oneof(
88-
refArb.map(formatRef),
89-
scriptTextArb.map((text) => JSON.stringify(`label=${text}`)),
90-
// #3197: a hand-written line quotes a selector with single quotes the way the
91-
// shell does. The formatter always rewrites to the JSON spelling, so this tests
92-
// the tokenizer's reading without expecting the single-quote form back.
93-
fc.constantFrom('Sign in', 'Log in', 'Submit').map((label) => `'label=${JSON.stringify(label)}'`),
94-
);
95-
96-
const coordinateArb = fc.integer({ min: 0, max: 1200 }).map(String);
97-
98-
/**
99-
* The `wait` line tails the script grammar accepts after the target: its budget
100-
* positional, and the capture-scope flags it declares (#3197). Both may appear on
101-
* one line, positional before flags, which is the order the writer emits them in.
102-
*/
103-
const WAIT_TIMEOUT_ARB: fc.Arbitrary<string> = fc
104-
.integer({ min: 100, max: 5000 })
105-
.map((timeout) => ` ${timeout}`);
106-
107-
const WAIT_CAPTURE_FLAG_ARB: fc.Arbitrary<string> = fc.oneof(
108-
fc.constant(' --raw'),
109-
fc.integer({ min: 0, max: 6 }).map((depth) => ` --depth ${depth}`),
110-
scriptTextArb.map((scope) => ` --scope ${JSON.stringify(scope)}`),
111-
);
112-
113-
/** The `scroll` flags the script grammar carries (#3197): the stop condition. */
114-
const SCROLL_FLAG_ARB: fc.Arbitrary<string> = scriptTargetArb.map((target) => ` --until ${target}`);
115-
116-
/**
117-
* A command is either generated from a line template or explicitly waived with
118-
* the reason it needs none. `Record<PublicCommandName, …>` makes the
119-
* classification exhaustive: a new public command does not compile until it is
120-
* given a template or a waiver.
121-
*/
122-
/**
123-
* Every command name a `.ad` line can carry. Internal commands are included
124-
* because scripts record them too (`runtime` has its own parse/print branch).
125-
*/
126-
type ReplayCommandName =
127-
| (typeof PUBLIC_COMMANDS)[keyof typeof PUBLIC_COMMANDS]
128-
| (typeof INTERNAL_COMMANDS)[keyof typeof INTERNAL_COMMANDS];
129-
130-
type ReplayLinePlan = fc.Arbitrary<string> | { waived: string };
131-
132-
/**
133-
* Commands whose `.ad` line is a bare `<command> <token>…` handled by the
134-
* generic parse/print branch (`appendGenericActionScriptArgs`), whose shape the
135-
* `wait`/`longpress` templates already exercise. A command that grows its own
136-
* branch in the replay command's script formatting must move to a template.
137-
*/
138-
const GENERIC_REPLAY_LINE = {
139-
waived: 'generic line shape, covered by the wait/longpress templates',
140-
} as const;
141-
142-
const REPLAY_SCRIPT_LINE_PLANS = {
143-
click: fc.oneof(
144-
scriptTargetArb.map((target) => `click ${target}`),
145-
fc.tuple(coordinateArb, coordinateArb).map(([x, y]) => `click ${x} ${y}`),
146-
fc
147-
.tuple(scriptTargetArb, fc.integer({ min: 1, max: 5 }))
148-
.map(([target, count]) => `click ${target} --count ${count}`),
149-
),
150-
press: fc.oneof(
151-
scriptTargetArb.map((target) => `press ${target}`),
152-
fc
153-
.tuple(scriptTargetArb, fc.constantFrom('primary', 'secondary', 'middle'))
154-
.map(([target, button]) => `press ${target} --button ${button}`),
155-
),
156-
longpress: scriptTargetArb.map((target) => `longpress ${target}`),
157-
hover: scriptTargetArb.map((target) => `hover ${target}`),
158-
wait: fc
159-
.tuple(
160-
scriptTargetArb,
161-
fc.option(WAIT_TIMEOUT_ARB, { nil: '' }),
162-
fc.option(WAIT_CAPTURE_FLAG_ARB, { nil: '' }),
163-
)
164-
.map(([target, timeout, capture]) => `wait ${target}${timeout}${capture}`),
165-
fill: fc
166-
.tuple(scriptTargetArb, scriptTextArb)
167-
.map(([target, text]) => `fill ${target} ${JSON.stringify(text)}`),
168-
type: scriptTextArb.map((text) => `type ${JSON.stringify(text)}`),
169-
get: fc
170-
.tuple(fc.constantFrom('text', 'value', 'label'), scriptTargetArb)
171-
.map(([property, target]) => `get ${property} ${target}`),
172-
swipe: fc
173-
.tuple(coordinateArb, coordinateArb, coordinateArb, coordinateArb)
174-
.map((coordinates) => `swipe ${coordinates.join(' ')}`),
175-
gesture: fc.oneof(
176-
fc
177-
.tuple(coordinateArb, coordinateArb, coordinateArb, coordinateArb)
178-
.map((coordinates) => `gesture pan ${coordinates.join(' ')}`),
179-
fc
180-
.tuple(fc.constantFrom(...SCROLL_DIRECTIONS), coordinateArb, coordinateArb)
181-
.map(([direction, x, y]) => `gesture fling ${direction} ${x} ${y}`),
182-
fc.constantFrom(...SWIPE_PRESETS).map((preset) => `gesture swipe ${preset}`),
183-
fc.integer({ min: -180, max: 180 }).map((degrees) => `gesture rotate ${degrees}`),
184-
),
185-
snapshot: fc.oneof(
186-
fc.constant('snapshot'),
187-
fc.constant('snapshot -i'),
188-
fc.integer({ min: 0, max: 6 }).map((depth) => `snapshot -d ${depth}`),
189-
scriptTextArb.map((scope) => `snapshot -s ${JSON.stringify(scope)}`),
190-
),
191-
screenshot: fc.constantFrom('screenshot', 'screenshot out.png'),
192-
open: fc.oneof(
193-
fc.constantFrom('open com.example.app', 'open com.example.app --relaunch'),
194-
fc
195-
.tuple(fc.constantFrom('ios', 'android'), fc.integer({ min: 1, max: 65_535 }))
196-
.map(
197-
([platform, port]) => `open com.example.app --platform ${platform} --metro-port ${port}`,
198-
),
199-
),
200-
runtime: fc.oneof(
201-
fc.constant('runtime metro'),
202-
fc.constantFrom('ios', 'android').map((platform) => `runtime metro --platform ${platform}`),
203-
),
204-
record: fc.oneof(
205-
fc.constantFrom('record start', 'record stop', 'record start --hide-touches'),
206-
fc.integer({ min: 1, max: 120 }).map((fps) => `record start --fps ${fps}`),
207-
),
208-
alert: GENERIC_REPLAY_LINE,
209-
'app-switcher': GENERIC_REPLAY_LINE,
210-
'action-button': GENERIC_REPLAY_LINE,
211-
fold: GENERIC_REPLAY_LINE,
212-
apps: GENERIC_REPLAY_LINE,
213-
appstate: GENERIC_REPLAY_LINE,
214-
artifacts: GENERIC_REPLAY_LINE,
215-
audio: GENERIC_REPLAY_LINE,
216-
back: GENERIC_REPLAY_LINE,
217-
batch: GENERIC_REPLAY_LINE,
218-
boot: GENERIC_REPLAY_LINE,
219-
capabilities: GENERIC_REPLAY_LINE,
220-
clipboard: GENERIC_REPLAY_LINE,
221-
close: GENERIC_REPLAY_LINE,
222-
devices: GENERIC_REPLAY_LINE,
223-
diff: GENERIC_REPLAY_LINE,
224-
doctor: GENERIC_REPLAY_LINE,
225-
events: GENERIC_REPLAY_LINE,
226-
find: GENERIC_REPLAY_LINE,
227-
focus: GENERIC_REPLAY_LINE,
228-
home: GENERIC_REPLAY_LINE,
229-
install: GENERIC_REPLAY_LINE,
230-
'install-from-source': GENERIC_REPLAY_LINE,
231-
is: GENERIC_REPLAY_LINE,
232-
keyboard: GENERIC_REPLAY_LINE,
233-
logs: GENERIC_REPLAY_LINE,
234-
network: GENERIC_REPLAY_LINE,
235-
orientation: GENERIC_REPLAY_LINE,
236-
perf: GENERIC_REPLAY_LINE,
237-
prepare: GENERIC_REPLAY_LINE,
238-
push: GENERIC_REPLAY_LINE,
239-
'react-native': GENERIC_REPLAY_LINE,
240-
reinstall: GENERIC_REPLAY_LINE,
241-
replay: GENERIC_REPLAY_LINE,
242-
scroll: fc
243-
.tuple(
244-
fc.constantFrom(...SCROLL_DIRECTIONS),
245-
fc.option(fc.constantFrom('0.5', '0.8'), { nil: undefined }),
246-
fc.option(SCROLL_FLAG_ARB, { nil: undefined }),
247-
)
248-
.map(
249-
([direction, amount, flags]) =>
250-
`scroll ${direction}${amount === undefined ? '' : ` ${amount}`}${flags ?? ''}`,
251-
),
252-
settings: GENERIC_REPLAY_LINE,
253-
shutdown: GENERIC_REPLAY_LINE,
254-
test: GENERIC_REPLAY_LINE,
255-
trace: GENERIC_REPLAY_LINE,
256-
'trigger-app-event': GENERIC_REPLAY_LINE,
257-
'tv-remote': GENERIC_REPLAY_LINE,
258-
viewport: GENERIC_REPLAY_LINE,
259-
human_control: { waived: 'host-local control commands are never recorded in replay scripts' },
260-
install_source: GENERIC_REPLAY_LINE,
261-
lease_allocate: GENERIC_REPLAY_LINE,
262-
lease_heartbeat: GENERIC_REPLAY_LINE,
263-
lease_release: GENERIC_REPLAY_LINE,
264-
release_materialized_paths: GENERIC_REPLAY_LINE,
265-
session_list: GENERIC_REPLAY_LINE,
266-
session_save_script: GENERIC_REPLAY_LINE,
267-
} satisfies Record<ReplayCommandName, ReplayLinePlan>;
268-
269-
/**
270-
* Line templates for every command, resolved through the runtime command
271-
* catalog so a command that exists at runtime but not in the plan fails loudly.
272-
*/
273-
function replayScriptLineArbs(): fc.Arbitrary<string>[] {
274-
const arbitraries: fc.Arbitrary<string>[] = [];
275-
const commands: ReplayCommandName[] = [
276-
...Object.values(PUBLIC_COMMANDS),
277-
...Object.values(INTERNAL_COMMANDS),
278-
];
279-
for (const command of commands) {
280-
const plan: ReplayLinePlan | undefined = REPLAY_SCRIPT_LINE_PLANS[command];
281-
if (plan === undefined) {
282-
throw new Error(
283-
`replay command "${command}" from @agent-device/command-registry/catalog is ` +
284-
'unclassified: ' +
285-
'add a line template or a waiver to REPLAY_SCRIPT_LINE_PLANS in ' +
286-
'packages/test-kit/src/property-arbitraries.ts',
287-
);
288-
}
289-
if (!('waived' in plan)) arbitraries.push(plan);
290-
}
291-
return arbitraries;
292-
}
293-
294-
const replayScriptLineArb: fc.Arbitrary<string> = fc.oneof(...replayScriptLineArbs());
295-
296-
/** A valid `.ad` script: action lines interleaved with comments and blank lines. */
297-
export const replayScriptArb: fc.Arbitrary<string> = fc
298-
.array(
299-
fc.oneof(
300-
{ weight: 8, arbitrary: replayScriptLineArb },
301-
{ weight: 1, arbitrary: fc.constantFrom('# a comment', '') },
302-
),
303-
{ minLength: 1, maxLength: 8 },
304-
)
305-
.map((lines) => `${lines.join('\n')}\n`);

‎packages/test-kit/tsconfig.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
"include": ["src"],
1313
"references": [
1414
{ "path": "../capture-kit" },
15-
{ "path": "../command-registry" },
1615
{ "path": "../contracts" },
17-
{ "path": "../daemon-contracts" },
1816
{ "path": "../host-kit" },
1917
{ "path": "../kernel" }
2018
]

‎pnpm-lock.yaml‎

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎scripts/fuzz/arbitraries.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// reaches deep parser branches that uniformly random noise never gets past the first token of.
99

1010
import fc from 'fast-check';
11-
import { replayScriptArb } from '@agent-device/test-kit';
11+
import { replayScriptArb } from '../../src/commands/replay/replay-script.fixtures.ts';
1212
import type { FuzzTarget, FuzzTargetName } from './target-types.ts';
1313

1414
const SELECTOR_VALUE_HAZARDS = [

packages/test-kit/src/legacy-daemon-fixture.ts renamed to src/__tests__/daemon-registration-owner.fixtures.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ export function spawnLegacyDaemonFixture(paths: DaemonPaths, acquisitionBarrier?
5050
fs.mkdirSync(path.dirname(entry), { recursive: true });
5151
fs.writeFileSync(path.join(codeDir, 'package.json'), '{"type":"module"}');
5252
const processUrl = new URL(
53-
'../../../packages/daemon-contracts/src/daemon-process.ts',
53+
'../../packages/daemon-contracts/src/daemon-process.ts',
5454
import.meta.url,
5555
).href;
56-
const hostProcessUrl = new URL('../../../packages/host-kit/src/process.ts', import.meta.url).href;
56+
const hostProcessUrl = new URL('../../packages/host-kit/src/process.ts', import.meta.url).href;
5757
fs.writeFileSync(
5858
entry,
5959
`

‎src/__tests__/daemon-registration-owner.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
} from '@agent-device/daemon-contracts/daemon-resolution';
2121
import { readRegisteredDaemonOwnership } from '@agent-device/daemon-contracts/daemon-registration';
2222
import { readDaemonShutdownReport } from '@agent-device/daemon-contracts/daemon-shutdown-report';
23-
import { mkdtempForTestSync, spawnLegacyDaemonFixture } from '@agent-device/test-kit';
23+
import { mkdtempForTestSync } from '@agent-device/test-kit';
24+
import { spawnLegacyDaemonFixture } from './daemon-registration-owner.fixtures.ts';
2425
import {
2526
registeredDaemonFixtureArgs,
2627
spawnRegisteredDaemonFixture,

0 commit comments

Comments
 (0)