Skip to content

Commit 5f47c18

Browse files
committed
refactor(mcp): project device management family output schemas from their owning module
Moves the 2 hand-authored MCP outputSchema entries for boot and shutdown out of command-output-schemas.ts into a frozen DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS map exported from src/commands/management/device.ts, following the #2810 projection seam already used for the interaction and system families on this stack. Registers the family in the PROJECTED_FAMILIES disjointness check and adds a colocated command-tools-management-device-schemas.test.ts proving reference equality with the module object. Neither command carries the post-action observation trait, so no settle-graft copy applies here. DEVICE_KINDS is duplicated locally rather than imported back from command-output-schemas.ts (which still needs its own copy for `prepare`), matching the constSchema/responseCostSchema duplication precedent from the system and interaction families' moves. Part of #2819.
1 parent 00d88dd commit 5f47c18

4 files changed

Lines changed: 95 additions & 35 deletions

File tree

‎src/commands/management/device.ts‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,71 @@
11
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
22
import type { CommandSchemaOverride } from '@agent-device/command-registry/command-schema';
3-
import { booleanField } from '../command-input.ts';
3+
import type { CommandResultMap } from '@agent-device/command-registry/command-result';
4+
import { DEVICE_TARGETS, PUBLIC_PLATFORMS } from '@agent-device/kernel/device';
5+
import {
6+
booleanField,
7+
booleanSchema,
8+
enumSchema,
9+
looseObjectSchema,
10+
numberSchema,
11+
objectSchema,
12+
stringSchema,
13+
} from '../command-input.ts';
14+
import type { JsonSchema } from '../command-contract.ts';
415
import { commonInputFromFlags, direct } from '../cli-grammar/common.ts';
516
import type { CliReader, DaemonWriter } from '../cli-grammar/types.ts';
617
import { defineCommandFacet } from '../family/types.ts';
718
import { defineFieldCommandMetadata } from '../field-command-contract.ts';
819
import { managementCliOutputFormatters } from './output.ts';
920

21+
// Duplicated from packages/kernel/src/device.ts, which exports only the `DeviceKind` type, not
22+
// this value list; importing the published schema back through command-output-schemas.ts would
23+
// cycle, matching the constSchema/responseCostSchema duplication precedent from the system and
24+
// interaction families' moves.
25+
const DEVICE_KINDS = ['simulator', 'emulator', 'device'] as const;
26+
27+
// boot / shutdown share the resolved-device header (packages/contracts/src/device.ts).
28+
const deviceHeaderProperties: Record<string, JsonSchema> = {
29+
// Public leaf vocabulary (ios | macos | android | linux | web): boot/shutdown
30+
// emit publicPlatformString, never the internal `apple` platform.
31+
platform: enumSchema(PUBLIC_PLATFORMS),
32+
target: enumSchema(DEVICE_TARGETS),
33+
device: stringSchema('Human-readable device name.'),
34+
id: stringSchema('Stable device id.'),
35+
kind: enumSchema(DEVICE_KINDS),
36+
};
37+
const deviceHeaderRequired = ['platform', 'target', 'device', 'id', 'kind'] as const;
38+
39+
// TargetShutdownResult (packages/contracts/src/target-shutdown-contract.ts).
40+
const targetShutdownResultSchema: JsonSchema = objectSchema(
41+
{
42+
success: booleanSchema(),
43+
exitCode: numberSchema(),
44+
stdout: stringSchema(),
45+
stderr: stringSchema(),
46+
error: looseObjectSchema('Normalized error detail when shutdown failed.'),
47+
},
48+
['success', 'exitCode', 'stdout', 'stderr'],
49+
);
50+
51+
/**
52+
* This family's advertised MCP `outputSchema`s, keyed by daemon command name and projected into
53+
* the command map by `src/mcp/command-output-schemas.ts`. Non-strict like every other entry: no
54+
* `additionalProperties: false`, so additive response fields such as `cost` keep validating.
55+
* Neither command carries the post-action observation trait (#1652): both are device-runtime
56+
* commands, not interaction commands, so no settle graft applies here.
57+
*/
58+
export const DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS = {
59+
boot: objectSchema({ ...deviceHeaderProperties, booted: { type: 'boolean', const: true } }, [
60+
...deviceHeaderRequired,
61+
'booted',
62+
]),
63+
shutdown: objectSchema({ ...deviceHeaderProperties, shutdown: targetShutdownResultSchema }, [
64+
...deviceHeaderRequired,
65+
'shutdown',
66+
]),
67+
} satisfies Pick<Record<keyof CommandResultMap, JsonSchema>, 'boot' | 'shutdown'>;
68+
1069
const devicesCommandMetadata = defineFieldCommandMetadata(
1170
'devices',
1271
'List available devices and simulators that can be selected for automation. Use platform, device, udid, or serial inputs on later commands to target one result.',
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 { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/device.ts';
5+
import { COMMAND_OUTPUT_SCHEMAS } from '../command-output-schemas.ts';
6+
7+
const DEVICE_MANAGEMENT_COMMANDS = Object.keys(DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS) as Array<
8+
keyof typeof DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS
9+
>;
10+
11+
// Neither boot nor shutdown carries the post-action observation trait (#1652): both are
12+
// device-runtime commands, not interaction commands, so the composed map never copies either
13+
// entry and reference equality must hold for both.
14+
test('MCP device management family output schemas are the family module entries, not copies', () => {
15+
for (const command of DEVICE_MANAGEMENT_COMMANDS) {
16+
assert.equal(
17+
COMMAND_OUTPUT_SCHEMAS[command],
18+
DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS[command],
19+
`${command} is not reference-equal to the device management 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 DEVICE_MANAGEMENT_COMMANDS) {
26+
assert.ok(
27+
ownerFilesForCommand(command).includes('src/commands/management/device.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
@@ -3,6 +3,7 @@ import { test } from 'vitest';
33
import type { ReplayCommandResult, ReplaySuiteResult } from '@agent-device/contracts/replay';
44
import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files';
55
import { commandSupportsSettleObservation } from '@agent-device/command-registry/registry';
6+
import { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/device.ts';
67
import { INTERACTION_COMMAND_OUTPUT_SCHEMAS } from '../../commands/interaction/index.ts';
78
import { REPLAY_COMMAND_OUTPUT_SCHEMAS } from '../../commands/replay/index.ts';
89
import { SYSTEM_COMMAND_OUTPUT_SCHEMAS } from '../../commands/system/index.ts';
@@ -73,6 +74,7 @@ const PROJECTED_FAMILIES = [
7374
{ name: 'replay', schemas: REPLAY_COMMAND_OUTPUT_SCHEMAS },
7475
{ name: 'system', schemas: SYSTEM_COMMAND_OUTPUT_SCHEMAS },
7576
{ name: 'interaction', schemas: INTERACTION_COMMAND_OUTPUT_SCHEMAS },
77+
{ name: 'management-device', schemas: DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS },
7678
] as const;
7779

7880
test('projected output-schema families claim disjoint commands and survive the composition intact', () => {

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

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
stringArraySchema,
1111
stringSchema,
1212
} from '../commands/command-input.ts';
13+
import { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/device.ts';
1314
import { INTERACTION_COMMAND_OUTPUT_SCHEMAS } from '../commands/interaction/index.ts';
1415
import { REPLAY_COMMAND_OUTPUT_SCHEMAS } from '../commands/replay/index.ts';
1516
import { SYSTEM_COMMAND_OUTPUT_SCHEMAS } from '../commands/system/index.ts';
@@ -147,30 +148,6 @@ const settleObservationSchema: JsonSchema = objectSchema(
147148
['settled', 'waitedMs', 'captures', 'quietMs', 'timeoutMs'],
148149
);
149150

150-
// boot / shutdown share the resolved-device header (packages/contracts/src/device.ts).
151-
const deviceHeaderProperties: Record<string, JsonSchema> = {
152-
// Public leaf vocabulary (ios | macos | android | linux | web): boot/shutdown
153-
// emit publicPlatformString, never the internal `apple` platform.
154-
platform: enumSchema(PUBLIC_PLATFORMS),
155-
target: enumSchema(DEVICE_TARGETS),
156-
device: stringSchema('Human-readable device name.'),
157-
id: stringSchema('Stable device id.'),
158-
kind: enumSchema(DEVICE_KINDS),
159-
};
160-
const deviceHeaderRequired = ['platform', 'target', 'device', 'id', 'kind'] as const;
161-
162-
// TargetShutdownResult (packages/contracts/src/target-shutdown-contract.ts).
163-
const targetShutdownResultSchema: JsonSchema = objectSchema(
164-
{
165-
success: booleanSchema(),
166-
exitCode: numberSchema(),
167-
stdout: stringSchema(),
168-
stderr: stringSchema(),
169-
error: looseObjectSchema('Normalized error detail when shutdown failed.'),
170-
},
171-
['success', 'exitCode', 'stdout', 'stderr'],
172-
);
173-
174151
/** Grafts the opt-in `--settle` observation onto a closed schema or union branch. */
175152
function withSettleObservation(schema: JsonSchema): JsonSchema {
176153
// Union-shaped results (fill) carry the observation in EACH branch, never
@@ -204,16 +181,6 @@ function deriveSettleObservationSchemas(
204181
}
205182

206183
const BASE_COMMAND_OUTPUT_SCHEMAS = {
207-
// packages/contracts/src/device.ts
208-
boot: objectSchema({ ...deviceHeaderProperties, booted: { type: 'boolean', const: true } }, [
209-
...deviceHeaderRequired,
210-
'booted',
211-
]),
212-
shutdown: objectSchema({ ...deviceHeaderProperties, shutdown: targetShutdownResultSchema }, [
213-
...deviceHeaderRequired,
214-
'shutdown',
215-
]),
216-
217184
// packages/contracts/src/viewport.ts
218185
viewport: objectSchema(
219186
{ width: numberSchema(), height: numberSchema(), message: stringSchema() },
@@ -479,6 +446,7 @@ const BASE_COMMAND_OUTPUT_SCHEMAS = {
479446
// command surface and projects it here. This spread stays last: a hand-written entry for
480447
// a projected command then fails as TS2783 instead of quietly overriding the family's,
481448
// and this map's `satisfies` still refuses a missing `CommandResultMap` key.
449+
...DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS,
482450
...INTERACTION_COMMAND_OUTPUT_SCHEMAS,
483451
...REPLAY_COMMAND_OUTPUT_SCHEMAS,
484452
...SYSTEM_COMMAND_OUTPUT_SCHEMAS,

0 commit comments

Comments
 (0)