From baad0ccfa1c48bb521b933e4f41009791fffd1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 28 Sep 2026 15:06:21 +0200 Subject: [PATCH 1/2] 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. --- src/commands/management/prepare.ts | 80 ++++++++++++++++++- ...d-tools-management-prepare-schemas.test.ts | 31 +++++++ .../command-tools-replay-schemas.test.ts | 2 + src/mcp/command-output-schemas.ts | 61 +------------- 4 files changed, 114 insertions(+), 60 deletions(-) create mode 100644 src/mcp/__tests__/command-tools-management-prepare-schemas.test.ts diff --git a/src/commands/management/prepare.ts b/src/commands/management/prepare.ts index c126ad5b9d..478a8c0799 100644 --- a/src/commands/management/prepare.ts +++ b/src/commands/management/prepare.ts @@ -1,6 +1,17 @@ import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import type { CommandSchemaOverride } from '@agent-device/command-registry/command-schema'; -import { enumField, integerField, requiredField } from '../command-input.ts'; +import type { CommandResultMap } from '@agent-device/command-registry/command-result'; +import { DEVICE_KINDS, PUBLIC_PLATFORMS } from '@agent-device/kernel/device'; +import { + constSchema, + enumField, + enumSchema, + integerField, + numberSchema, + objectSchema, + requiredField, + stringSchema, +} from '../command-input.ts'; import { commonInputFromFlags, direct, @@ -8,12 +19,79 @@ import { requiredString, } from '../cli-grammar/common.ts'; import type { CliReader, DaemonWriter } from '../cli-grammar/types.ts'; +import type { JsonSchema } from '../command-contract.ts'; import { defineCommandFacet } from '../family/types.ts'; import { defineFieldCommandMetadata } from '../field-command-contract.ts'; import { managementCliOutputFormatters } from './output.ts'; const PREPARE_ACTION_VALUES = ['ios-runner'] as const; +/** + * This family's advertised MCP `outputSchema`, keyed by daemon command name and projected into + * the command map by `src/mcp/command-output-schemas.ts`. Non-strict like every other entry: no + * `additionalProperties: false`, so additive response fields keep validating. prepare is not + * MCP-exposed, but the schema stays map-complete with `CommandResultMap`. + */ +export const PREPARE_COMMAND_OUTPUT_SCHEMAS = { + // packages/contracts/src/prepare.ts + prepare: objectSchema( + { + action: constSchema('ios-runner'), + // PublicPlatform leaf, mirroring PrepareCommandResult (packages/contracts/src/prepare.ts). + platform: enumSchema(PUBLIC_PLATFORMS), + deviceId: stringSchema(), + deviceName: stringSchema(), + kind: enumSchema(DEVICE_KINDS), + durationMs: numberSchema(), + runner: objectSchema({}, []), + cache: enumSchema(['exact', 'miss', 'external']), + artifact: enumSchema(['valid', 'rebuilt']), + buildMs: numberSchema(), + connectMs: numberSchema(), + healthCheckMs: numberSchema(), + xctestrunPath: stringSchema(), + recoveryReason: stringSchema(), + failureReason: stringSchema(), + timing: objectSchema( + { + totalMs: numberSchema(), + additiveParts: objectSchema( + { + buildMs: numberSchema(), + connectAfterBuildMs: numberSchema(), + healthCheckMs: numberSchema(), + }, + ['connectAfterBuildMs', 'healthCheckMs'], + ), + containment: objectSchema( + { + connectMs: { type: 'array', items: constSchema('buildMs') }, + healthCheckMs: { type: 'array', items: stringSchema() }, + }, + ['healthCheckMs'], + ), + note: stringSchema(), + }, + ['totalMs', 'additiveParts', 'containment', 'note'], + ), + message: stringSchema(), + }, + [ + 'action', + 'platform', + 'deviceId', + 'deviceName', + 'kind', + 'durationMs', + 'runner', + 'connectMs', + 'healthCheckMs', + 'timing', + 'message', + ], + ), +} satisfies Pick, 'prepare'>; + const prepareCommandMetadata = defineFieldCommandMetadata( 'prepare', '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.', diff --git a/src/mcp/__tests__/command-tools-management-prepare-schemas.test.ts b/src/mcp/__tests__/command-tools-management-prepare-schemas.test.ts new file mode 100644 index 0000000000..6ef57c3e87 --- /dev/null +++ b/src/mcp/__tests__/command-tools-management-prepare-schemas.test.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files'; +import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/prepare.ts'; +import { COMMAND_OUTPUT_SCHEMAS } from '../command-output-schemas.ts'; + +const PREPARE_COMMANDS = Object.keys(PREPARE_COMMAND_OUTPUT_SCHEMAS) as Array< + keyof typeof PREPARE_COMMAND_OUTPUT_SCHEMAS +>; + +// prepare does not carry the post-action observation trait (#1652): it is a device-runtime +// command, not an interaction command, so the composed map never copies the entry and +// reference equality must hold. +test('MCP prepare family output schemas are the family module entries, not copies', () => { + for (const command of PREPARE_COMMANDS) { + assert.equal( + COMMAND_OUTPUT_SCHEMAS[command], + PREPARE_COMMAND_OUTPUT_SCHEMAS[command], + `${command} is not reference-equal to the prepare module's own schema object`, + ); + } +}); + +test('the projected family map declares exactly the commands its module owns', () => { + for (const command of PREPARE_COMMANDS) { + assert.ok( + ownerFilesForCommand(command).includes('src/commands/management/prepare.ts'), + `${command} projects its output schema from this module but does not name it as its owner`, + ); + } +}); diff --git a/src/mcp/__tests__/command-tools-replay-schemas.test.ts b/src/mcp/__tests__/command-tools-replay-schemas.test.ts index a5943140af..4d1be21c1c 100644 --- a/src/mcp/__tests__/command-tools-replay-schemas.test.ts +++ b/src/mcp/__tests__/command-tools-replay-schemas.test.ts @@ -4,6 +4,7 @@ import type { ReplayCommandResult, ReplaySuiteResult } from '@agent-device/contr import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files'; import { commandSupportsSettleObservation } from '@agent-device/command-registry/registry'; import { WAIT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/capture/wait.ts'; +import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/prepare.ts'; import { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/device.ts'; import { PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/push.ts'; import { VIEWPORT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/viewport.ts'; @@ -79,6 +80,7 @@ const PROJECTED_FAMILIES = [ { name: 'system', schemas: SYSTEM_COMMAND_OUTPUT_SCHEMAS }, { name: 'interaction', schemas: INTERACTION_COMMAND_OUTPUT_SCHEMAS }, { name: 'capture-wait', schemas: WAIT_COMMAND_OUTPUT_SCHEMAS }, + { name: 'management-prepare', schemas: PREPARE_COMMAND_OUTPUT_SCHEMAS }, { name: 'management-device', schemas: DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS }, { name: 'management-push', schemas: PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS }, { name: 'management-viewport', schemas: VIEWPORT_COMMAND_OUTPUT_SCHEMAS }, diff --git a/src/mcp/command-output-schemas.ts b/src/mcp/command-output-schemas.ts index 6d12e1e90d..ff8d9f406e 100644 --- a/src/mcp/command-output-schemas.ts +++ b/src/mcp/command-output-schemas.ts @@ -12,6 +12,7 @@ import { stringSchema, } from '../commands/command-input.ts'; import { WAIT_COMMAND_OUTPUT_SCHEMAS } from '../commands/capture/wait.ts'; +import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/prepare.ts'; import { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/device.ts'; import { PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/push.ts'; import { VIEWPORT_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/viewport.ts'; @@ -183,65 +184,6 @@ const BASE_COMMAND_OUTPUT_SCHEMAS = { ['direction'], ), - // packages/contracts/src/prepare.ts — prepare is not MCP-exposed, but the schema stays - // map-complete with CommandResultMap. - prepare: objectSchema( - { - action: constSchema('ios-runner'), - // PublicPlatform leaf, mirroring PrepareCommandResult (packages/contracts/src/prepare.ts). - platform: enumSchema(PUBLIC_PLATFORMS), - deviceId: stringSchema(), - deviceName: stringSchema(), - kind: enumSchema(DEVICE_KINDS), - durationMs: numberSchema(), - runner: objectSchema({}, []), - cache: enumSchema(['exact', 'miss', 'external']), - artifact: enumSchema(['valid', 'rebuilt']), - buildMs: numberSchema(), - connectMs: numberSchema(), - healthCheckMs: numberSchema(), - xctestrunPath: stringSchema(), - recoveryReason: stringSchema(), - failureReason: stringSchema(), - timing: objectSchema( - { - totalMs: numberSchema(), - additiveParts: objectSchema( - { - buildMs: numberSchema(), - connectAfterBuildMs: numberSchema(), - healthCheckMs: numberSchema(), - }, - ['connectAfterBuildMs', 'healthCheckMs'], - ), - containment: objectSchema( - { - connectMs: { type: 'array', items: constSchema('buildMs') }, - healthCheckMs: { type: 'array', items: stringSchema() }, - }, - ['healthCheckMs'], - ), - note: stringSchema(), - }, - ['totalMs', 'additiveParts', 'containment', 'note'], - ), - message: stringSchema(), - }, - [ - 'action', - 'platform', - 'deviceId', - 'deviceName', - 'kind', - 'durationMs', - 'runner', - 'connectMs', - 'healthCheckMs', - 'timing', - 'message', - ], - ), - // packages/contracts/src/doctor.ts doctor: objectSchema( { @@ -304,6 +246,7 @@ const BASE_COMMAND_OUTPUT_SCHEMAS = { // a projected command then fails as TS2783 instead of quietly overriding the family's, // and this map's `satisfies` still refuses a missing `CommandResultMap` key. ...WAIT_COMMAND_OUTPUT_SCHEMAS, + ...PREPARE_COMMAND_OUTPUT_SCHEMAS, ...DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS, ...PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS, ...VIEWPORT_COMMAND_OUTPUT_SCHEMAS, From 0aeb0adf8275a7937c72dcb3c25dd0965aa56a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 29 Sep 2026 08:25:54 +0200 Subject: [PATCH 2/2] refactor(mcp): import the shared constSchema and DEVICE_KINDS in the prepare family --- src/mcp/command-output-schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/command-output-schemas.ts b/src/mcp/command-output-schemas.ts index ff8d9f406e..98f95291bc 100644 --- a/src/mcp/command-output-schemas.ts +++ b/src/mcp/command-output-schemas.ts @@ -23,7 +23,7 @@ import { import { RECORDING_COMMAND_OUTPUT_SCHEMAS } from '../commands/recording/output-schemas.ts'; import { REPLAY_COMMAND_OUTPUT_SCHEMAS } from '../commands/replay/index.ts'; import { SYSTEM_COMMAND_OUTPUT_SCHEMAS } from '../commands/system/index.ts'; -import { DEVICE_KINDS, DEVICE_TARGETS, PUBLIC_PLATFORMS } from '@agent-device/kernel/device'; +import { DEVICE_TARGETS } from '@agent-device/kernel/device'; /** * Registry of per-command MCP `outputSchema`s, keyed by the daemon command