Skip to content

Commit 02064bb

Browse files
committed
refactor(mcp): project prepare family output schema from its owning module
Moves the single hand-authored MCP outputSchema entry for prepare out of command-output-schemas.ts into a frozen PREPARE_COMMAND_OUTPUT_SCHEMAS map exported from src/commands/management/prepare.ts, following the #2810 projection seam already used by the replay, system, interaction, device-management, push-management, viewport, recording, and capture-wait families on this stack. Registers the family in the PROJECTED_FAMILIES disjointness check and adds command-tools-management-prepare-schemas.test.ts with a reference-equality proof against the module object and an owner-file check, matching the pattern from the other projected families. prepare's descriptor declares no post-action observation trait (#1652), so no settle-graft copy applies here. The moved code is byte-identical to what command-output-schemas.ts held: same objectSchema/numberSchema/stringSchema/enumSchema/constSchema calls and field order. Verified the published outputSchema for prepare is byte-identical to origin/main by dumping COMMAND_OUTPUT_SCHEMAS.prepare as JSON on both refs. Part of #2819.
1 parent 2f27436 commit 02064bb

4 files changed

Lines changed: 124 additions & 61 deletions

File tree

‎src/commands/management/prepare.ts‎

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,106 @@
11
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
22
import type { CommandSchemaOverride } from '@agent-device/command-registry/command-schema';
3-
import { enumField, integerField, requiredField } from '../command-input.ts';
3+
import type { CommandResultMap } from '@agent-device/command-registry/command-result';
4+
import { PUBLIC_PLATFORMS } from '@agent-device/kernel/device';
5+
import {
6+
enumField,
7+
enumSchema,
8+
integerField,
9+
numberSchema,
10+
objectSchema,
11+
requiredField,
12+
stringSchema,
13+
} from '../command-input.ts';
414
import {
515
commonInputFromFlags,
616
direct,
717
requiredDaemonString,
818
requiredString,
919
} from '../cli-grammar/common.ts';
1020
import type { CliReader, DaemonWriter } from '../cli-grammar/types.ts';
21+
import type { JsonSchema } from '../command-contract.ts';
1122
import { defineCommandFacet } from '../family/types.ts';
1223
import { defineFieldCommandMetadata } from '../field-command-contract.ts';
1324
import { managementCliOutputFormatters } from './output.ts';
1425

1526
const PREPARE_ACTION_VALUES = ['ios-runner'] as const;
1627

28+
// Duplicated from packages/kernel/src/device.ts, which exports only the `DeviceKind` type, not
29+
// this value list; importing the published schema back through command-output-schemas.ts would
30+
// cycle, matching the constSchema/DEVICE_KINDS duplication precedent from the other projected
31+
// management families.
32+
const DEVICE_KINDS = ['simulator', 'emulator', 'device'] as const;
33+
34+
function constSchema(value: string): JsonSchema {
35+
return { type: 'string', const: value };
36+
}
37+
38+
/**
39+
* This family's advertised MCP `outputSchema`, keyed by daemon command name and projected into
40+
* the command map by `src/mcp/command-output-schemas.ts`. Non-strict like every other entry: no
41+
* `additionalProperties: false`, so additive response fields keep validating. prepare is not
42+
* MCP-exposed, but the schema stays map-complete with `CommandResultMap`.
43+
*/
44+
export const PREPARE_COMMAND_OUTPUT_SCHEMAS = {
45+
// packages/contracts/src/prepare.ts
46+
prepare: objectSchema(
47+
{
48+
action: constSchema('ios-runner'),
49+
// PublicPlatform leaf, mirroring PrepareCommandResult (packages/contracts/src/prepare.ts).
50+
platform: enumSchema(PUBLIC_PLATFORMS),
51+
deviceId: stringSchema(),
52+
deviceName: stringSchema(),
53+
kind: enumSchema(DEVICE_KINDS),
54+
durationMs: numberSchema(),
55+
runner: objectSchema({}, []),
56+
cache: enumSchema(['exact', 'miss', 'external']),
57+
artifact: enumSchema(['valid', 'rebuilt']),
58+
buildMs: numberSchema(),
59+
connectMs: numberSchema(),
60+
healthCheckMs: numberSchema(),
61+
xctestrunPath: stringSchema(),
62+
recoveryReason: stringSchema(),
63+
failureReason: stringSchema(),
64+
timing: objectSchema(
65+
{
66+
totalMs: numberSchema(),
67+
additiveParts: objectSchema(
68+
{
69+
buildMs: numberSchema(),
70+
connectAfterBuildMs: numberSchema(),
71+
healthCheckMs: numberSchema(),
72+
},
73+
['connectAfterBuildMs', 'healthCheckMs'],
74+
),
75+
containment: objectSchema(
76+
{
77+
connectMs: { type: 'array', items: constSchema('buildMs') },
78+
healthCheckMs: { type: 'array', items: stringSchema() },
79+
},
80+
['healthCheckMs'],
81+
),
82+
note: stringSchema(),
83+
},
84+
['totalMs', 'additiveParts', 'containment', 'note'],
85+
),
86+
message: stringSchema(),
87+
},
88+
[
89+
'action',
90+
'platform',
91+
'deviceId',
92+
'deviceName',
93+
'kind',
94+
'durationMs',
95+
'runner',
96+
'connectMs',
97+
'healthCheckMs',
98+
'timing',
99+
'message',
100+
],
101+
),
102+
} satisfies Pick<Record<keyof CommandResultMap, JsonSchema>, 'prepare'>;
103+
17104
const prepareCommandMetadata = defineFieldCommandMetadata(
18105
'prepare',
19106
'Prepare platform helper infrastructure. ios-runner builds/reuses, starts, and health-checks the XCTest runner so later Apple snapshots and interactions do not pay first-use startup cost. In JSON output, top-level buildMs/connectMs/healthCheckMs are diagnostic fields and may overlap; use timing.additiveParts for additive wall-clock phase totals. In CI, run it after boot/install and before replay/test; if replay/test starts a separate daemon, stop the prepare daemon before replay/test so it does not keep the prepared runner lease. It is not a recovery step for "runner already owned by another agent-device daemon"; stop the owning daemon on the Mac with simulator access instead. Runner build/start output is written to the session runner.log; daemon.log is for daemon lifecycle/startup issues.',
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files';
4+
import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/prepare.ts';
5+
import { COMMAND_OUTPUT_SCHEMAS } from '../command-output-schemas.ts';
6+
7+
const PREPARE_COMMANDS = Object.keys(PREPARE_COMMAND_OUTPUT_SCHEMAS) as Array<
8+
keyof typeof PREPARE_COMMAND_OUTPUT_SCHEMAS
9+
>;
10+
11+
// prepare does not carry the post-action observation trait (#1652): it is a device-runtime
12+
// command, not an interaction command, so the composed map never copies the entry and
13+
// reference equality must hold.
14+
test('MCP prepare family output schemas are the family module entries, not copies', () => {
15+
for (const command of PREPARE_COMMANDS) {
16+
assert.equal(
17+
COMMAND_OUTPUT_SCHEMAS[command],
18+
PREPARE_COMMAND_OUTPUT_SCHEMAS[command],
19+
`${command} is not reference-equal to the prepare module's own schema object`,
20+
);
21+
}
22+
});
23+
24+
test('the projected family map declares exactly the commands its module owns', () => {
25+
for (const command of PREPARE_COMMANDS) {
26+
assert.ok(
27+
ownerFilesForCommand(command).includes('src/commands/management/prepare.ts'),
28+
`${command} projects its output schema from this module but does not name it as its owner`,
29+
);
30+
}
31+
});

‎src/mcp/__tests__/command-tools-replay-schemas.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ReplayCommandResult, ReplaySuiteResult } from '@agent-device/contr
44
import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files';
55
import { commandSupportsSettleObservation } from '@agent-device/command-registry/registry';
66
import { WAIT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/capture/wait.ts';
7+
import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/prepare.ts';
78
import { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/device.ts';
89
import { PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/push.ts';
910
import { VIEWPORT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/viewport.ts';
@@ -79,6 +80,7 @@ const PROJECTED_FAMILIES = [
7980
{ name: 'system', schemas: SYSTEM_COMMAND_OUTPUT_SCHEMAS },
8081
{ name: 'interaction', schemas: INTERACTION_COMMAND_OUTPUT_SCHEMAS },
8182
{ name: 'capture-wait', schemas: WAIT_COMMAND_OUTPUT_SCHEMAS },
83+
{ name: 'management-prepare', schemas: PREPARE_COMMAND_OUTPUT_SCHEMAS },
8284
{ name: 'management-device', schemas: DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS },
8385
{ name: 'management-push', schemas: PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS },
8486
{ name: 'management-viewport', schemas: VIEWPORT_COMMAND_OUTPUT_SCHEMAS },

‎src/mcp/command-output-schemas.ts‎

Lines changed: 3 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import {
1111
stringSchema,
1212
} from '../commands/command-input.ts';
1313
import { WAIT_COMMAND_OUTPUT_SCHEMAS } from '../commands/capture/wait.ts';
14+
import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/prepare.ts';
1415
import { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/device.ts';
1516
import { PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/push.ts';
1617
import { VIEWPORT_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/viewport.ts';
1718
import { INTERACTION_COMMAND_OUTPUT_SCHEMAS } from '../commands/interaction/index.ts';
1819
import { RECORDING_COMMAND_OUTPUT_SCHEMAS } from '../commands/recording/index.ts';
1920
import { REPLAY_COMMAND_OUTPUT_SCHEMAS } from '../commands/replay/index.ts';
2021
import { SYSTEM_COMMAND_OUTPUT_SCHEMAS } from '../commands/system/index.ts';
21-
import { DEVICE_TARGETS, PUBLIC_PLATFORMS } from '@agent-device/kernel/device';
22+
import { DEVICE_TARGETS } from '@agent-device/kernel/device';
2223

2324
/**
2425
* Registry of per-command MCP `outputSchema`s, keyed by the daemon command
@@ -200,65 +201,6 @@ const BASE_COMMAND_OUTPUT_SCHEMAS = {
200201
['direction'],
201202
),
202203

203-
// packages/contracts/src/prepare.ts — prepare is not MCP-exposed, but the schema stays
204-
// map-complete with CommandResultMap.
205-
prepare: objectSchema(
206-
{
207-
action: constSchema('ios-runner'),
208-
// PublicPlatform leaf, mirroring PrepareCommandResult (packages/contracts/src/prepare.ts).
209-
platform: enumSchema(PUBLIC_PLATFORMS),
210-
deviceId: stringSchema(),
211-
deviceName: stringSchema(),
212-
kind: enumSchema(DEVICE_KINDS),
213-
durationMs: numberSchema(),
214-
runner: objectSchema({}, []),
215-
cache: enumSchema(['exact', 'miss', 'external']),
216-
artifact: enumSchema(['valid', 'rebuilt']),
217-
buildMs: numberSchema(),
218-
connectMs: numberSchema(),
219-
healthCheckMs: numberSchema(),
220-
xctestrunPath: stringSchema(),
221-
recoveryReason: stringSchema(),
222-
failureReason: stringSchema(),
223-
timing: objectSchema(
224-
{
225-
totalMs: numberSchema(),
226-
additiveParts: objectSchema(
227-
{
228-
buildMs: numberSchema(),
229-
connectAfterBuildMs: numberSchema(),
230-
healthCheckMs: numberSchema(),
231-
},
232-
['connectAfterBuildMs', 'healthCheckMs'],
233-
),
234-
containment: objectSchema(
235-
{
236-
connectMs: { type: 'array', items: constSchema('buildMs') },
237-
healthCheckMs: { type: 'array', items: stringSchema() },
238-
},
239-
['healthCheckMs'],
240-
),
241-
note: stringSchema(),
242-
},
243-
['totalMs', 'additiveParts', 'containment', 'note'],
244-
),
245-
message: stringSchema(),
246-
},
247-
[
248-
'action',
249-
'platform',
250-
'deviceId',
251-
'deviceName',
252-
'kind',
253-
'durationMs',
254-
'runner',
255-
'connectMs',
256-
'healthCheckMs',
257-
'timing',
258-
'message',
259-
],
260-
),
261-
262204
// packages/contracts/src/doctor.ts
263205
doctor: objectSchema(
264206
{
@@ -321,6 +263,7 @@ const BASE_COMMAND_OUTPUT_SCHEMAS = {
321263
// a projected command then fails as TS2783 instead of quietly overriding the family's,
322264
// and this map's `satisfies` still refuses a missing `CommandResultMap` key.
323265
...WAIT_COMMAND_OUTPUT_SCHEMAS,
266+
...PREPARE_COMMAND_OUTPUT_SCHEMAS,
324267
...DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS,
325268
...PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS,
326269
...VIEWPORT_COMMAND_OUTPUT_SCHEMAS,

0 commit comments

Comments
 (0)