From 79d6b5c9f3ec715b94fb1e64c8d6ecb73c50c402 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 16 Jul 2026 12:39:11 +0100 Subject: [PATCH 01/23] feat(xcodebuild): Support prepared test artifacts Extend existing build and test tools to prepare, return, and consume reusable test artifacts while preserving normal build behavior. Fixes #450 --- CHANGELOG.md | 2 +- README.md | 6 + manifests/tools/build_device.yaml | 7 +- manifests/tools/build_macos.yaml | 7 +- manifests/tools/build_sim.yaml | 7 +- manifests/tools/test_device.yaml | 2 +- manifests/tools/test_macos.yaml | 2 +- manifests/tools/test_sim.yaml | 2 +- .../3.schema.json | 122 +++++++ .../3.schema.json | 148 +++++++++ src/cli/commands/purge-ui.ts | 3 +- src/cli/commands/purge.ts | 2 +- .../structured-output-schema.test.ts | 48 +++ .../device/__tests__/build_device.test.ts | 76 ++++- .../device/__tests__/test_device.test.ts | 29 +- src/mcp/tools/device/build_device.ts | 127 +++++-- src/mcp/tools/device/test_device.ts | 62 ++-- .../tools/macos/__tests__/build_macos.test.ts | 43 ++- .../tools/macos/__tests__/test_macos.test.ts | 45 +-- src/mcp/tools/macos/build_macos.ts | 118 ++++++- src/mcp/tools/macos/test_macos.ts | 64 ++-- .../simulator/__tests__/build_sim.test.ts | 64 +++- .../simulator/__tests__/test_sim.test.ts | 12 +- src/mcp/tools/simulator/build_sim.ts | 98 +++++- src/mcp/tools/simulator/test_sim.ts | 68 ++-- src/types/domain-fragments.ts | 3 + src/types/domain-results.ts | 4 + .../__tests__/prepared-test-execution.test.ts | 110 +++++++ src/utils/__tests__/test-common.test.ts | 118 +++++-- .../__tests__/test-products-lifecycle.test.ts | 111 +++++++ .../__tests__/test-products-path.test.ts | 73 +++++ .../__tests__/test-products-purge.test.ts | 73 +++++ src/utils/__tests__/test-source.test.ts | 61 ++++ src/utils/build-utils.ts | 9 + src/utils/log-paths.ts | 3 + src/utils/purge-storage/enumerate.ts | 2 + src/utils/purge-storage/execution.ts | 46 ++- src/utils/purge-storage/planning.ts | 3 + src/utils/purge-storage/test-products.ts | 155 +++++++++ src/utils/purge-storage/types.ts | 2 + .../__tests__/domain-result-text.test.ts | 41 +++ src/utils/renderers/domain-result-text.ts | 23 ++ src/utils/test-common.ts | 309 ++++++++++++------ src/utils/test-products-lifecycle.ts | 111 +++++++ src/utils/test-products-path.ts | 113 +++++++ src/utils/test-source.ts | 129 ++++++++ src/utils/workspace-filesystem-lifecycle.ts | 14 +- src/utils/xcodebuild-domain-results.ts | 3 +- 48 files changed, 2373 insertions(+), 307 deletions(-) create mode 100644 schemas/structured-output/xcodebuildmcp.output.build-result/3.schema.json create mode 100644 schemas/structured-output/xcodebuildmcp.output.test-result/3.schema.json create mode 100644 src/utils/__tests__/prepared-test-execution.test.ts create mode 100644 src/utils/__tests__/test-products-lifecycle.test.ts create mode 100644 src/utils/__tests__/test-products-path.test.ts create mode 100644 src/utils/__tests__/test-products-purge.test.ts create mode 100644 src/utils/__tests__/test-source.test.ts create mode 100644 src/utils/purge-storage/test-products.ts create mode 100644 src/utils/test-products-lifecycle.ts create mode 100644 src/utils/test-products-path.ts create mode 100644 src/utils/test-source.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9194236bd..352993bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added `xcodebuildmcp purge` to report and explicitly clean XcodeBuildMCP-managed workspace storage, including opt-in DerivedData cleanup with dry-run and confirmation safeguards. - Added `extraArgs` as a first-class session-default value. Repo config or runtime defaults can now carry common `xcodebuild` flags (for example `-skipPackagePluginValidation` or `-disableAutomaticPackageResolution`) so they don't need repeating on every build or test call. Per-call `extraArgs` replace matching configured flags or build settings and append after non-matching defaults, while an explicit empty array (`extraArgs: []`) clears the defaults for a single call. The session management tools show, set, sync, and clear `extraArgs` alongside the other defaults. +- Added reusable test preparation to the existing simulator, device, and macOS build tools. Builds can now return a portable `.xctestproducts` artifact, and test tools can run that artifact—or an advanced `.xctestrun` input—without rebuilding before producing a new `.xcresult` ([#450](https://github.com/getsentry/XcodeBuildMCP/issues/450)). ### Fixed @@ -693,4 +694,3 @@ Please note that the UI automation features are an early preview and currently i - Initial release of XcodeBuildMCP - Basic support for building iOS and macOS applications - diff --git a/README.md b/README.md index 0bc513ff4..46914c2e8 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,12 @@ xcodebuildmcp tools # Build for simulator xcodebuildmcp simulator build --scheme MyApp --project-path ./MyApp.xcodeproj + +# Prepare portable test products without running tests +xcodebuildmcp simulator build --scheme MyApp --project-path ./MyApp.xcodeproj --simulator-name "iPhone 17" --build-for-testing --test-products-path ./MyApp.xctestproducts + +# Run previously prepared test products +xcodebuildmcp simulator test --test-products-path ./MyApp.xctestproducts --simulator-name "iPhone 17" ``` Check for updates and upgrade in place: diff --git a/manifests/tools/build_device.yaml b/manifests/tools/build_device.yaml index 8f1c716a3..e02d03180 100644 --- a/manifests/tools/build_device.yaml +++ b/manifests/tools/build_device.yaml @@ -6,7 +6,7 @@ names: description: Build for device. outputSchema: schema: xcodebuildmcp.output.build-result - version: "2" + version: '3' predicates: - hideWhenXcodeAgentMode annotations: @@ -14,8 +14,3 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false -nextSteps: - - label: Get built device app path - toolId: get_device_app_path - priority: 1 - when: success diff --git a/manifests/tools/build_macos.yaml b/manifests/tools/build_macos.yaml index 51c4f4f24..a7de53cf6 100644 --- a/manifests/tools/build_macos.yaml +++ b/manifests/tools/build_macos.yaml @@ -6,7 +6,7 @@ names: description: Build macOS app. outputSchema: schema: xcodebuildmcp.output.build-result - version: "2" + version: '3' predicates: - hideWhenXcodeAgentMode annotations: @@ -14,8 +14,3 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false -nextSteps: - - label: Get built macOS app path - toolId: get_mac_app_path - priority: 1 - when: success diff --git a/manifests/tools/build_sim.yaml b/manifests/tools/build_sim.yaml index e7305ee09..2e06bbfa6 100644 --- a/manifests/tools/build_sim.yaml +++ b/manifests/tools/build_sim.yaml @@ -6,7 +6,7 @@ names: description: Build for iOS sim (compile-only, no launch). outputSchema: schema: xcodebuildmcp.output.build-result - version: "2" + version: '3' predicates: - hideWhenXcodeAgentMode annotations: @@ -14,8 +14,3 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false -nextSteps: - - label: Get built app path in simulator derived data - toolId: get_sim_app_path - priority: 1 - when: success diff --git a/manifests/tools/test_device.yaml b/manifests/tools/test_device.yaml index 3f0562f1d..7f5677f7f 100644 --- a/manifests/tools/test_device.yaml +++ b/manifests/tools/test_device.yaml @@ -6,7 +6,7 @@ names: description: Test on device. outputSchema: schema: xcodebuildmcp.output.test-result - version: "2" + version: "3" predicates: - hideWhenXcodeAgentMode annotations: diff --git a/manifests/tools/test_macos.yaml b/manifests/tools/test_macos.yaml index ef5db1325..6100ebe37 100644 --- a/manifests/tools/test_macos.yaml +++ b/manifests/tools/test_macos.yaml @@ -6,7 +6,7 @@ names: description: Test macOS target. outputSchema: schema: xcodebuildmcp.output.test-result - version: "2" + version: "3" predicates: - hideWhenXcodeAgentMode annotations: diff --git a/manifests/tools/test_sim.yaml b/manifests/tools/test_sim.yaml index d15375220..1e2e527a9 100644 --- a/manifests/tools/test_sim.yaml +++ b/manifests/tools/test_sim.yaml @@ -6,7 +6,7 @@ names: description: Test on iOS sim. outputSchema: schema: xcodebuildmcp.output.test-result - version: "2" + version: "3" predicates: - hideWhenXcodeAgentMode annotations: diff --git a/schemas/structured-output/xcodebuildmcp.output.build-result/3.schema.json b/schemas/structured-output/xcodebuildmcp.output.build-result/3.schema.json new file mode 100644 index 000000000..a1ac882a9 --- /dev/null +++ b/schemas/structured-output/xcodebuildmcp.output.build-result/3.schema.json @@ -0,0 +1,122 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.build-result/3.schema.json", + "type": "object", + "additionalProperties": false, + "allOf": [ + { + "$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/errorConsistency" + } + ], + "$defs": { + "buildInvocationRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "buildForTesting": { "type": "boolean" }, + "scheme": { "type": "string" }, + "workspacePath": { "type": "string" }, + "projectPath": { "type": "string" }, + "packagePath": { "type": "string" }, + "targetName": { "type": "string" }, + "configuration": { "type": "string" }, + "platform": { "type": "string" }, + "target": { + "enum": ["simulator", "device", "macos", "swift-package"] + }, + "simulatorName": { "type": "string" }, + "simulatorId": { "type": "string" }, + "deviceId": { "type": "string" }, + "executableName": { "type": "string" }, + "arch": { "type": "string" }, + "derivedDataPath": { "type": "string" }, + "testProductsPath": { "type": "string" }, + "xctestrunPath": { "type": "string" }, + "onlyTesting": { + "type": "array", + "items": { "type": "string" } + }, + "skipTesting": { + "type": "array", + "items": { "type": "string" } + } + } + } + }, + "properties": { + "schema": { + "const": "xcodebuildmcp.output.build-result" + }, + "schemaVersion": { + "const": "3" + }, + "didError": { + "type": "boolean" + }, + "error": { + "type": ["string", "null"] + }, + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "request": { + "$ref": "#/$defs/buildInvocationRequest" + }, + "summary": { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "enum": ["SUCCEEDED", "FAILED"] + }, + "durationMs": { + "type": "integer", + "minimum": 0 + }, + "target": { + "enum": ["simulator", "device", "macos", "swift-package"] + } + }, + "required": ["status"] + }, + "artifacts": { + "type": "object", + "additionalProperties": false, + "properties": { + "appPath": { "type": "string" }, + "bundleId": { "type": "string" }, + "buildLogPath": { "type": "string" }, + "packagePath": { "type": "string" }, + "workspacePath": { "type": "string" }, + "scheme": { "type": "string" }, + "configuration": { "type": "string" }, + "platform": { "type": "string" }, + "testProductsPath": { "type": "string" }, + "xctestrunPaths": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": [], + "minProperties": 1 + }, + "diagnostics": { + "$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/basicDiagnostics" + } + }, + "required": ["summary", "artifacts", "diagnostics"] + }, + { + "type": "null" + } + ] + }, + "nextSteps": { + "$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/nextSteps" + } + }, + "required": ["schema", "schemaVersion", "didError", "error", "data"] +} diff --git a/schemas/structured-output/xcodebuildmcp.output.test-result/3.schema.json b/schemas/structured-output/xcodebuildmcp.output.test-result/3.schema.json new file mode 100644 index 000000000..4cc8d3fd2 --- /dev/null +++ b/schemas/structured-output/xcodebuildmcp.output.test-result/3.schema.json @@ -0,0 +1,148 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://xcodebuildmcp.com/schemas/structured-output/xcodebuildmcp.output.test-result/3.schema.json", + "type": "object", + "additionalProperties": false, + "allOf": [ + { + "$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/errorConsistency" + } + ], + "$defs": { + "buildInvocationRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "buildForTesting": { "type": "boolean" }, + "scheme": { "type": "string" }, + "workspacePath": { "type": "string" }, + "projectPath": { "type": "string" }, + "packagePath": { "type": "string" }, + "targetName": { "type": "string" }, + "configuration": { "type": "string" }, + "platform": { "type": "string" }, + "target": { + "enum": ["simulator", "device", "macos", "swift-package"] + }, + "simulatorName": { "type": "string" }, + "simulatorId": { "type": "string" }, + "deviceId": { "type": "string" }, + "executableName": { "type": "string" }, + "arch": { "type": "string" }, + "derivedDataPath": { "type": "string" }, + "testProductsPath": { "type": "string" }, + "xctestrunPath": { "type": "string" }, + "onlyTesting": { + "type": "array", + "items": { "type": "string" } + }, + "skipTesting": { + "type": "array", + "items": { "type": "string" } + } + } + } + }, + "properties": { + "schema": { + "const": "xcodebuildmcp.output.test-result" + }, + "schemaVersion": { + "const": "3" + }, + "didError": { + "type": "boolean" + }, + "error": { + "type": ["string", "null"] + }, + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "request": { + "$ref": "#/$defs/buildInvocationRequest" + }, + "summary": { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "enum": ["SUCCEEDED", "FAILED"] + }, + "durationMs": { + "type": "integer", + "minimum": 0 + }, + "target": { + "enum": ["simulator", "device", "macos", "swift-package"] + }, + "counts": { + "$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/counts" + } + }, + "required": ["status"] + }, + "artifacts": { + "type": "object", + "additionalProperties": false, + "properties": { + "deviceId": { "type": "string" }, + "buildLogPath": { "type": "string" }, + "packagePath": { "type": "string" }, + "xcresultPath": { "type": "string" }, + "testProductsPath": { "type": "string" }, + "xctestrunPath": { "type": "string" } + }, + "required": [], + "minProperties": 1 + }, + "diagnostics": { + "$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/testDiagnostics" + }, + "tests": { + "type": "object", + "additionalProperties": false, + "properties": { + "selected": { + "type": "array", + "items": { "type": "string" } + }, + "discovered": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { "type": "integer", "minimum": 0 }, + "items": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["total", "items"] + } + }, + "required": [] + }, + "testCases": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "suite": { "type": "string" }, + "test": { "type": "string" }, + "status": { "enum": ["passed", "failed", "skipped"] }, + "durationMs": { "type": "integer", "minimum": 0 } + }, + "required": ["test", "status"] + } + } + }, + "required": ["summary", "artifacts", "diagnostics"] + }, + "nextSteps": { + "$ref": "https://xcodebuildmcp.com/schemas/structured-output/_defs/common.schema.json#/$defs/nextSteps" + } + }, + "required": ["schema", "schemaVersion", "didError", "error", "data"] +} diff --git a/src/cli/commands/purge-ui.ts b/src/cli/commands/purge-ui.ts index 78bf51ecf..8feb7d742 100644 --- a/src/cli/commands/purge-ui.ts +++ b/src/cli/commands/purge-ui.ts @@ -14,6 +14,7 @@ const CLASS_LABELS: Record = { derivedData: 'DerivedData', logs: 'Logs', resultBundles: 'Result bundles', + testProducts: 'Test products', stateTransients: 'State transients', locks: 'Locks', }; @@ -345,5 +346,5 @@ export function executionToJson(result: PurgeStorageExecutionResult): object { } export function defaultClassKeys(): PurgeStorageDeletableClass[] { - return ['logs', 'resultBundles', 'stateTransients']; + return ['logs', 'resultBundles', 'testProducts', 'stateTransients']; } diff --git a/src/cli/commands/purge.ts b/src/cli/commands/purge.ts index e76cc75d8..99910aa39 100644 --- a/src/cli/commands/purge.ts +++ b/src/cli/commands/purge.ts @@ -348,7 +348,7 @@ export function registerPurgeCommand(app: Argv, opts: { currentWorkspaceKey: str .option('classes', { type: 'string', describe: - 'Comma-separated storage classes: derivedData,logs,resultBundles,stateTransients. "all" excludes DerivedData.', + 'Comma-separated storage classes: derivedData,logs,resultBundles,testProducts,stateTransients. "all" excludes DerivedData.', }) .option('older-than', { type: 'string', diff --git a/src/core/__tests__/structured-output-schema.test.ts b/src/core/__tests__/structured-output-schema.test.ts index e999df86f..a415ab3af 100644 --- a/src/core/__tests__/structured-output-schema.test.ts +++ b/src/core/__tests__/structured-output-schema.test.ts @@ -211,6 +211,54 @@ describe('structured output schema bundling', () => { ).toBe(true); }); + it('accepts prepared test artifacts only in build and test result v3', () => { + const ajv = new Ajv2020({ allErrors: true, strict: true, validateSchema: true }); + const buildV2 = ajv.compile( + getMcpOutputSchema({ schema: 'xcodebuildmcp.output.build-result', version: '2' }), + ); + const buildV3 = ajv.compile( + getMcpOutputSchema({ schema: 'xcodebuildmcp.output.build-result', version: '3' }), + ); + const testV3 = ajv.compile( + getMcpOutputSchema({ schema: 'xcodebuildmcp.output.test-result', version: '3' }), + ); + const buildEnvelope = { + schema: 'xcodebuildmcp.output.build-result', + schemaVersion: '3', + didError: false, + error: null, + data: { + request: { buildForTesting: true, scheme: 'CalculatorApp' }, + summary: { status: 'SUCCEEDED', target: 'simulator' }, + artifacts: { + testProductsPath: 'artifacts/CalculatorApp.xctestproducts', + xctestrunPaths: ['artifacts/CalculatorApp.xctestproducts/CalculatorApp.xctestrun'], + }, + diagnostics: { warnings: [], errors: [] }, + }, + }; + + expect(buildV2({ ...buildEnvelope, schemaVersion: '2' })).toBe(false); + expect(buildV3(buildEnvelope)).toBe(true); + expect( + testV3({ + schema: 'xcodebuildmcp.output.test-result', + schemaVersion: '3', + didError: false, + error: null, + data: { + request: { testProductsPath: 'artifacts/CalculatorApp.xctestproducts' }, + summary: { status: 'SUCCEEDED', target: 'simulator' }, + artifacts: { + testProductsPath: 'artifacts/CalculatorApp.xctestproducts', + xcresultPath: 'artifacts/test.xcresult', + }, + diagnostics: { warnings: [], errors: [], testFailures: [] }, + }, + }), + ).toBe(true); + }); + it('accepts video recording capture payloads in the bumped capture contract', () => { const schema = getMcpOutputSchema({ schema: 'xcodebuildmcp.output.capture-result', diff --git a/src/mcp/tools/device/__tests__/build_device.test.ts b/src/mcp/tools/device/__tests__/build_device.test.ts index 10b7a6a11..a59923fb8 100644 --- a/src/mcp/tools/device/__tests__/build_device.test.ts +++ b/src/mcp/tools/device/__tests__/build_device.test.ts @@ -49,7 +49,26 @@ describe('build_device plugin', () => { expect(schemaObj.safeParse({ platform: 'macOS' }).success).toBe(false); const schemaKeys = Object.keys(schema).sort(); - expect(schemaKeys).toEqual(['extraArgs', 'platform']); + expect(schemaKeys).toEqual([ + 'buildForTesting', + 'deviceId', + 'extraArgs', + 'platform', + 'testProductsPath', + ]); + }); + + it('should reject testProductsPath without buildForTesting', async () => { + const result = await callHandler(handler, { + projectPath: '/path/to/MyProject.xcodeproj', + scheme: 'MyScheme', + testProductsPath: '/tmp/MyApp.xctestproducts', + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain( + 'testProductsPath requires buildForTesting to be true', + ); }); }); @@ -358,5 +377,60 @@ describe('build_device plugin', () => { }), ); }); + + it('should prepare reusable test products for a selected device', async () => { + const spy = createSpyExecutor(); + const testProductsPath = '/tmp/MyApp Tests.xctestproducts'; + + const { result } = await runToolLogic(() => + buildDeviceLogic( + { + projectPath: '/path/to/MyProject.xcodeproj', + scheme: 'MyScheme', + platform: 'iOS', + deviceId: 'DEVICE-UDID', + buildForTesting: true, + testProductsPath, + }, + spy.executor, + ), + ); + + expect(spy.commandCalls).toHaveLength(1); + expect(spy.commandCalls[0].args).toContain('platform=iOS,id=DEVICE-UDID'); + expect(spy.commandCalls[0].args.slice(-3)).toEqual([ + '-testProductsPath', + testProductsPath, + 'build-for-testing', + ]); + expect(spy.commandCalls[0].logPrefix).toBe('iOS Device Build for Testing'); + expect(result.nextStepParams).toEqual({ + test_device: { + testProductsPath, + deviceId: 'DEVICE-UDID', + platform: 'iOS', + }, + }); + }); + + it('should not suggest running generic device test products without a device', async () => { + const spy = createSpyExecutor(); + const testProductsPath = '/tmp/MyApp Tests.xctestproducts'; + + const { result } = await runToolLogic(() => + buildDeviceLogic( + { + projectPath: '/path/to/MyProject.xcodeproj', + scheme: 'MyScheme', + buildForTesting: true, + testProductsPath, + }, + spy.executor, + ), + ); + + expect(spy.commandCalls[0].args).toContain('generic/platform=iOS'); + expect(result.nextStepParams).toBeUndefined(); + }); }); }); diff --git a/src/mcp/tools/device/__tests__/test_device.test.ts b/src/mcp/tools/device/__tests__/test_device.test.ts index f007bfd7e..9c7cb833f 100644 --- a/src/mcp/tools/device/__tests__/test_device.test.ts +++ b/src/mcp/tools/device/__tests__/test_device.test.ts @@ -71,7 +71,14 @@ describe('test_device plugin', () => { ); const schemaKeys = Object.keys(schema).sort(); - expect(schemaKeys).toEqual(['extraArgs', 'platform', 'progress', 'testRunnerEnv']); + expect(schemaKeys).toEqual([ + 'extraArgs', + 'platform', + 'progress', + 'testProductsPath', + 'testRunnerEnv', + 'xctestrunPath', + ]); }); it('should validate XOR between projectPath and workspacePath', async () => { @@ -112,7 +119,7 @@ describe('test_device plugin', () => { expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); - expect(result.content[0].text).toContain('Provide scheme and deviceId'); + expect(result.content[0].text).toContain('Provide deviceId'); }); it('should require project or workspace when defaults provide scheme and device', async () => { @@ -121,7 +128,7 @@ describe('test_device plugin', () => { const result = await callHandler(handler, {}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Provide a project or workspace'); + expect(result.content[0].text).toContain('Either projectPath or workspacePath is required'); }); it('should reject mutually exclusive project inputs when defaults satisfy requirements', async () => { @@ -155,7 +162,7 @@ describe('test_device plugin', () => { mockFs(), ); - expect(spy.commandCalls).toHaveLength(1); + expect(spy.commandCalls).toHaveLength(2); expect(spy.commandCalls[0].args).toEqual([ 'xcodebuild', '-project', @@ -171,9 +178,21 @@ describe('test_device plugin', () => { 'never', '-derivedDataPath', computeScopedDerivedDataPath('/path/to/project.xcodeproj'), + '-testProductsPath', + expect.stringContaining('/test-products/test_device_'), + 'build-for-testing', + ]); + expect(spy.commandCalls[1].args).toEqual([ + 'xcodebuild', + '-testProductsPath', + expect.stringContaining('/test-products/test_device_'), + '-destination', + 'platform=iOS,id=test-device-123', + '-collect-test-diagnostics', + 'never', '-resultBundlePath', expect.stringContaining('/result-bundles/test_device_'), - 'test', + 'test-without-building', ]); }); }); diff --git a/src/mcp/tools/device/build_device.ts b/src/mcp/tools/device/build_device.ts index e3921d244..536f6f3e0 100644 --- a/src/mcp/tools/device/build_device.ts +++ b/src/mcp/tools/device/build_device.ts @@ -27,11 +27,54 @@ import { setXcodebuildStructuredOutput, } from '../../../utils/xcodebuild-domain-results.ts'; import type { BuildInvocationRequest } from '../../../types/domain-fragments.ts'; +import { displayPath } from '../../../utils/build-preflight.ts'; import { resolveEffectiveDerivedDataPath } from '../../../utils/derived-data-path.ts'; +import { resolvePathFromCwd } from '../../../utils/path.ts'; +import { + createDefaultTestProductsPath, + findXctestrunPaths, + markTestProductsPathCompleted, +} from '../../../utils/test-products-path.ts'; import { createBuildInvocationFragment } from '../../../utils/xcodebuild-pipeline.ts'; -function createBuildDeviceRequest(params: BuildDeviceParams): BuildInvocationRequest { +interface PreparedBuildDeviceExecution { + buildAction: 'build' | 'build-for-testing'; + invocationRequest: BuildInvocationRequest; + isManagedTestProductsPath: boolean; + logLabel: 'Build' | 'Build for Testing'; + sharedBuildParams: BuildDeviceParams; + testProductsPath?: string; +} + +function prepareBuildDeviceExecution(params: BuildDeviceParams): PreparedBuildDeviceExecution { + const buildForTesting = params.buildForTesting ?? false; + const isManagedTestProductsPath = buildForTesting && params.testProductsPath === undefined; + const testProductsPath = buildForTesting + ? (resolvePathFromCwd(params.testProductsPath) ?? createDefaultTestProductsPath('build_device')) + : undefined; + const sharedBuildParams = testProductsPath + ? { + ...params, + extraArgs: [...(params.extraArgs ?? []), '-testProductsPath', testProductsPath], + } + : params; + return { + buildAction: buildForTesting ? 'build-for-testing' : 'build', + invocationRequest: createBuildDeviceRequest(params, testProductsPath), + isManagedTestProductsPath, + logLabel: buildForTesting ? 'Build for Testing' : 'Build', + sharedBuildParams, + testProductsPath, + }; +} + +function createBuildDeviceRequest( + params: BuildDeviceParams, + testProductsPath?: string, +): BuildInvocationRequest { + return { + ...(params.buildForTesting ? { buildForTesting: true } : {}), scheme: params.scheme, workspacePath: params.workspacePath, projectPath: params.projectPath, @@ -39,6 +82,8 @@ function createBuildDeviceRequest(params: BuildDeviceParams): BuildInvocationReq configuration: params.configuration, platform: String(mapDevicePlatform(params.platform)), target: 'device', + ...(params.buildForTesting && params.deviceId ? { deviceId: params.deviceId } : {}), + ...(testProductsPath ? { testProductsPath: displayPath(testProductsPath) } : {}), }; } @@ -51,11 +96,23 @@ const baseSchemaObject = z.object({ derivedDataPath: z.string().optional(), extraArgs: z.array(z.string()).optional(), preferXcodebuild: z.boolean().optional(), + deviceId: z.string().optional().describe('UDID of the destination device'), + buildForTesting: z + .boolean() + .optional() + .describe('Build reusable test products without running tests (default: false)'), + testProductsPath: z + .string() + .optional() + .describe('Output path for the .xctestproducts bundle when buildForTesting is true'), }); const buildDeviceSchema = z.preprocess( nullifyEmptyStrings, - withProjectOrWorkspace(baseSchemaObject), + withProjectOrWorkspace(baseSchemaObject).refine( + (params) => params.testProductsPath === undefined || params.buildForTesting === true, + { message: 'testProductsPath requires buildForTesting to be true' }, + ), ); export type BuildDeviceParams = z.infer; @@ -72,37 +129,50 @@ const publicSchemaObject = baseSchemaObject.omit({ export function createBuildDeviceExecutor( executor: CommandExecutor, + prepared?: PreparedBuildDeviceExecution, ): StreamingExecutor { return async (params, ctx) => { + const resolved = prepared ?? prepareBuildDeviceExecution(params); const platform = mapDevicePlatform(params.platform); - const processedParams = { - ...params, - configuration: params.configuration, - }; const started = createDomainStreamingPipeline('build_device', 'BUILD', ctx, 'build-result'); const buildResult = await executeXcodeBuildCommand( - processedParams, + resolved.sharedBuildParams, { platform, - logPrefix: `${platform} Device Build`, + logPrefix: `${platform} Device ${resolved.logLabel}`, + deviceId: params.buildForTesting ? params.deviceId : undefined, }, params.preferXcodebuild ?? false, - 'build', + resolved.buildAction, executor, undefined, started.pipeline, ); + const succeeded = !buildResult.isError; + + if (resolved.isManagedTestProductsPath) { + markTestProductsPathCompleted(resolved.testProductsPath); + } + + const xctestrunPaths = + succeeded && resolved.testProductsPath + ? await findXctestrunPaths(resolved.testProductsPath) + : []; return createBuildDomainResult({ started, - succeeded: !buildResult.isError, + succeeded, target: 'device', artifacts: { - buildLogPath: started.pipeline.logPath, + buildLogPath: displayPath(started.pipeline.logPath), + ...(succeeded && resolved.testProductsPath + ? { testProductsPath: displayPath(resolved.testProductsPath) } + : {}), + ...(xctestrunPaths.length > 0 ? { xctestrunPaths: xctestrunPaths.map(displayPath) } : {}), }, fallbackErrorMessages: collectFallbackErrorMessages(started, [], buildResult.content), - request: createBuildDeviceRequest(params), + request: resolved.invocationRequest, }); }; } @@ -112,18 +182,26 @@ export async function buildDeviceLogic( executor: CommandExecutor, ): Promise { const ctx = getHandlerContext(); - const invocationRequest = createBuildDeviceRequest(params); + const prepared = prepareBuildDeviceExecution(params); - ctx.emit(createBuildInvocationFragment('build-result', 'BUILD', invocationRequest)); + ctx.emit(createBuildInvocationFragment('build-result', 'BUILD', prepared.invocationRequest)); const executionContext = createStreamingExecutionContext(ctx); - const executeBuildDevice = createBuildDeviceExecutor(executor); + const executeBuildDevice = createBuildDeviceExecutor(executor, prepared); const result = await executeBuildDevice(params, executionContext); - setXcodebuildStructuredOutput(ctx, 'build-result', result); + setXcodebuildStructuredOutput(ctx, 'build-result', result, '3'); if (!result.didError) { - ctx.nextStepParams = { - get_device_app_path: { + if (prepared.testProductsPath && params.deviceId) { + const nextParams = { + testProductsPath: displayPath(prepared.testProductsPath), + deviceId: params.deviceId, + ...(params.platform ? { platform: String(mapDevicePlatform(params.platform)) } : {}), + }; + ctx.nextStepParams = { test_device: nextParams }; + ctx.nextSteps = [{ tool: 'test_device', label: 'Run prepared tests', params: nextParams }]; + } else if (!prepared.testProductsPath) { + const nextParams = { scheme: params.scheme, ...(params.derivedDataPath !== undefined ? { derivedDataPath: params.derivedDataPath } @@ -131,8 +209,17 @@ export async function buildDeviceLogic( ...(params.platform !== undefined ? { platform: String(mapDevicePlatform(params.platform)) } : {}), - }, - }; + }; + ctx.nextStepParams = { get_device_app_path: nextParams }; + ctx.nextSteps = [ + { + tool: 'get_device_app_path', + label: 'Get built device app path', + params: nextParams, + priority: 1, + }, + ]; + } } } diff --git a/src/mcp/tools/device/test_device.ts b/src/mcp/tools/device/test_device.ts index c5582a378..eb948ea55 100644 --- a/src/mcp/tools/device/test_device.ts +++ b/src/mcp/tools/device/test_device.ts @@ -21,7 +21,12 @@ import { getSessionAwareToolSchemaShape, toInternalSchema, } from '../../../utils/typed-tool-factory.ts'; -import { nullifyEmptyStrings, withProjectOrWorkspace } from '../../../utils/schema-helpers.ts'; +import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts'; +import { + hasPreparedTestSource, + TEST_SOURCE_EXCLUSIVE_GROUPS, + withProjectWorkspaceOrTestArtifact, +} from '../../../utils/test-source.ts'; import { resolveTestPreflight, type TestPreflightResult } from '../../../utils/test-preflight.ts'; import { getHandlerContext } from '../../../utils/typed-tool-factory.ts'; import { @@ -31,11 +36,20 @@ import { import type { BuildInvocationRequest } from '../../../types/domain-fragments.ts'; import { resolveEffectiveDerivedDataPath } from '../../../utils/derived-data-path.ts'; import { createBuildInvocationFragment } from '../../../utils/xcodebuild-pipeline.ts'; +import { displayPath } from '../../../utils/build-preflight.ts'; const baseSchemaObject = z.object({ projectPath: z.string().optional().describe('Path to the .xcodeproj file'), workspacePath: z.string().optional().describe('Path to the .xcworkspace file'), - scheme: z.string().describe('The scheme to test'), + scheme: z.string().optional().describe('The scheme to test in source mode'), + testProductsPath: z + .string() + .optional() + .describe('Path to a prepared .xctestproducts package. Cannot be combined with source inputs'), + xctestrunPath: z + .string() + .optional() + .describe('Path to a prepared .xctestrun file. Cannot be combined with source inputs'), deviceId: z.string().describe('UDID of the device (obtained from list_devices)'), configuration: z.string().optional().describe('Build configuration (Debug, Release)'), derivedDataPath: z.string().optional(), @@ -56,7 +70,7 @@ const baseSchemaObject = z.object({ const testDeviceSchema = z.preprocess( nullifyEmptyStrings, - withProjectOrWorkspace(baseSchemaObject), + withProjectWorkspaceOrTestArtifact(baseSchemaObject), ); export type TestDeviceParams = z.infer; @@ -83,19 +97,22 @@ async function prepareTestDeviceExecution( params: TestDeviceParams, fileSystemExecutor: FileSystemExecutor, ): Promise { - const configuration = params.configuration; + const preparedTestSource = hasPreparedTestSource(params); + const configuration = preparedTestSource ? undefined : params.configuration; const platform = mapDevicePlatform(params.platform); - const preflight = await resolveTestPreflight( - { - projectPath: params.projectPath, - workspacePath: params.workspacePath, - scheme: params.scheme, - configuration, - extraArgs: params.extraArgs, - destinationName: params.deviceId, - }, - fileSystemExecutor, - ); + const preflight = preparedTestSource + ? null + : await resolveTestPreflight( + { + projectPath: params.projectPath, + workspacePath: params.workspacePath, + scheme: params.scheme!, + configuration, + extraArgs: params.extraArgs, + destinationName: params.deviceId, + }, + fileSystemExecutor, + ); return { configuration, @@ -105,11 +122,13 @@ async function prepareTestDeviceExecution( scheme: params.scheme, workspacePath: params.workspacePath, projectPath: params.projectPath, - derivedDataPath: resolveEffectiveDerivedDataPath(params), + derivedDataPath: preparedTestSource ? undefined : resolveEffectiveDerivedDataPath(params), configuration, platform: String(platform), deviceId: params.deviceId, target: 'device' as const, + testProductsPath: params.testProductsPath ? displayPath(params.testProductsPath) : undefined, + xctestrunPath: params.xctestrunPath ? displayPath(params.xctestrunPath) : undefined, onlyTesting: preflight?.selectors.onlyTesting.map((selector) => selector.raw), skipTesting: preflight?.selectors.skipTesting.map((selector) => selector.raw), } satisfies BuildInvocationRequest, @@ -144,6 +163,8 @@ export function createTestDeviceExecutor( useLatestOS: false, testRunnerEnv: params.testRunnerEnv, progress: params.progress, + testProductsPath: params.testProductsPath, + xctestrunPath: params.xctestrunPath, }, ctx, ); @@ -163,7 +184,7 @@ export async function testDeviceLogic( const executeTestDevice = createTestDeviceExecutor(executor, fileSystemExecutor, prepared); const result = await executeTestDevice(params, executionContext); - setXcodebuildStructuredOutput(ctx, 'test-result', result); + setXcodebuildStructuredOutput(ctx, 'test-result', result, '3'); } export const schema = getSessionAwareToolSchemaShape({ @@ -176,9 +197,6 @@ export const handler = createSessionAwareTool({ logicFunction: (params, executor) => testDeviceLogic(params, executor, getDefaultFileSystemExecutor()), getExecutor: getDefaultCommandExecutor, - requirements: [ - { allOf: ['scheme', 'deviceId'], message: 'Provide scheme and deviceId' }, - { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' }, - ], - exclusivePairs: [['projectPath', 'workspacePath']], + requirements: [{ allOf: ['deviceId'], message: 'Provide deviceId' }], + exclusivePairs: [...TEST_SOURCE_EXCLUSIVE_GROUPS, ['projectPath', 'workspacePath']], }); diff --git a/src/mcp/tools/macos/__tests__/build_macos.test.ts b/src/mcp/tools/macos/__tests__/build_macos.test.ts index ef24f5509..5380c2d20 100644 --- a/src/mcp/tools/macos/__tests__/build_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/build_macos.test.ts @@ -51,7 +51,20 @@ describe('build_macos plugin', () => { expect(zodSchema.safeParse({ preferXcodebuild: true }).success).toBe(false); const schemaKeys = Object.keys(schema).sort(); - expect(schemaKeys).toEqual(['extraArgs']); + expect(schemaKeys).toEqual(['buildForTesting', 'extraArgs', 'testProductsPath']); + }); + + it('should reject testProductsPath without buildForTesting', async () => { + const result = await callHandler(handler, { + projectPath: '/path/to/MyProject.xcodeproj', + scheme: 'MyScheme', + testProductsPath: '/tmp/MyApp.xctestproducts', + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain( + 'testProductsPath requires buildForTesting to be true', + ); }); }); @@ -367,6 +380,34 @@ describe('build_macos plugin', () => { 'build', ]); }); + + it('should prepare reusable macOS test products without app inspection', async () => { + const calls: string[][] = []; + const testProductsPath = '/tmp/MyApp Tests.xctestproducts'; + const executor = createMockExecutor({ + success: true, + output: 'BUILD SUCCEEDED', + onExecute: (command) => calls.push(command), + }); + + const { result } = await runBuildMacOS( + { + projectPath: '/path/to/project.xcodeproj', + scheme: 'MyScheme', + buildForTesting: true, + testProductsPath, + }, + executor, + ); + + expect(calls).toHaveLength(1); + expect(calls[0].slice(-3)).toEqual([ + '-testProductsPath', + testProductsPath, + 'build-for-testing', + ]); + expect(result.nextStepParams).toEqual({ test_macos: { testProductsPath } }); + }); }); describe('XOR Validation', () => { diff --git a/src/mcp/tools/macos/__tests__/test_macos.test.ts b/src/mcp/tools/macos/__tests__/test_macos.test.ts index 84e95e950..8c79fdca3 100644 --- a/src/mcp/tools/macos/__tests__/test_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/test_macos.test.ts @@ -54,7 +54,9 @@ describe('test_macos plugin (unified)', () => { expect(zodSchema.safeParse({ testRunnerEnv: { FOO: 123 } }).success).toBe(false); const schemaKeys = Object.keys(schema).sort(); - expect(schemaKeys).toEqual(['extraArgs', 'progress', 'testRunnerEnv'].sort()); + expect(schemaKeys).toEqual( + ['extraArgs', 'progress', 'testProductsPath', 'testRunnerEnv', 'xctestrunPath'].sort(), + ); }); }); @@ -72,7 +74,7 @@ describe('test_macos plugin (unified)', () => { const result = await callHandler(handler, {}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Provide a project or workspace'); + expect(result.content[0].text).toContain('Either projectPath or workspacePath is required'); }); it('should reject when both projectPath and workspacePath provided explicitly', async () => { @@ -95,7 +97,7 @@ describe('test_macos plugin (unified)', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Provide a project or workspace'); + expect(result.content[0].text).toContain('Either projectPath or workspacePath is required'); }); it('should validate that both projectPath and workspacePath cannot be provided', async () => { @@ -251,16 +253,16 @@ describe('test_macos plugin (unified)', () => { }); it('should return pending response on successful test', async () => { - const commandCalls: { command: string[]; logPrefix?: string }[] = []; + const commandCalls: { command: string[]; logPrefix?: string; cwd?: string }[] = []; const mockExecutor = async ( command: string[], logPrefix?: string, _useShell?: boolean, - _opts?: { env?: Record }, + opts?: { env?: Record; cwd?: string }, _detached?: boolean, ) => { - commandCalls.push({ command, logPrefix }); + commandCalls.push({ command, logPrefix, cwd: opts?.cwd }); return createMockCommandResponse({ success: true, output: 'Test Succeeded', @@ -278,14 +280,19 @@ describe('test_macos plugin (unified)', () => { mockFs(), ); - expect(commandCalls).toHaveLength(1); + expect(commandCalls).toHaveLength(2); expect(commandCalls[0].command).toContain('xcodebuild'); expect(commandCalls[0].command).toContain('-workspace'); expect(commandCalls[0].command).toContain('/path/to/MyProject.xcworkspace'); expect(commandCalls[0].command).toContain('-scheme'); expect(commandCalls[0].command).toContain('MyScheme'); - expect(commandCalls[0].command).toContain('test'); + expect(commandCalls[0].command).toContain('build-for-testing'); expect(commandCalls[0].logPrefix).toBe('Test Run'); + expect(commandCalls[1].command).toContain('-testProductsPath'); + expect(commandCalls[1].command).not.toContain('-workspace'); + expect(commandCalls[1].command).not.toContain('-scheme'); + expect(commandCalls[1].command).toContain('test-without-building'); + expect(commandCalls.map((call) => call.cwd)).toEqual(['/path/to', '/path/to']); expectPendingBuildResponse(result); expect(result.isError()).toBeFalsy(); @@ -383,23 +390,23 @@ describe('test_macos plugin (unified)', () => { expect(result.isError()).toBe(true); }); - it('should return error response when executor throws an exception', async () => { + it('should propagate executor exceptions as runtime errors', async () => { const mockExecutor = createMockExecutor({ success: false, error: '', shouldThrow: new Error('Network error'), }); - const { result } = await runTestMacosLogic( - { - workspacePath: '/path/to/MyProject.xcworkspace', - scheme: 'MyScheme', - }, - mockExecutor, - mockFs(), - ); - - expect(result.isError()).toBe(true); + await expect( + runTestMacosLogic( + { + workspacePath: '/path/to/MyProject.xcworkspace', + scheme: 'MyScheme', + }, + mockExecutor, + mockFs(), + ), + ).rejects.toThrow('Network error'); }); }); }); diff --git a/src/mcp/tools/macos/build_macos.ts b/src/mcp/tools/macos/build_macos.ts index cadf3382d..b66e15f4e 100644 --- a/src/mcp/tools/macos/build_macos.ts +++ b/src/mcp/tools/macos/build_macos.ts @@ -22,11 +22,54 @@ import { setXcodebuildStructuredOutput, } from '../../../utils/xcodebuild-domain-results.ts'; import type { BuildInvocationRequest } from '../../../types/domain-fragments.ts'; +import { displayPath } from '../../../utils/build-preflight.ts'; import { resolveEffectiveDerivedDataPath } from '../../../utils/derived-data-path.ts'; +import { resolvePathFromCwd } from '../../../utils/path.ts'; +import { + createDefaultTestProductsPath, + findXctestrunPaths, + markTestProductsPathCompleted, +} from '../../../utils/test-products-path.ts'; import { createBuildInvocationFragment } from '../../../utils/xcodebuild-pipeline.ts'; -function createBuildMacOSRequest(params: BuildMacOSParams): BuildInvocationRequest { +interface PreparedBuildMacOSExecution { + buildAction: 'build' | 'build-for-testing'; + invocationRequest: BuildInvocationRequest; + isManagedTestProductsPath: boolean; + logLabel: 'Build' | 'Build for Testing'; + sharedBuildParams: BuildMacOSParams; + testProductsPath?: string; +} + +function prepareBuildMacOSExecution(params: BuildMacOSParams): PreparedBuildMacOSExecution { + const buildForTesting = params.buildForTesting ?? false; + const isManagedTestProductsPath = buildForTesting && params.testProductsPath === undefined; + const testProductsPath = buildForTesting + ? (resolvePathFromCwd(params.testProductsPath) ?? createDefaultTestProductsPath('build_macos')) + : undefined; + const sharedBuildParams = testProductsPath + ? { + ...params, + extraArgs: [...(params.extraArgs ?? []), '-testProductsPath', testProductsPath], + } + : params; + + return { + buildAction: buildForTesting ? 'build-for-testing' : 'build', + invocationRequest: createBuildMacOSRequest(params, testProductsPath), + isManagedTestProductsPath, + logLabel: buildForTesting ? 'Build for Testing' : 'Build', + sharedBuildParams, + testProductsPath, + }; +} + +function createBuildMacOSRequest( + params: BuildMacOSParams, + testProductsPath?: string, +): BuildInvocationRequest { return { + ...(params.buildForTesting ? { buildForTesting: true } : {}), scheme: params.scheme, workspacePath: params.workspacePath, projectPath: params.projectPath, @@ -35,6 +78,7 @@ function createBuildMacOSRequest(params: BuildMacOSParams): BuildInvocationReque platform: 'macOS', arch: params.arch, target: 'macos', + ...(testProductsPath ? { testProductsPath: displayPath(testProductsPath) } : {}), }; } @@ -50,6 +94,14 @@ const baseSchemaObject = z.object({ .describe('Architecture to build for (arm64 or x86_64). For macOS only.'), extraArgs: z.array(z.string()).optional(), preferXcodebuild: z.boolean().optional(), + buildForTesting: z + .boolean() + .optional() + .describe('Build reusable test products without running tests (default: false)'), + testProductsPath: z + .string() + .optional() + .describe('Output path for the .xctestproducts bundle when buildForTesting is true'), }); const publicSchemaObject = baseSchemaObject.omit({ @@ -64,7 +116,10 @@ const publicSchemaObject = baseSchemaObject.omit({ const buildMacOSSchema = z.preprocess( nullifyEmptyStrings, - withProjectOrWorkspace(baseSchemaObject), + withProjectOrWorkspace(baseSchemaObject).refine( + (params) => params.testProductsPath === undefined || params.buildForTesting === true, + { message: 'testProductsPath requires buildForTesting to be true' }, + ), ); export type BuildMacOSParams = z.infer; @@ -72,26 +127,28 @@ type BuildMacOSResult = BuildResultDomainResult; export function createBuildMacOSExecutor( executor: CommandExecutor, + prepared?: PreparedBuildMacOSExecution, ): StreamingExecutor { return async (params, ctx) => { + const resolved = prepared ?? prepareBuildMacOSExecution(params); const configuration = params.configuration; const started = createDomainStreamingPipeline('build_macos', 'BUILD', ctx, 'build-result'); const buildResult = await executeXcodeBuildCommand( - { ...params, configuration }, + { ...resolved.sharedBuildParams, configuration }, { platform: XcodePlatform.macOS, arch: params.arch, - logPrefix: 'macOS Build', + logPrefix: `macOS ${resolved.logLabel}`, }, params.preferXcodebuild ?? false, - 'build', + resolved.buildAction, executor, undefined, started.pipeline, ); let bundleId: string | undefined; - if (!buildResult.isError) { + if (!buildResult.isError && !params.buildForTesting) { try { const appPath = await resolveAppPathFromBuildSettings( { @@ -118,17 +175,31 @@ export function createBuildMacOSExecutor( // bundle ID is informational only } } + const succeeded = !buildResult.isError; + + if (resolved.isManagedTestProductsPath) { + markTestProductsPathCompleted(resolved.testProductsPath); + } + + const xctestrunPaths = + succeeded && resolved.testProductsPath + ? await findXctestrunPaths(resolved.testProductsPath) + : []; return createBuildDomainResult({ started, - succeeded: !buildResult.isError, + succeeded, target: 'macos', artifacts: { ...(bundleId ? { bundleId } : {}), - buildLogPath: started.pipeline.logPath, + buildLogPath: displayPath(started.pipeline.logPath), + ...(succeeded && resolved.testProductsPath + ? { testProductsPath: displayPath(resolved.testProductsPath) } + : {}), + ...(xctestrunPaths.length > 0 ? { xctestrunPaths: xctestrunPaths.map(displayPath) } : {}), }, fallbackErrorMessages: collectFallbackErrorMessages(started, [], buildResult.content), - request: createBuildMacOSRequest(params), + request: resolved.invocationRequest, }); }; } @@ -138,26 +209,39 @@ export async function buildMacOSLogic( executor: CommandExecutor, ): Promise { const ctx = getHandlerContext(); - const invocationRequest = createBuildMacOSRequest(params); + const prepared = prepareBuildMacOSExecution(params); log('info', `Starting macOS build for scheme ${params.scheme}`); - ctx.emit(createBuildInvocationFragment('build-result', 'BUILD', invocationRequest)); + ctx.emit(createBuildInvocationFragment('build-result', 'BUILD', prepared.invocationRequest)); const executionContext = createStreamingExecutionContext(ctx); - const executeBuildMacOS = createBuildMacOSExecutor(executor); + const executeBuildMacOS = createBuildMacOSExecutor(executor, prepared); const result = await executeBuildMacOS(params, executionContext); - setXcodebuildStructuredOutput(ctx, 'build-result', result); + setXcodebuildStructuredOutput(ctx, 'build-result', result, '3'); if (!result.didError) { - ctx.nextStepParams = { - get_mac_app_path: { + if (prepared.testProductsPath) { + const nextParams = { testProductsPath: displayPath(prepared.testProductsPath) }; + ctx.nextStepParams = { test_macos: nextParams }; + ctx.nextSteps = [{ tool: 'test_macos', label: 'Run prepared tests', params: nextParams }]; + } else { + const nextParams = { scheme: params.scheme, ...(params.derivedDataPath !== undefined ? { derivedDataPath: params.derivedDataPath } : {}), - }, - }; + }; + ctx.nextStepParams = { get_mac_app_path: nextParams }; + ctx.nextSteps = [ + { + tool: 'get_mac_app_path', + label: 'Get built macOS app path', + params: nextParams, + priority: 1, + }, + ]; + } } } diff --git a/src/mcp/tools/macos/test_macos.ts b/src/mcp/tools/macos/test_macos.ts index a0bf1117f..94e6448de 100644 --- a/src/mcp/tools/macos/test_macos.ts +++ b/src/mcp/tools/macos/test_macos.ts @@ -20,7 +20,12 @@ import { getSessionAwareToolSchemaShape, toInternalSchema, } from '../../../utils/typed-tool-factory.ts'; -import { nullifyEmptyStrings, withProjectOrWorkspace } from '../../../utils/schema-helpers.ts'; +import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts'; +import { + hasPreparedTestSource, + TEST_SOURCE_EXCLUSIVE_GROUPS, + withProjectWorkspaceOrTestArtifact, +} from '../../../utils/test-source.ts'; import { resolveTestPreflight, type TestPreflightResult } from '../../../utils/test-preflight.ts'; import { getHandlerContext } from '../../../utils/typed-tool-factory.ts'; import { @@ -30,11 +35,20 @@ import { import type { BuildInvocationRequest } from '../../../types/domain-fragments.ts'; import { resolveEffectiveDerivedDataPath } from '../../../utils/derived-data-path.ts'; import { createBuildInvocationFragment } from '../../../utils/xcodebuild-pipeline.ts'; +import { displayPath } from '../../../utils/build-preflight.ts'; const baseSchemaObject = z.object({ projectPath: z.string().optional().describe('Path to the .xcodeproj file'), workspacePath: z.string().optional().describe('Path to the .xcworkspace file'), - scheme: z.string().describe('The scheme to use'), + scheme: z.string().optional().describe('The scheme to use in source mode'), + testProductsPath: z + .string() + .optional() + .describe('Path to a prepared .xctestproducts package. Cannot be combined with source inputs'), + xctestrunPath: z + .string() + .optional() + .describe('Path to a prepared .xctestrun file. Cannot be combined with source inputs'), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), derivedDataPath: z.string().optional(), extraArgs: z.array(z.string()).optional(), @@ -60,7 +74,10 @@ const publicSchemaObject = baseSchemaObject.omit({ preferXcodebuild: true, } as const); -const testMacosSchema = z.preprocess(nullifyEmptyStrings, withProjectOrWorkspace(baseSchemaObject)); +const testMacosSchema = z.preprocess( + nullifyEmptyStrings, + withProjectWorkspaceOrTestArtifact(baseSchemaObject), +); export type TestMacosParams = z.infer; type TestMacosResult = TestResultDomainResult; @@ -75,18 +92,21 @@ async function prepareTestMacosExecution( params: TestMacosParams, fileSystemExecutor: FileSystemExecutor, ): Promise { - const configuration = params.configuration; - const preflight = await resolveTestPreflight( - { - projectPath: params.projectPath, - workspacePath: params.workspacePath, - scheme: params.scheme, - configuration, - extraArgs: params.extraArgs, - destinationName: 'macOS', - }, - fileSystemExecutor, - ); + const preparedTestSource = hasPreparedTestSource(params); + const configuration = preparedTestSource ? undefined : params.configuration; + const preflight = preparedTestSource + ? null + : await resolveTestPreflight( + { + projectPath: params.projectPath, + workspacePath: params.workspacePath, + scheme: params.scheme!, + configuration, + extraArgs: params.extraArgs, + destinationName: 'macOS', + }, + fileSystemExecutor, + ); return { configuration, @@ -95,9 +115,11 @@ async function prepareTestMacosExecution( scheme: params.scheme, workspacePath: params.workspacePath, projectPath: params.projectPath, - derivedDataPath: resolveEffectiveDerivedDataPath(params), + derivedDataPath: preparedTestSource ? undefined : resolveEffectiveDerivedDataPath(params), configuration, platform: 'macOS', + testProductsPath: params.testProductsPath ? displayPath(params.testProductsPath) : undefined, + xctestrunPath: params.xctestrunPath ? displayPath(params.xctestrunPath) : undefined, onlyTesting: preflight?.selectors.onlyTesting.map((selector) => selector.raw), skipTesting: preflight?.selectors.skipTesting.map((selector) => selector.raw), }, @@ -130,6 +152,8 @@ export function createTestMacOSExecutor( platform: XcodePlatform.macOS, testRunnerEnv: params.testRunnerEnv, progress: params.progress, + testProductsPath: params.testProductsPath, + xctestrunPath: params.xctestrunPath, }, ctx, ); @@ -149,7 +173,7 @@ export async function testMacosLogic( const executeTestMacOS = createTestMacOSExecutor(executor, fileSystemExecutor, prepared); const result = await executeTestMacOS(params, executionContext); - setXcodebuildStructuredOutput(ctx, 'test-result', result); + setXcodebuildStructuredOutput(ctx, 'test-result', result, '3'); } export const schema = getSessionAwareToolSchemaShape({ @@ -162,9 +186,5 @@ export const handler = createSessionAwareTool({ logicFunction: (params, executor) => testMacosLogic(params, executor, getDefaultFileSystemExecutor()), getExecutor: getDefaultCommandExecutor, - requirements: [ - { allOf: ['scheme'], message: 'scheme is required' }, - { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' }, - ], - exclusivePairs: [['projectPath', 'workspacePath']], + exclusivePairs: [...TEST_SOURCE_EXCLUSIVE_GROUPS, ['projectPath', 'workspacePath']], }); diff --git a/src/mcp/tools/simulator/__tests__/build_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_sim.test.ts index 38f4f927f..6b0df7560 100644 --- a/src/mcp/tools/simulator/__tests__/build_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_sim.test.ts @@ -12,7 +12,7 @@ import { } from '../../../../test-utils/test-helpers.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; -import { schema, handler, build_simLogic } from '../build_sim.ts'; +import { schema, handler, build_simLogic, buildSimulatorSchema } from '../build_sim.ts'; const runBuildSimLogic = ( params: Parameters[0], @@ -43,6 +43,23 @@ describe('build_sim tool', () => { expect(schemaObj.safeParse({ derivedDataPath: '/path/to/derived' }).success).toBe(false); expect(schemaObj.safeParse({ extraArgs: [123] }).success).toBe(false); expect(schemaObj.safeParse({ preferXcodebuild: false }).success).toBe(false); + expect( + schemaObj.safeParse({ + buildForTesting: true, + testProductsPath: '/tmp/MyApp.xctestproducts', + }).success, + ).toBe(true); + }); + + it('should reject testProductsPath without buildForTesting', () => { + const result = buildSimulatorSchema.safeParse({ + projectPath: '/path/to/MyProject.xcodeproj', + scheme: 'MyScheme', + simulatorName: 'iPhone 17', + testProductsPath: '/tmp/MyApp.xctestproducts', + }); + + expect(result.success).toBe(false); }); }); @@ -392,6 +409,31 @@ describe('build_sim tool', () => { 'watchOS Simulator Build', ); }); + + it('should prepare reusable simulator test products', async () => { + const callHistory: Array<{ command: string[]; logPrefix?: string }> = []; + const testProductsPath = '/tmp/MyApp Tests.xctestproducts'; + + const { result } = await runBuildSimLogic( + { + projectPath: '/path/to/MyProject.xcodeproj', + scheme: 'MyScheme', + simulatorName: 'iPhone 17', + buildForTesting: true, + testProductsPath, + }, + createTrackingExecutor(callHistory), + ); + + expect(callHistory).toHaveLength(2); + expect(callHistory[1].command.slice(-3)).toEqual([ + '-testProductsPath', + testProductsPath, + 'build-for-testing', + ]); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build for Testing'); + expect(result.nextStepParams).toBeUndefined(); + }); }); describe('Response Processing', () => { @@ -528,6 +570,26 @@ describe('build_sim tool', () => { expect(result.isError()).toBeFalsy(); expectPendingBuildResponse(result, 'get_sim_app_path'); }); + + it('should suggest running prepared simulator tests after success', async () => { + const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' }); + const testProductsPath = '/tmp/MyApp Tests.xctestproducts'; + + const { result } = await runBuildSimLogic( + { + workspacePath: '/path/to/workspace', + scheme: 'MyScheme', + simulatorName: 'iPhone 17', + buildForTesting: true, + testProductsPath, + }, + mockExecutor, + ); + + expect(result.nextStepParams).toEqual({ + test_sim: { testProductsPath, simulatorName: 'iPhone 17' }, + }); + }); }); describe('Error Handling', () => { diff --git a/src/mcp/tools/simulator/__tests__/test_sim.test.ts b/src/mcp/tools/simulator/__tests__/test_sim.test.ts index ce3d6abe9..ca2a5b775 100644 --- a/src/mcp/tools/simulator/__tests__/test_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/test_sim.test.ts @@ -35,25 +35,27 @@ describe('test_sim tool', () => { expect(schemaObj.safeParse({ testRunnerEnv: { FOO: 123 } }).success).toBe(false); const schemaKeys = Object.keys(schema).sort(); - expect(schemaKeys).toEqual(['extraArgs', 'progress', 'testRunnerEnv'].sort()); + expect(schemaKeys).toEqual( + ['extraArgs', 'progress', 'testProductsPath', 'testRunnerEnv', 'xctestrunPath'].sort(), + ); }); }); describe('Handler Requirements', () => { - it('should require scheme when not provided', async () => { + it('should require a simulator destination before source validation', async () => { const result = await callHandler(handler, {}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('scheme is required'); + expect(result.content[0].text).toContain('Provide simulatorId or simulatorName'); }); it('should require project or workspace when scheme default exists', async () => { sessionStore.setDefaults({ scheme: 'MyScheme' }); - const result = await callHandler(handler, {}); + const result = await callHandler(handler, { simulatorId: 'SIM-UUID' }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Provide a project or workspace'); + expect(result.content[0].text).toContain('Either projectPath or workspacePath is required'); }); it('should require simulator identifier when scheme and project defaults exist', async () => { diff --git a/src/mcp/tools/simulator/build_sim.ts b/src/mcp/tools/simulator/build_sim.ts index e28136fe7..ea73921ea 100644 --- a/src/mcp/tools/simulator/build_sim.ts +++ b/src/mcp/tools/simulator/build_sim.ts @@ -33,7 +33,14 @@ import { setXcodebuildStructuredOutput, } from '../../../utils/xcodebuild-domain-results.ts'; import type { BuildInvocationRequest } from '../../../types/domain-fragments.ts'; +import { displayPath } from '../../../utils/build-preflight.ts'; import { resolveEffectiveDerivedDataPath } from '../../../utils/derived-data-path.ts'; +import { resolvePathFromCwd } from '../../../utils/path.ts'; +import { + createDefaultTestProductsPath, + findXctestrunPaths, + markTestProductsPathCompleted, +} from '../../../utils/test-products-path.ts'; import { createBuildInvocationFragment } from '../../../utils/xcodebuild-pipeline.ts'; const baseOptions = { @@ -58,6 +65,14 @@ const baseOptions = { .optional() .describe('Whether to use the latest OS version for the named simulator'), preferXcodebuild: z.boolean().optional(), + buildForTesting: z + .boolean() + .optional() + .describe('Build reusable test products without running tests (default: false)'), + testProductsPath: z + .string() + .optional() + .describe('Output path for the .xctestproducts bundle when buildForTesting is true'), }; const baseSchemaObject = z.object({ @@ -72,15 +87,19 @@ const baseSchemaObject = z.object({ ...baseOptions, }); -const buildSimulatorSchema = z.preprocess( +export const buildSimulatorSchema = z.preprocess( nullifyEmptyStrings, - withSimulatorIdOrName(withProjectOrWorkspace(baseSchemaObject)), + withSimulatorIdOrName(withProjectOrWorkspace(baseSchemaObject)).refine( + (params) => params.testProductsPath === undefined || params.buildForTesting === true, + { message: 'testProductsPath requires buildForTesting to be true' }, + ), ); export type BuildSimulatorParams = z.infer; type BuildSimulatorResult = BuildResultDomainResult; -interface PreparedBuildSimExecution { +export interface PreparedBuildSimExecution { + buildAction: 'build' | 'build-for-testing'; configuration?: string; detectedPlatform: InferPlatformResult['platform']; platformName: string; @@ -93,10 +112,12 @@ interface PreparedBuildSimExecution { logPrefix: string; }; invocationRequest: BuildInvocationRequest; + isManagedTestProductsPath: boolean; + testProductsPath?: string; warningMessage?: string; } -async function prepareBuildSimExecution( +export async function prepareBuildSimExecution( params: BuildSimulatorParams, executor: CommandExecutor, ): Promise { @@ -114,20 +135,34 @@ async function prepareBuildSimExecution( ); const detectedPlatform = inferred.platform; const platformName = detectedPlatform.replace(' Simulator', ''); + const buildForTesting = params.buildForTesting ?? false; + const isManagedTestProductsPath = buildForTesting && params.testProductsPath === undefined; + const testProductsPath = buildForTesting + ? (resolvePathFromCwd(params.testProductsPath) ?? createDefaultTestProductsPath('build_sim')) + : undefined; + const sharedBuildParams = testProductsPath + ? { + ...params, + configuration, + extraArgs: [...(params.extraArgs ?? []), '-testProductsPath', testProductsPath], + } + : { ...params, configuration }; return { + buildAction: buildForTesting ? 'build-for-testing' : 'build', configuration, detectedPlatform, platformName, - sharedBuildParams: { ...params, configuration }, + sharedBuildParams, platformOptions: { platform: detectedPlatform, simulatorName: params.simulatorName, simulatorId: params.simulatorId, useLatestOS: params.simulatorId ? false : useLatestOS, - logPrefix: `${platformName} Simulator Build`, + logPrefix: `${platformName} Simulator ${buildForTesting ? 'Build for Testing' : 'Build'}`, }, invocationRequest: { + ...(buildForTesting ? { buildForTesting: true } : {}), scheme: params.scheme, workspacePath: params.workspacePath, projectPath: params.projectPath, @@ -136,7 +171,10 @@ async function prepareBuildSimExecution( platform: detectedPlatform, simulatorName: params.simulatorName, simulatorId: params.simulatorId, + ...(testProductsPath ? { testProductsPath: displayPath(testProductsPath) } : {}), }, + isManagedTestProductsPath, + testProductsPath, warningMessage: params.simulatorId && params.useLatestOS !== undefined ? 'useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)' @@ -177,18 +215,32 @@ export function createBuildSimExecutor( resolved.sharedBuildParams, resolved.platformOptions, params.preferXcodebuild ?? false, - 'build', + resolved.buildAction, executor, undefined, started.pipeline, ); + const succeeded = !buildResult.isError; + + if (resolved.isManagedTestProductsPath) { + markTestProductsPathCompleted(resolved.testProductsPath); + } + + const xctestrunPaths = + succeeded && resolved.testProductsPath + ? await findXctestrunPaths(resolved.testProductsPath) + : []; return createBuildDomainResult({ started, - succeeded: !buildResult.isError, + succeeded, target: 'simulator', artifacts: { - buildLogPath: started.pipeline.logPath, + buildLogPath: displayPath(started.pipeline.logPath), + ...(succeeded && resolved.testProductsPath + ? { testProductsPath: displayPath(resolved.testProductsPath) } + : {}), + ...(xctestrunPaths.length > 0 ? { xctestrunPaths: xctestrunPaths.map(displayPath) } : {}), }, fallbackErrorMessages: collectFallbackErrorMessages(started, [], buildResult.content), request: resolved.invocationRequest, @@ -208,11 +260,20 @@ export async function build_simLogic( const executeBuildSim = createBuildSimExecutor(executor, prepared); const result = await executeBuildSim(params, executionContext); - setXcodebuildStructuredOutput(ctx, 'build-result', result); + setXcodebuildStructuredOutput(ctx, 'build-result', result, '3'); if (!result.didError) { - ctx.nextStepParams = { - get_sim_app_path: { + if (prepared.testProductsPath) { + const nextParams = { + testProductsPath: displayPath(prepared.testProductsPath), + ...(params.simulatorId + ? { simulatorId: params.simulatorId } + : { simulatorName: params.simulatorName ?? '' }), + }; + ctx.nextStepParams = { test_sim: nextParams }; + ctx.nextSteps = [{ tool: 'test_sim', label: 'Run prepared tests', params: nextParams }]; + } else { + const nextParams = { ...(params.simulatorId ? { simulatorId: params.simulatorId } : { simulatorName: params.simulatorName ?? '' }), @@ -221,8 +282,17 @@ export async function build_simLogic( ...(params.derivedDataPath !== undefined ? { derivedDataPath: params.derivedDataPath } : {}), - }, - }; + }; + ctx.nextStepParams = { get_sim_app_path: nextParams }; + ctx.nextSteps = [ + { + tool: 'get_sim_app_path', + label: 'Get built app path in simulator derived data', + params: nextParams, + priority: 1, + }, + ]; + } } } diff --git a/src/mcp/tools/simulator/test_sim.ts b/src/mcp/tools/simulator/test_sim.ts index e50516f2d..3c7886b23 100644 --- a/src/mcp/tools/simulator/test_sim.ts +++ b/src/mcp/tools/simulator/test_sim.ts @@ -16,11 +16,12 @@ import { getDefaultCommandExecutor, getDefaultFileSystemExecutor, } from '../../../utils/execution/index.ts'; +import { nullifyEmptyStrings, withSimulatorIdOrName } from '../../../utils/schema-helpers.ts'; import { - nullifyEmptyStrings, - withProjectOrWorkspace, - withSimulatorIdOrName, -} from '../../../utils/schema-helpers.ts'; + hasPreparedTestSource, + TEST_SOURCE_EXCLUSIVE_GROUPS, + withProjectWorkspaceOrTestArtifact, +} from '../../../utils/test-source.ts'; import { createSessionAwareTool, getSessionAwareToolSchemaShape, @@ -39,6 +40,7 @@ import { import type { BuildInvocationRequest } from '../../../types/domain-fragments.ts'; import { resolveEffectiveDerivedDataPath } from '../../../utils/derived-data-path.ts'; import { createBuildInvocationFragment } from '../../../utils/xcodebuild-pipeline.ts'; +import { displayPath } from '../../../utils/build-preflight.ts'; const baseSchemaObject = z.object({ projectPath: z @@ -49,7 +51,15 @@ const baseSchemaObject = z.object({ .string() .optional() .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'), - scheme: z.string().describe('The scheme to use (Required)'), + scheme: z.string().optional().describe('The scheme to use in source mode'), + testProductsPath: z + .string() + .optional() + .describe('Path to a prepared .xctestproducts package. Cannot be combined with source inputs'), + xctestrunPath: z + .string() + .optional() + .describe('Path to a prepared .xctestrun file. Cannot be combined with source inputs'), simulatorId: z .string() .optional() @@ -84,7 +94,7 @@ const baseSchemaObject = z.object({ const testSimulatorSchema = z.preprocess( nullifyEmptyStrings, - withSimulatorIdOrName(withProjectOrWorkspace(baseSchemaObject)), + withSimulatorIdOrName(withProjectWorkspaceOrTestArtifact(baseSchemaObject)), ); type TestSimulatorParams = z.infer; @@ -105,7 +115,8 @@ async function prepareTestSimExecution( executor: CommandExecutor, fileSystemExecutor: FileSystemExecutor, ): Promise { - const configuration = params.configuration; + const preparedTestSource = hasPreparedTestSource(params); + const configuration = preparedTestSource ? undefined : params.configuration; const inferred = await inferPlatform( { projectPath: params.projectPath, @@ -137,11 +148,15 @@ async function prepareTestSimExecution( scheme: params.scheme, workspacePath: params.workspacePath, projectPath: params.projectPath, - derivedDataPath: resolveEffectiveDerivedDataPath(params), + derivedDataPath: preparedTestSource ? undefined : resolveEffectiveDerivedDataPath(params), configuration, platform: inferred.platform, simulatorName: params.simulatorName, simulatorId: params.simulatorId, + testProductsPath: params.testProductsPath + ? displayPath(params.testProductsPath) + : undefined, + xctestrunPath: params.xctestrunPath ? displayPath(params.xctestrunPath) : undefined, }, warningMessage: params.simulatorId && params.useLatestOS !== undefined @@ -151,17 +166,19 @@ async function prepareTestSimExecution( } const destinationName = params.simulatorName ?? simulatorResolution.simulatorName; - const preflight = await resolveTestPreflight( - { - projectPath: params.projectPath, - workspacePath: params.workspacePath, - scheme: params.scheme, - configuration, - extraArgs: params.extraArgs, - destinationName, - }, - fileSystemExecutor, - ); + const preflight = preparedTestSource + ? null + : await resolveTestPreflight( + { + projectPath: params.projectPath, + workspacePath: params.workspacePath, + scheme: params.scheme!, + configuration, + extraArgs: params.extraArgs, + destinationName, + }, + fileSystemExecutor, + ); return { configuration, @@ -172,11 +189,13 @@ async function prepareTestSimExecution( scheme: params.scheme, workspacePath: params.workspacePath, projectPath: params.projectPath, - derivedDataPath: resolveEffectiveDerivedDataPath(params), + derivedDataPath: preparedTestSource ? undefined : resolveEffectiveDerivedDataPath(params), configuration, platform: inferred.platform, simulatorName: params.simulatorName, simulatorId: params.simulatorId, + testProductsPath: params.testProductsPath ? displayPath(params.testProductsPath) : undefined, + xctestrunPath: params.xctestrunPath ? displayPath(params.xctestrunPath) : undefined, onlyTesting: preflight?.selectors.onlyTesting.map((selector) => selector.raw), skipTesting: preflight?.selectors.skipTesting.map((selector) => selector.raw), }, @@ -212,7 +231,7 @@ export function createTestSimExecutor( succeeded: false, target: 'simulator', artifacts: { - buildLogPath: started.pipeline.logPath, + buildLogPath: displayPath(started.pipeline.logPath), }, fallbackErrorMessages: [ resolved.resolutionError ?? 'Failed to resolve simulator identifier for test execution.', @@ -243,6 +262,8 @@ export function createTestSimExecutor( platform: resolved.platform, testRunnerEnv: params.testRunnerEnv, progress: params.progress, + testProductsPath: params.testProductsPath, + xctestrunPath: params.xctestrunPath, }, ctx, ); @@ -262,7 +283,7 @@ export async function test_simLogic( const executeTestSim = createTestSimExecutor(executor, fileSystemExecutor, prepared); const result = await executeTestSim(params, executionContext); - setXcodebuildStructuredOutput(ctx, 'test-result', result); + setXcodebuildStructuredOutput(ctx, 'test-result', result, '3'); } const publicSchemaObject = baseSchemaObject.omit({ @@ -288,11 +309,10 @@ export const handler = createSessionAwareTool({ test_simLogic(params, executor, getDefaultFileSystemExecutor()), getExecutor: getDefaultCommandExecutor, requirements: [ - { allOf: ['scheme'], message: 'scheme is required' }, - { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' }, { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' }, ], exclusivePairs: [ + ...TEST_SOURCE_EXCLUSIVE_GROUPS, ['projectPath', 'workspacePath'], ['simulatorId', 'simulatorName'], ], diff --git a/src/types/domain-fragments.ts b/src/types/domain-fragments.ts index 3d5ae45fb..520a8cb67 100644 --- a/src/types/domain-fragments.ts +++ b/src/types/domain-fragments.ts @@ -46,6 +46,7 @@ export type BuildLikeKind = 'build-result' | 'build-run-result' | 'test-result'; // --------------------------------------------------------------------------- export interface BuildInvocationRequest { + buildForTesting?: boolean; scheme?: string; workspacePath?: string; projectPath?: string; @@ -60,6 +61,8 @@ export interface BuildInvocationRequest { executableName?: string; arch?: string; derivedDataPath?: string; + testProductsPath?: string; + xctestrunPath?: string; onlyTesting?: string[]; skipTesting?: string[]; } diff --git a/src/types/domain-results.ts b/src/types/domain-results.ts index b71546674..cc035f6ed 100644 --- a/src/types/domain-results.ts +++ b/src/types/domain-results.ts @@ -142,6 +142,8 @@ export type BuildResultArtifacts = AtLeastOne<{ scheme: string; configuration: string; platform: string; + testProductsPath: string; + xctestrunPaths: string[]; }>; export type BuildRunResultArtifacts = AtLeastOne<{ appPath: string; @@ -176,6 +178,8 @@ export type TestResultArtifacts = AtLeastOne<{ buildLogPath: string; packagePath: string; xcresultPath: string; + testProductsPath: string; + xctestrunPath: string; }>; export interface CoverageSummary extends StatusSummary { coveragePct?: number; diff --git a/src/utils/__tests__/prepared-test-execution.test.ts b/src/utils/__tests__/prepared-test-execution.test.ts new file mode 100644 index 000000000..f9e5399f6 --- /dev/null +++ b/src/utils/__tests__/prepared-test-execution.test.ts @@ -0,0 +1,110 @@ +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import type { CommandExecutor } from '../command.ts'; +import { createMockCommandResponse } from '../../test-utils/mock-executors.ts'; +import { DefaultStreamingExecutionContext } from '../execution/index.ts'; +import { setXcodeBuildMCPAppDirOverrideForTests } from '../log-paths.ts'; +import { setRuntimeInstanceForTests } from '../runtime-instance.ts'; +import { createTestExecutor } from '../test-common.ts'; +import { resetWorkspaceFilesystemLifecycleStateForTests } from '../workspace-filesystem-lifecycle.ts'; +import { XcodePlatform } from '../xcode.ts'; + +describe('prepared test execution', () => { + let tempAppDir: string; + + beforeEach(() => { + tempAppDir = mkdtempSync(join(tmpdir(), 'xcodebuildmcp-prepared-test-')); + setXcodeBuildMCPAppDirOverrideForTests(tempAppDir); + setRuntimeInstanceForTests({ + instanceId: 'prepared-test', + pid: process.pid, + workspaceKey: 'workspace-a', + }); + }); + + afterEach(() => { + resetWorkspaceFilesystemLifecycleStateForTests(); + setXcodeBuildMCPAppDirOverrideForTests(null); + setRuntimeInstanceForTests(null); + rmSync(tempAppDir, { recursive: true, force: true }); + }); + + it('runs prepared test products without source or DerivedData arguments', async () => { + const commands: string[][] = []; + const workingDirectories: Array = []; + const executor: CommandExecutor = async (command, _logPrefix, _useShell, options) => { + commands.push(command); + workingDirectories.push(options?.cwd); + return createMockCommandResponse({ success: true, output: '', exitCode: 0 }); + }; + const executeTest = createTestExecutor(executor, { + toolName: 'test_sim', + target: 'simulator', + request: { + platform: XcodePlatform.iOSSimulator, + simulatorId: 'A2C64636-37E9-4B68-B872-E7F0A82A5670', + }, + }); + + const result = await executeTest( + { + testProductsPath: '/tmp/Prepared Tests.xctestproducts', + simulatorId: 'A2C64636-37E9-4B68-B872-E7F0A82A5670', + platform: XcodePlatform.iOSSimulator, + extraArgs: ['-only-testing:WeatherTests/testWeather'], + }, + new DefaultStreamingExecutionContext(), + ); + + expect(commands).toHaveLength(1); + expect(commands[0]).toEqual([ + 'xcodebuild', + '-testProductsPath', + '/tmp/Prepared Tests.xctestproducts', + '-destination', + 'platform=iOS Simulator,id=A2C64636-37E9-4B68-B872-E7F0A82A5670', + '-collect-test-diagnostics', + 'never', + '-only-testing:WeatherTests/testWeather', + '-resultBundlePath', + expect.stringContaining('/result-bundles/test_sim_'), + 'test-without-building', + ]); + expect(result.artifacts.testProductsPath).toBe('/tmp/Prepared Tests.xctestproducts'); + expect(result.artifacts.xcresultPath).toEqual(expect.stringMatching(/\.xcresult$/u)); + expect(workingDirectories).toEqual([undefined]); + }); + + it('runs an xctestrun artifact directly for a physical device', async () => { + const commands: string[][] = []; + const executor: CommandExecutor = async (command) => { + commands.push(command); + return createMockCommandResponse({ success: true, output: '', exitCode: 0 }); + }; + const executeTest = createTestExecutor(executor, { + toolName: 'test_device', + target: 'device', + request: { platform: XcodePlatform.iOS, deviceId: 'DEVICE-123' }, + }); + + const result = await executeTest( + { + xctestrunPath: '/tmp/Weather.xctestrun', + deviceId: 'DEVICE-123', + platform: XcodePlatform.iOS, + }, + new DefaultStreamingExecutionContext(), + ); + + expect(commands).toHaveLength(1); + expect(commands[0]).toContain('-xctestrun'); + expect(commands[0]).toContain('/tmp/Weather.xctestrun'); + expect(commands[0]).toContain('platform=iOS,id=DEVICE-123'); + expect(commands[0]).not.toContain('-scheme'); + expect(commands[0]).not.toContain('-derivedDataPath'); + expect(commands[0]!.at(-1)).toBe('test-without-building'); + expect(result.artifacts.xctestrunPath).toBe('/tmp/Weather.xctestrun'); + }); +}); diff --git a/src/utils/__tests__/test-common.test.ts b/src/utils/__tests__/test-common.test.ts index 87071f951..1678abbc3 100644 --- a/src/utils/__tests__/test-common.test.ts +++ b/src/utils/__tests__/test-common.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { ChildProcess } from 'node:child_process'; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createTestExecutor, resolveTestProgressEnabled } from '../test-common.ts'; @@ -15,6 +15,7 @@ import { } from '../log-paths.ts'; import { setRuntimeInstanceForTests } from '../runtime-instance.ts'; import { resetWorkspaceFilesystemLifecycleStateForTests } from '../workspace-filesystem-lifecycle.ts'; +import { getTestProductsCompletionMarkerPath } from '../test-products-path.ts'; function createSuccessfulCommandResponse(): CommandResponse { return { @@ -217,13 +218,21 @@ describe('createTestExecutor', () => { new DefaultStreamingExecutionContext(), ); - expect(commands).toHaveLength(1); - const resultBundlePath = expectDefaultResultBundlePath(commands[0]!, 'test_macos'); - expect(commands[0]!.at(-1)).toBe('test'); + expect(commands).toHaveLength(2); + expect(commands[0]!.at(-1)).toBe('build-for-testing'); + expect(commands[0]).toContain('-testProductsPath'); + const resultBundlePath = expectDefaultResultBundlePath(commands[1]!, 'test_macos'); + expect(commands[1]!.at(-1)).toBe('test-without-building'); + expect(commands[1]).not.toContain('-project'); + expect(commands[1]).not.toContain('-scheme'); + expect(commands[1]).not.toContain('-derivedDataPath'); expect(result.artifacts.xcresultPath).toBe(resultBundlePath); + expect(result.artifacts.testProductsPath).toEqual( + expect.stringContaining(getWorkspaceFilesystemLayout('workspace-a').testProducts), + ); }); - it('returns a structured test-result when default result bundle path resolution fails', async () => { + it('propagates default result bundle setup failures as runtime errors', async () => { const layout = getWorkspaceFilesystemLayout('workspace-a'); mkdirSync(layout.root, { recursive: true }); writeFileSync(layout.resultBundles, 'not a directory'); @@ -240,23 +249,19 @@ describe('createTestExecutor', () => { }, }); - const result = await executeTest( - { - projectPath: 'Weather.xcodeproj', - scheme: 'Weather', - configuration: 'Debug', - platform: XcodePlatform.macOS, - }, - new DefaultStreamingExecutionContext(), - ); + await expect( + executeTest( + { + projectPath: 'Weather.xcodeproj', + scheme: 'Weather', + configuration: 'Debug', + platform: XcodePlatform.macOS, + }, + new DefaultStreamingExecutionContext(), + ), + ).rejects.toThrow('Unable to create writable result bundle directory'); expect(executor).not.toHaveBeenCalled(); - expect(result.kind).toBe('test-result'); - expect(result.didError).toBe(true); - expect(result.summary.status).toBe('FAILED'); - expect(result.diagnostics.rawOutput?.join('\n')).toContain( - 'Unable to create writable result bundle directory', - ); }); it('injects a workspace-scoped default result bundle path for device test commands', async () => { @@ -289,16 +294,21 @@ describe('createTestExecutor', () => { new DefaultStreamingExecutionContext(), ); - expect(commands).toHaveLength(1); - const resultBundlePath = expectDefaultResultBundlePath(commands[0]!, 'test_device'); - expect(commands[0]!.at(-1)).toBe('test'); + expect(commands).toHaveLength(2); + expect(commands[0]!.at(-1)).toBe('build-for-testing'); + const resultBundlePath = expectDefaultResultBundlePath(commands[1]!, 'test_device'); + expect(commands[1]!.at(-1)).toBe('test-without-building'); expect(result.artifacts.xcresultPath).toBe(resultBundlePath); }); it('does not surface a result bundle when simulator build-for-testing fails before tests run', async () => { const commands: string[][] = []; + let testProductsPath: string | undefined; const executor: CommandExecutor = async (command, _logPrefix, _useShell, opts) => { commands.push(command); + const testProductsIndex = command.indexOf('-testProductsPath'); + testProductsPath = command[testProductsIndex + 1]; + mkdirSync(testProductsPath!); opts?.onStderr?.( 'Writing error result bundle to /var/folders/test/ResultBundle_2026-05-07_09-38-0016.xcresult\n', ); @@ -336,6 +346,7 @@ describe('createTestExecutor', () => { expect(commands).toHaveLength(1); expect(commands[0]).not.toContain('-resultBundlePath'); expect(result.artifacts.xcresultPath).toBeUndefined(); + expect(existsSync(getTestProductsCompletionMarkerPath(testProductsPath!))).toBe(true); }); it('injects the default result bundle only into the simulator test execution phase', async () => { @@ -370,9 +381,14 @@ describe('createTestExecutor', () => { expect(commands).toHaveLength(2); expect(commands[0]).not.toContain('-resultBundlePath'); + expect(commands[0]).toContain('-testProductsPath'); expect(commands[0]!.at(-1)).toBe('build-for-testing'); const testResultBundlePath = expectDefaultResultBundlePath(commands[1]!, 'test_sim'); expect(commands[1]!.at(-1)).toBe('test-without-building'); + expect(commands[1]).toContain('-testProductsPath'); + expect(commands[1]).not.toContain('-project'); + expect(commands[1]).not.toContain('-scheme'); + expect(commands[1]).not.toContain('-derivedDataPath'); expect(result.artifacts.xcresultPath).toBe(testResultBundlePath); }); @@ -405,11 +421,57 @@ describe('createTestExecutor', () => { new DefaultStreamingExecutionContext(), ); - expect(commands).toHaveLength(1); - expect(commands[0]).toContain('-quiet'); - expect(commands[0]).toContain('-resultBundlePath'); - expect(commands[0]).toContain('/tmp/User Provided.xcresult'); - expect(commands[0]).not.toContain(getWorkspaceFilesystemLayout('workspace-a').resultBundles); + expect(commands).toHaveLength(2); + expect(commands[1]).toContain('-quiet'); + expect(commands[1]).toContain('-resultBundlePath'); + expect(commands[1]).toContain('/tmp/User Provided.xcresult'); + expect(commands[1].filter((argument) => argument === '-resultBundlePath')).toHaveLength(1); + expect( + commands[1].filter((argument) => argument === '/tmp/User Provided.xcresult'), + ).toHaveLength(1); + expect(commands[0]).not.toContain('/tmp/User Provided.xcresult'); + expect(commands[1]).not.toContain(getWorkspaceFilesystemLayout('workspace-a').resultBundles); expect(result.artifacts.xcresultPath).toBe('/tmp/User Provided.xcresult'); }); + + it('keeps managed test products active until a failed test run settles', async () => { + let testProductsPath: string | undefined; + const executor: CommandExecutor = async (command) => { + if (command.at(-1) === 'build-for-testing') { + const testProductsIndex = command.indexOf('-testProductsPath'); + testProductsPath = command[testProductsIndex + 1]; + mkdirSync(testProductsPath!); + return createSuccessfulCommandResponse(); + } + + expect(existsSync(getTestProductsCompletionMarkerPath(testProductsPath!))).toBe(false); + return { + ...createSuccessfulCommandResponse(), + success: false, + error: 'Tests failed', + exitCode: 65, + }; + }; + const executeTest = createTestExecutor(executor, { + toolName: 'test_macos', + target: 'macos', + request: { + scheme: 'Weather', + projectPath: '/tmp/Weather/Weather.xcodeproj', + platform: XcodePlatform.macOS, + }, + }); + + const result = await executeTest( + { + projectPath: '/tmp/Weather/Weather.xcodeproj', + scheme: 'Weather', + platform: XcodePlatform.macOS, + }, + new DefaultStreamingExecutionContext(), + ); + + expect(result.didError).toBe(true); + expect(existsSync(getTestProductsCompletionMarkerPath(testProductsPath!))).toBe(true); + }); }); diff --git a/src/utils/__tests__/test-products-lifecycle.test.ts b/src/utils/__tests__/test-products-lifecycle.test.ts new file mode 100644 index 000000000..587072f93 --- /dev/null +++ b/src/utils/__tests__/test-products-lifecycle.test.ts @@ -0,0 +1,111 @@ +import { existsSync, mkdirSync, mkdtempSync, utimesSync, writeFileSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import * as path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + TEST_PRODUCTS_MAX_AGE_MS, + pruneManagedTestProductsDirectory, +} from '../test-products-lifecycle.ts'; +import { + getTestProductsCompletionMarkerPath, + isXcodeBuildMCPManagedTestProductsName, +} from '../test-products-path.ts'; + +const DAY_MS = 24 * 60 * 60 * 1000; +const DEAD_OWNER_PID = 999_999_999; + +function managedName(name: string, pid = DEAD_OWNER_PID): string { + return `${name}_2026-05-02T12-00-00-000Z_pid${pid}_abcdef12.xctestproducts`; +} + +function writeTestProducts(directory: string, mtimeMs: number, completed = false): void { + mkdirSync(directory, { recursive: true }); + writeFileSync(path.join(directory, 'Tests.xctestrun'), 'stub'); + const mtime = new Date(mtimeMs); + utimesSync(directory, mtime, mtime); + if (completed) { + writeFileSync(getTestProductsCompletionMarkerPath(directory), 'completed'); + } +} + +describe('test products lifecycle', () => { + let root: string; + + beforeEach(() => { + root = mkdtempSync(path.join(tmpdir(), 'xcodebuildmcp-test-products-lifecycle-')); + }); + + afterEach(async () => { + await rm(root, { recursive: true, force: true }); + }); + + it('prunes managed products after three days while preserving caller-owned paths', async () => { + const now = Date.UTC(2026, 4, 6, 12); + const oldManaged = path.join(root, managedName('old')); + const recentManaged = path.join(root, managedName('recent')); + const callerOwned = path.join(root, 'caller-provided.xctestproducts'); + const externalCallerOwned = path.join( + path.dirname(root), + `${path.basename(root)}-external-caller.xctestproducts`, + ); + writeTestProducts(oldManaged, now - TEST_PRODUCTS_MAX_AGE_MS - 1, true); + writeTestProducts(recentManaged, now - 2 * DAY_MS, true); + writeTestProducts(callerOwned, now - 10 * DAY_MS, true); + writeTestProducts(externalCallerOwned, now - 10 * DAY_MS, true); + + const result = await pruneManagedTestProductsDirectory({ + testProductsDir: root, + now, + minVisibleMs: 0, + }); + + expect(result).toEqual({ scanned: 2, deleted: 1 }); + expect(existsSync(oldManaged)).toBe(false); + expect(existsSync(recentManaged)).toBe(true); + expect(existsSync(callerOwned)).toBe(true); + expect(existsSync(externalCallerOwned)).toBe(true); + await rm(externalCallerOwned, { recursive: true, force: true }); + }); + + it('protects live in-progress products until their completion marker exists', async () => { + const now = Date.UTC(2026, 4, 6, 12); + const live = path.join(root, managedName('live', process.pid)); + writeTestProducts(live, now - 4 * DAY_MS); + + expect(isXcodeBuildMCPManagedTestProductsName(path.basename(live))).toBe(true); + expect( + await pruneManagedTestProductsDirectory({ testProductsDir: root, now, minVisibleMs: 0 }), + ).toEqual({ scanned: 1, deleted: 0 }); + expect(existsSync(live)).toBe(true); + + writeFileSync(getTestProductsCompletionMarkerPath(live), 'completed'); + expect( + await pruneManagedTestProductsDirectory({ testProductsDir: root, now, minVisibleMs: 0 }), + ).toEqual({ scanned: 1, deleted: 1 }); + expect(existsSync(live)).toBe(false); + }); + + it('uses a separate count cap for retained test products', async () => { + const now = Date.UTC(2026, 4, 6, 12); + const oldest = path.join(root, managedName('oldest')); + const middle = path.join(root, managedName('middle')); + const newest = path.join(root, managedName('newest')); + writeTestProducts(oldest, now - 3 * DAY_MS, true); + writeTestProducts(middle, now - 2 * DAY_MS, true); + writeTestProducts(newest, now - DAY_MS, true); + + const result = await pruneManagedTestProductsDirectory({ + testProductsDir: root, + now, + minVisibleMs: 0, + maxAgeMs: 10 * DAY_MS, + maxCount: 2, + }); + + expect(result).toEqual({ scanned: 3, deleted: 1 }); + expect(existsSync(oldest)).toBe(false); + expect(existsSync(middle)).toBe(true); + expect(existsSync(newest)).toBe(true); + }); +}); diff --git a/src/utils/__tests__/test-products-path.test.ts b/src/utils/__tests__/test-products-path.test.ts new file mode 100644 index 000000000..42233c751 --- /dev/null +++ b/src/utils/__tests__/test-products-path.test.ts @@ -0,0 +1,73 @@ +import { existsSync, mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import * as path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + createDefaultTestProductsPath, + findXctestrunPaths, + getTestProductsCompletionMarkerPath, + isXcodeBuildMCPManagedTestProductsName, + markTestProductsPathCompleted, +} from '../test-products-path.ts'; +import { + getWorkspaceFilesystemLayout, + setXcodeBuildMCPAppDirOverrideForTests, +} from '../log-paths.ts'; +import { setRuntimeInstanceForTests } from '../runtime-instance.ts'; + +describe('test products paths', () => { + let appDir: string; + + beforeEach(() => { + appDir = mkdtempSync(path.join(tmpdir(), 'xcodebuildmcp-test-products-path-')); + setXcodeBuildMCPAppDirOverrideForTests(appDir); + setRuntimeInstanceForTests({ + instanceId: 'test-products-path', + pid: process.pid, + workspaceKey: 'workspace-a', + }); + }); + + afterEach(async () => { + setRuntimeInstanceForTests(null); + setXcodeBuildMCPAppDirOverrideForTests(null); + await rm(appDir, { recursive: true, force: true }); + }); + + it('creates unique workspace-scoped managed paths', () => { + const first = createDefaultTestProductsPath('build_sim'); + const second = createDefaultTestProductsPath('build_sim'); + + expect(path.dirname(first)).toBe(getWorkspaceFilesystemLayout('workspace-a').testProducts); + expect(first).not.toBe(second); + expect(isXcodeBuildMCPManagedTestProductsName(path.basename(first))).toBe(true); + expect(isXcodeBuildMCPManagedTestProductsName('caller-provided.xctestproducts')).toBe(false); + }); + + it('atomically marks a generated test products directory completed', () => { + const testProductsPath = createDefaultTestProductsPath('test_sim'); + mkdirSync(testProductsPath); + + markTestProductsPathCompleted(testProductsPath); + + expect(existsSync(getTestProductsCompletionMarkerPath(testProductsPath))).toBe(true); + }); + + it('finds xctestrun files without traversing symbolic links', async () => { + const testProductsPath = createDefaultTestProductsPath('test_sim'); + const nested = path.join(testProductsPath, 'nested'); + const outside = path.join(appDir, 'outside'); + mkdirSync(nested, { recursive: true }); + mkdirSync(outside); + writeFileSync(path.join(testProductsPath, 'B.xctestrun'), 'b'); + writeFileSync(path.join(nested, 'A.xctestrun'), 'a'); + writeFileSync(path.join(outside, 'Outside.xctestrun'), 'outside'); + symlinkSync(outside, path.join(testProductsPath, 'linked-outside')); + + expect(await findXctestrunPaths(testProductsPath)).toEqual([ + path.join(testProductsPath, 'B.xctestrun'), + path.join(nested, 'A.xctestrun'), + ]); + }); +}); diff --git a/src/utils/__tests__/test-products-purge.test.ts b/src/utils/__tests__/test-products-purge.test.ts new file mode 100644 index 000000000..c94ab8c4e --- /dev/null +++ b/src/utils/__tests__/test-products-purge.test.ts @@ -0,0 +1,73 @@ +import { existsSync, mkdirSync, mkdtempSync, utimesSync, writeFileSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import * as path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + enumeratePurgeStorage, + executePurgeStoragePlan, + planPurgeStorage, +} from '../purge-storage.ts'; +import { + getWorkspaceFilesystemLayout, + setXcodeBuildMCPAppDirOverrideForTests, +} from '../log-paths.ts'; +import { getTestProductsCompletionMarkerPath } from '../test-products-path.ts'; + +const WORKSPACE_KEY = 'Demo-aaaaaaaaaaaa'; + +function writeTestProducts(directory: string, mtimeMs: number): void { + mkdirSync(directory, { recursive: true }); + writeFileSync(path.join(directory, 'Tests.xctestrun'), 'stub'); + const mtime = new Date(mtimeMs); + utimesSync(directory, mtime, mtime); +} + +describe('test products purge storage', () => { + let appDir: string; + + beforeEach(() => { + appDir = mkdtempSync(path.join(tmpdir(), 'xcodebuildmcp-test-products-purge-')); + setXcodeBuildMCPAppDirOverrideForTests(appDir); + }); + + afterEach(async () => { + setXcodeBuildMCPAppDirOverrideForTests(null); + await rm(appDir, { recursive: true, force: true }); + }); + + it('reports and deletes only managed test products in the workspace storage root', async () => { + const now = Date.UTC(2026, 4, 6, 12); + const layout = getWorkspaceFilesystemLayout(WORKSPACE_KEY); + const managed = path.join( + layout.testProducts, + 'test_sim_2026-05-02T12-00-00-000Z_pid999999999_abcdef12.xctestproducts', + ); + const callerOwned = path.join(layout.testProducts, 'caller-provided.xctestproducts'); + const externalCallerOwned = path.join(appDir, 'external-caller.xctestproducts'); + writeTestProducts(managed, now - 4 * 24 * 60 * 60 * 1000); + writeFileSync(getTestProductsCompletionMarkerPath(managed), 'completed'); + writeTestProducts(callerOwned, now - 4 * 24 * 60 * 60 * 1000); + writeTestProducts(externalCallerOwned, now - 4 * 24 * 60 * 60 * 1000); + + const report = await enumeratePurgeStorage({ + scope: { type: 'workspace', workspaceKey: WORKSPACE_KEY }, + }); + expect(report.workspaces[0]?.classes.testProducts.exists).toBe(true); + + const plan = await planPurgeStorage({ + report, + scope: { type: 'workspace', workspaceKey: WORKSPACE_KEY }, + classes: ['testProducts'], + now, + }); + expect(plan.candidates.map((candidate) => candidate.path)).toEqual([managed]); + + const result = await executePurgeStoragePlan(plan, { now }); + expect(result.deleted.map((entry) => entry.path)).toEqual([managed]); + expect(existsSync(managed)).toBe(false); + expect(existsSync(getTestProductsCompletionMarkerPath(managed))).toBe(false); + expect(existsSync(callerOwned)).toBe(true); + expect(existsSync(externalCallerOwned)).toBe(true); + }); +}); diff --git a/src/utils/__tests__/test-source.test.ts b/src/utils/__tests__/test-source.test.ts new file mode 100644 index 000000000..c5f1ca267 --- /dev/null +++ b/src/utils/__tests__/test-source.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import * as z from 'zod'; +import { mergeSessionDefaultArgs } from '../session-default-args.ts'; +import { + TEST_SOURCE_EXCLUSIVE_GROUPS, + withProjectWorkspaceOrTestArtifact, +} from '../test-source.ts'; + +const schema = withProjectWorkspaceOrTestArtifact( + z.object({ + projectPath: z.string().optional(), + workspacePath: z.string().optional(), + scheme: z.string().optional(), + configuration: z.string().optional(), + derivedDataPath: z.string().optional(), + testProductsPath: z.string().optional(), + xctestrunPath: z.string().optional(), + extraArgs: z.array(z.string()).optional(), + }), +); + +describe('prepared test source validation', () => { + it('accepts either prepared artifact without source inputs', () => { + expect(schema.safeParse({ testProductsPath: '/tmp/Tests.xctestproducts' }).success).toBe(true); + expect(schema.safeParse({ xctestrunPath: '/tmp/Tests.xctestrun' }).success).toBe(true); + }); + + it('rejects prepared artifacts combined with source inputs or conflicting extra arguments', () => { + expect( + schema.safeParse({ + testProductsPath: '/tmp/Tests.xctestproducts', + scheme: 'Tests', + }).success, + ).toBe(false); + expect( + schema.safeParse({ + xctestrunPath: '/tmp/Tests.xctestrun', + extraArgs: ['-project', 'Weather.xcodeproj'], + }).success, + ).toBe(false); + }); + + it('suppresses source session defaults while preserving destination defaults', () => { + const merged = mergeSessionDefaultArgs({ + defaults: { + projectPath: 'Weather.xcodeproj', + scheme: 'Weather', + configuration: 'Debug', + derivedDataPath: '/tmp/DerivedData', + simulatorId: 'SIM-123', + }, + explicitArgs: { testProductsPath: '/tmp/Tests.xctestproducts' }, + exclusivePairs: TEST_SOURCE_EXCLUSIVE_GROUPS, + }); + + expect(merged).toEqual({ + simulatorId: 'SIM-123', + testProductsPath: '/tmp/Tests.xctestproducts', + }); + }); +}); diff --git a/src/utils/build-utils.ts b/src/utils/build-utils.ts index 1415768ab..42faba86e 100644 --- a/src/utils/build-utils.ts +++ b/src/utils/build-utils.ts @@ -22,6 +22,10 @@ export interface BuildCommandResult { isError?: boolean; } +export interface BuildCommandExecutionOptions { + propagateInfrastructureErrors?: boolean; +} + function getDefaultSwiftPackageCachePath(): string { return path.join(os.homedir(), 'Library', 'Caches', 'org.swift.swiftpm'); } @@ -34,6 +38,7 @@ export async function executeXcodeBuildCommand( executor: CommandExecutor, execOpts?: CommandExecOptions, pipeline?: XcodebuildPipeline, + executionOptions?: BuildCommandExecutionOptions, ): Promise { function addBuildMessage(message: string, level: 'info' | 'success' = 'info'): void { pipeline?.emitFragment( @@ -249,6 +254,10 @@ export async function executeXcodeBuildCommand( return successResponse; } catch (error) { + if (executionOptions?.propagateInfrastructureErrors) { + throw error; + } + const errorMessage = error instanceof Error ? error.message : String(error); const isSpawnError = diff --git a/src/utils/log-paths.ts b/src/utils/log-paths.ts index b6faf7ff8..911b723b6 100644 --- a/src/utils/log-paths.ts +++ b/src/utils/log-paths.ts @@ -23,6 +23,7 @@ export interface WorkspaceFilesystemLayout { locks: string; derivedData: string; resultBundles: string; + testProducts: string; logRetention: LogRetentionPaths; filesystemLifecycle: WorkspaceFilesystemLifecyclePaths; simulatorLaunchOsLogRegistryDir: string; @@ -58,6 +59,7 @@ export function getWorkspaceFilesystemLayout(workspaceKey: string): WorkspaceFil const locks = path.join(root, 'locks'); const derivedData = path.join(root, 'DerivedData'); const resultBundles = path.join(root, 'result-bundles'); + const testProducts = path.join(root, 'test-products'); return { workspaceKey: normalizedWorkspaceKey, @@ -67,6 +69,7 @@ export function getWorkspaceFilesystemLayout(workspaceKey: string): WorkspaceFil locks, derivedData, resultBundles, + testProducts, logRetention: { lockDir: path.join(locks, 'log-retention.lock'), markerPath: path.join(state, 'log-retention', 'last-cleanup'), diff --git a/src/utils/purge-storage/enumerate.ts b/src/utils/purge-storage/enumerate.ts index fdc330fef..36bf83185 100644 --- a/src/utils/purge-storage/enumerate.ts +++ b/src/utils/purge-storage/enumerate.ts @@ -34,6 +34,8 @@ function classPathForWorkspace(workspaceKey: string, storageClass: PurgeStorageC return layout.logs; case 'resultBundles': return layout.resultBundles; + case 'testProducts': + return layout.testProducts; case 'stateTransients': return layout.state; case 'locks': diff --git a/src/utils/purge-storage/execution.ts b/src/utils/purge-storage/execution.ts index 6e89c52d3..a22bbccd8 100644 --- a/src/utils/purge-storage/execution.ts +++ b/src/utils/purge-storage/execution.ts @@ -8,6 +8,12 @@ import { getResultBundleCompletionMarkerPath, isResultBundleCompletionMarkerTempName, } from '../result-bundle-path.ts'; +import { + getTestProductsCompletionMarkerPath, + isTestProductsCompletionMarkerTempName, + isXcodeBuildMCPManagedTestProductsName, +} from '../test-products-path.ts'; +import { isProtectedManagedTestProducts } from '../test-products-lifecycle.ts'; import { WORKSPACE_FILESYSTEM_LIFECYCLE_LOCK_LEASE_MS, WORKSPACE_FILESYSTEM_LIFECYCLE_MIN_VISIBLE_MS, @@ -62,6 +68,8 @@ function deletionRootForClass( return layout.logs; case 'resultBundles': return layout.resultBundles; + case 'testProducts': + return layout.testProducts; case 'stateTransients': return layout.state; } @@ -165,6 +173,24 @@ async function validateClassSpecificDeletionCandidate( return 'result bundle candidate is protected by active lifecycle owner'; } return null; + case 'testProducts': + if (isTestProductsCompletionMarkerTempName(name)) { + return candidate.kind === 'file' + ? null + : 'test products temp marker candidate is not a file'; + } + if (!isXcodeBuildMCPManagedTestProductsName(name)) { + return 'test products candidate is not managed'; + } + if ( + await isProtectedManagedTestProducts( + { name, path: candidate.path, mtimeMs: candidate.mtimeMs }, + { now, minVisibleMs: 0 }, + ) + ) { + return 'test products candidate is protected by active lifecycle owner'; + } + return null; case 'stateTransients': return validateStateTransientCandidate(candidate); } @@ -220,15 +246,23 @@ async function validateSidecarPath( candidate: PurgeStorageCandidate, sidecarPath: string, ): Promise { - if (candidate.storageClass !== 'resultBundles') { - return 'sidecar path is not associated with a result bundle'; + const layout = getWorkspaceFilesystemLayout(candidate.workspaceKey); + const expectedSidecarPath = + candidate.storageClass === 'resultBundles' + ? getResultBundleCompletionMarkerPath(candidate.path) + : candidate.storageClass === 'testProducts' + ? getTestProductsCompletionMarkerPath(candidate.path) + : null; + if (expectedSidecarPath === null) { + return 'sidecar path is not associated with a managed artifact'; } - if (sidecarPath !== getResultBundleCompletionMarkerPath(candidate.path)) { + if (sidecarPath !== expectedSidecarPath) { return 'sidecar path does not match the candidate completion marker'; } - const layout = getWorkspaceFilesystemLayout(candidate.workspaceKey); - if (!pathIsInside(layout.resultBundles, sidecarPath)) { - return 'sidecar path is outside result-bundles'; + const sidecarRoot = + candidate.storageClass === 'resultBundles' ? layout.resultBundles : layout.testProducts; + if (!pathIsInside(sidecarRoot, sidecarPath)) { + return 'sidecar path is outside the managed artifact root'; } try { await fs.lstat(sidecarPath); diff --git a/src/utils/purge-storage/planning.ts b/src/utils/purge-storage/planning.ts index f6bec9a69..b57003041 100644 --- a/src/utils/purge-storage/planning.ts +++ b/src/utils/purge-storage/planning.ts @@ -21,6 +21,7 @@ import { import { enumeratePurgeStorage } from './enumerate.ts'; import { readRegistryRecord } from './registry-record.ts'; import { describeFsError, errorMessage, isEnoent, scanPath } from './scan.ts'; +import { collectTestProductsCandidates } from './test-products.ts'; import type { PlanPurgeStorageOptions, PurgeStorageCandidate, @@ -461,6 +462,8 @@ async function collectCandidatesForWorkspaceClass( return collectLogCandidates(workspaceKey, options); case 'resultBundles': return collectResultBundleCandidates(workspaceKey, options); + case 'testProducts': + return collectTestProductsCandidates(workspaceKey, options); case 'stateTransients': return collectStateTransientCandidates(workspaceKey, options); } diff --git a/src/utils/purge-storage/test-products.ts b/src/utils/purge-storage/test-products.ts new file mode 100644 index 000000000..5104d50bd --- /dev/null +++ b/src/utils/purge-storage/test-products.ts @@ -0,0 +1,155 @@ +import type { Dirent, Stats } from 'node:fs'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { getWorkspaceFilesystemLayout } from '../log-paths.ts'; +import { + getTestProductsCompletionMarkerPath, + isTestProductsCompletionMarkerTempName, + isXcodeBuildMCPManagedTestProductsName, +} from '../test-products-path.ts'; +import { isProtectedManagedTestProducts } from '../test-products-lifecycle.ts'; +import { describeFsError, errorMessage, isEnoent, scanPath } from './scan.ts'; +import type { + PurgeStorageCandidate, + PurgeStorageSkippedCandidate, + RequiredPlanContext, +} from './types.ts'; + +type PlannedTestProductsCandidate = PurgeStorageCandidate | PurgeStorageSkippedCandidate; + +function skipped( + workspaceKey: string, + candidatePath: string, + reason: string, +): PurgeStorageSkippedCandidate { + return { workspaceKey, storageClass: 'testProducts', path: candidatePath, reason }; +} + +async function readManagedDir( + directory: string, +): Promise<{ entries: Dirent[]; error: string | null }> { + try { + const stat = await fs.lstat(directory); + if (!stat.isDirectory() || stat.isSymbolicLink()) { + return { entries: [], error: null }; + } + return { entries: await fs.readdir(directory, { withFileTypes: true }), error: null }; + } catch (error) { + return { entries: [], error: isEnoent(error) ? null : errorMessage(error) }; + } +} + +async function candidateFromPath(params: { + workspaceKey: string; + candidatePath: string; + reason: string; + now: number; + olderThanMs?: number; + sidecarPaths?: string[]; +}): Promise { + let stat: Stats; + try { + stat = await fs.lstat(params.candidatePath); + } catch (error) { + return skipped( + params.workspaceKey, + params.candidatePath, + describeFsError(error, 'path disappeared during planning'), + ); + } + if (stat.isSymbolicLink() || (!stat.isFile() && !stat.isDirectory())) { + return skipped( + params.workspaceKey, + params.candidatePath, + 'non-regular filesystem entry skipped', + ); + } + + const scan = await scanPath(params.candidatePath); + const retentionMtimeMs = stat.isDirectory() + ? (scan.latestFileMtimeMs ?? scan.latestDirectoryMtimeMs ?? stat.mtimeMs) + : stat.mtimeMs; + if (params.olderThanMs !== undefined && params.now - retentionMtimeMs <= params.olderThanMs) { + return skipped( + params.workspaceKey, + params.candidatePath, + `newer than retention filter (${params.olderThanMs}ms)`, + ); + } + + return { + workspaceKey: params.workspaceKey, + storageClass: 'testProducts', + path: params.candidatePath, + kind: stat.isDirectory() ? 'directory' : 'file', + bytes: scan.bytes, + fileCount: scan.fileCount, + directoryCount: scan.directoryCount, + mtimeMs: stat.mtimeMs, + reason: params.reason, + sidecarPaths: params.sidecarPaths ?? [], + }; +} + +export async function collectTestProductsCandidates( + workspaceKey: string, + options: RequiredPlanContext, +): Promise { + const testProductsDir = getWorkspaceFilesystemLayout(workspaceKey).testProducts; + const { entries, error } = await readManagedDir(testProductsDir); + const planned: PlannedTestProductsCandidate[] = error + ? [skipped(workspaceKey, testProductsDir, `directory unreadable; skipped (${error})`)] + : []; + + for (const entry of entries) { + const candidatePath = path.join(testProductsDir, entry.name); + if (entry.isFile() && isTestProductsCompletionMarkerTempName(entry.name)) { + planned.push( + await candidateFromPath({ + workspaceKey, + candidatePath, + reason: 'orphan test products completion temp marker', + ...options, + }), + ); + continue; + } + if (!entry.isDirectory() || !isXcodeBuildMCPManagedTestProductsName(entry.name)) { + continue; + } + + let stat: Stats; + try { + stat = await fs.lstat(candidatePath); + } catch (statError) { + planned.push( + skipped( + workspaceKey, + candidatePath, + describeFsError(statError, 'path disappeared during planning'), + ), + ); + continue; + } + if ( + await isProtectedManagedTestProducts( + { name: entry.name, path: candidatePath, mtimeMs: stat.mtimeMs }, + { now: options.now, minVisibleMs: 0 }, + ) + ) { + planned.push(skipped(workspaceKey, candidatePath, 'protected by active lifecycle owner')); + continue; + } + + planned.push( + await candidateFromPath({ + workspaceKey, + candidatePath, + reason: 'managed test products', + sidecarPaths: [getTestProductsCompletionMarkerPath(candidatePath)], + ...options, + }), + ); + } + return planned; +} diff --git a/src/utils/purge-storage/types.ts b/src/utils/purge-storage/types.ts index 993901034..a79f17e45 100644 --- a/src/utils/purge-storage/types.ts +++ b/src/utils/purge-storage/types.ts @@ -2,6 +2,7 @@ export const PURGE_STORAGE_CLASSES = [ 'derivedData', 'logs', 'resultBundles', + 'testProducts', 'stateTransients', 'locks', ] as const; @@ -10,6 +11,7 @@ export const PURGE_STORAGE_DELETABLE_CLASSES = [ 'derivedData', 'logs', 'resultBundles', + 'testProducts', 'stateTransients', ] as const; diff --git a/src/utils/renderers/__tests__/domain-result-text.test.ts b/src/utils/renderers/__tests__/domain-result-text.test.ts index ca9f7e490..f57b27e95 100644 --- a/src/utils/renderers/__tests__/domain-result-text.test.ts +++ b/src/utils/renderers/__tests__/domain-result-text.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import type { BuildResultDomainResult, LaunchResultDomainResult, + TestResultDomainResult, UiActionResultDomainResult, } from '../../../types/domain-results.ts'; import { renderDomainResultTextItems } from '../domain-result-text.ts'; @@ -151,6 +152,46 @@ describe('renderDomainResultTextItems', () => { expect(titles).toContain('Errors (1):'); }); + it('renders prepared test artifacts in build and test results', () => { + const buildResult: BuildResultDomainResult = { + kind: 'build-result', + didError: false, + error: null, + summary: { status: 'SUCCEEDED' }, + artifacts: { + testProductsPath: 'artifacts/App.xctestproducts', + xctestrunPaths: ['artifacts/App.xctestproducts/App.xctestrun'], + }, + diagnostics: { warnings: [], errors: [] }, + }; + const testResult: TestResultDomainResult = { + kind: 'test-result', + didError: false, + error: null, + summary: { status: 'SUCCEEDED' }, + artifacts: { + testProductsPath: 'artifacts/App.xctestproducts', + xcresultPath: 'artifacts/test.xcresult', + }, + diagnostics: { warnings: [], errors: [], testFailures: [] }, + }; + + expect(renderDomainResultTextItems(buildResult)).toContainEqual({ + type: 'detail-tree', + items: [ + { label: 'Test Products', path: 'artifacts/App.xctestproducts' }, + { label: 'XCTest Run', path: 'artifacts/App.xctestproducts/App.xctestrun' }, + ], + }); + expect(renderDomainResultTextItems(testResult)).toContainEqual({ + type: 'detail-tree', + items: [ + { label: 'Result Bundle', path: 'artifacts/test.xcresult' }, + { label: 'Test Products', path: 'artifacts/App.xctestproducts' }, + ], + }); + }); + it('renders batch UI action results', () => { expect( renderDomainResultTextItems( diff --git a/src/utils/renderers/domain-result-text.ts b/src/utils/renderers/domain-result-text.ts index dfc8f2818..b325d4cb5 100644 --- a/src/utils/renderers/domain-result-text.ts +++ b/src/utils/renderers/domain-result-text.ts @@ -2247,6 +2247,17 @@ export function createBuildLikeTailItems(result: ToolDomainResult): TextRenderab if ('buildLogPath' in result.artifacts && typeof result.artifacts.buildLogPath === 'string') { items.push(createPathDetailItem('Build Logs', result.artifacts.buildLogPath)); } + if ( + 'testProductsPath' in result.artifacts && + typeof result.artifacts.testProductsPath === 'string' + ) { + items.push(createPathDetailItem('Test Products', result.artifacts.testProductsPath)); + } + if ('xctestrunPaths' in result.artifacts && Array.isArray(result.artifacts.xctestrunPaths)) { + for (const xctestrunPath of result.artifacts.xctestrunPaths) { + items.push(createPathDetailItem('XCTest Run', xctestrunPath)); + } + } return items.length > 0 ? [createDetailTree(items)] : []; } case 'build-run-result': { @@ -2307,6 +2318,18 @@ export function createBuildLikeTailItems(result: ToolDomainResult): TextRenderab if ('buildLogPath' in result.artifacts && typeof result.artifacts.buildLogPath === 'string') { items.push(createPathDetailItem('Build Logs', result.artifacts.buildLogPath)); } + if ( + 'testProductsPath' in result.artifacts && + typeof result.artifacts.testProductsPath === 'string' + ) { + items.push(createPathDetailItem('Test Products', result.artifacts.testProductsPath)); + } + if ( + 'xctestrunPath' in result.artifacts && + typeof result.artifacts.xctestrunPath === 'string' + ) { + items.push(createPathDetailItem('XCTest Run', result.artifacts.xctestrunPath)); + } return items.length > 0 ? [createDetailTree(items)] : []; } default: diff --git a/src/utils/test-common.ts b/src/utils/test-common.ts index c3bffc460..bafee6513 100644 --- a/src/utils/test-common.ts +++ b/src/utils/test-common.ts @@ -4,9 +4,9 @@ * This module provides shared functionality for all xcodebuild-backed test tools across platforms. */ +import * as path from 'node:path'; import { log } from './logger.ts'; -import { toErrorMessage } from './errors.ts'; -import type { XcodePlatform } from './xcode.ts'; +import { constructDestinationString, XcodePlatform } from './xcode.ts'; import { executeXcodeBuildCommand } from './build/index.ts'; import { extractTestFailuresFromXcresult } from './xcresult-test-failures.ts'; @@ -21,6 +21,12 @@ import { createDefaultResultBundlePath, markResultBundlePathCompleted, } from './result-bundle-path.ts'; +import { + createDefaultTestProductsPath, + markTestProductsPathCompleted, +} from './test-products-path.ts'; +import { resolvePathFromCwd } from './path.ts'; +import { displayPath } from './build-preflight.ts'; import type { BuildTarget, @@ -68,11 +74,36 @@ function createXcodebuildTestArtifacts( params: Pick, started: ReturnType, xcresultPath?: string, -): TestResultArtifacts { + preparedTestSource?: Pick, +): TestResultArtifacts & { testProductsPath?: string; xctestrunPath?: string } { return { ...(params.deviceId ? { deviceId: params.deviceId } : {}), buildLogPath: started.pipeline.logPath, ...(xcresultPath ? { xcresultPath } : {}), + ...(preparedTestSource?.testProductsPath + ? { testProductsPath: displayPath(preparedTestSource.testProductsPath) } + : {}), + ...(preparedTestSource?.xctestrunPath + ? { xctestrunPath: displayPath(preparedTestSource.xctestrunPath) } + : {}), + }; +} + +function createDisplayedTestDomainResult( + options: Parameters[0], +): TestResultDomainResult { + const result = createTestDomainResult(options); + return { + ...result, + artifacts: { + ...result.artifacts, + ...(result.artifacts.buildLogPath + ? { buildLogPath: displayPath(result.artifacts.buildLogPath) } + : {}), + ...(result.artifacts.xcresultPath + ? { xcresultPath: displayPath(result.artifacts.xcresultPath) } + : {}), + }, }; } @@ -83,7 +114,7 @@ export function resolveTestProgressEnabled(progress: boolean | undefined): boole export interface SharedTestExecutorParams { workspacePath?: string; projectPath?: string; - scheme: string; + scheme?: string; configuration?: string; simulatorName?: string; simulatorId?: string; @@ -96,6 +127,8 @@ export interface SharedTestExecutorParams { platform: XcodePlatform; testRunnerEnv?: Record; progress?: boolean; + testProductsPath?: string; + xctestrunPath?: string; } export interface SharedTestExecutorOptions { @@ -105,6 +138,86 @@ export interface SharedTestExecutorOptions { request: BuildInvocationRequest; } +function createPreparedTestDestination(params: SharedTestExecutorParams): string | undefined { + if (String(params.platform).includes('Simulator')) { + return constructDestinationString( + params.platform, + params.simulatorName, + params.simulatorId, + params.useLatestOS, + ); + } + if (params.platform === XcodePlatform.macOS) { + return constructDestinationString(params.platform); + } + if (params.deviceId) { + return `platform=${String(params.platform)},id=${params.deviceId}`; + } + return undefined; +} + +function resolveSourceWorkingDirectory(params: SharedTestExecutorParams): string | undefined { + const sourcePath = params.workspacePath ?? params.projectPath; + return sourcePath ? path.dirname(resolvePathFromCwd(sourcePath)) : undefined; +} + +async function executePreparedTestCommand( + params: SharedTestExecutorParams, + extraArgs: string[], + resultBundlePath: string, + executor: CommandExecutor, + execOpts: CommandExecOptions | undefined, + pipeline: ReturnType['pipeline'], +): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> { + const sourceArgs = params.testProductsPath + ? ['-testProductsPath', resolvePathFromCwd(params.testProductsPath)] + : params.xctestrunPath + ? ['-xctestrun', resolvePathFromCwd(params.xctestrunPath)] + : []; + const destination = createPreparedTestDestination(params); + if (sourceArgs.length === 0) { + return { + content: [{ type: 'text', text: 'A prepared test artifact is required.' }], + isError: true, + }; + } + if (!destination) { + return { + content: [{ type: 'text', text: 'A destination is required to run prepared tests.' }], + isError: true, + }; + } + + const command = [ + 'xcodebuild', + ...sourceArgs, + '-destination', + destination, + '-collect-test-diagnostics', + 'never', + ...extraArgs, + '-resultBundlePath', + resultBundlePath, + 'test-without-building', + ]; + const sourceWorkingDirectory = resolveSourceWorkingDirectory(params); + const response = await executor(command, 'Test Run', false, { + ...execOpts, + ...(sourceWorkingDirectory ? { cwd: sourceWorkingDirectory } : {}), + onStdout: (chunk) => pipeline.onStdout(chunk), + onStderr: (chunk) => pipeline.onStderr(chunk), + }); + + return response.success + ? { content: [{ type: 'text', text: 'Test Run test-without-building succeeded.' }] } + : { + content: [{ type: 'text', text: 'Test Run test-without-building failed.' }], + isError: true, + }; +} + +type PreparedTestCommandResult = Awaited>; + export function createTestExecutor( executor: CommandExecutor = getDefaultCommandExecutor(), options: SharedTestExecutorOptions, @@ -112,14 +225,13 @@ export function createTestExecutor( return async (params, ctx) => { log( 'info', - `Starting test run for scheme ${params.scheme} on platform ${params.platform} (executor)`, + `Starting test run for ${params.scheme ? `scheme ${params.scheme}` : 'prepared tests'} on platform ${params.platform} (executor)`, ); const execOpts: CommandExecOptions | undefined = params.testRunnerEnv ? { env: normalizeTestRunnerEnv(params.testRunnerEnv) } : undefined; - const shouldUseTwoPhaseSimulatorExecution = - String(params.platform).includes('Simulator') && Boolean(options.preflight); + const hasPreparedTestSource = Boolean(params.testProductsPath ?? params.xctestrunPath); const toolName = options.toolName ?? 'test_sim'; const target = options.target ?? getBuildTarget(params.platform); const started = createDomainStreamingPipeline(toolName, 'TEST', ctx, 'test-result'); @@ -138,127 +250,134 @@ export function createTestExecutor( started.pipeline.emitFragment(discoveryEvent); } - try { - const parsedResultBundleArgs = parseResultBundlePathArgs(params.extraArgs); - const shouldUseDefaultResultBundlePath = !parsedResultBundleArgs.resultBundlePath; - const resultBundlePath = - parsedResultBundleArgs.resultBundlePath ?? createDefaultResultBundlePath(toolName); + const parsedResultBundleArgs = parseResultBundlePathArgs(params.extraArgs); + const shouldUseDefaultResultBundlePath = !parsedResultBundleArgs.resultBundlePath; + const resultBundlePath = + parsedResultBundleArgs.resultBundlePath ?? createDefaultResultBundlePath(toolName); - if (shouldUseTwoPhaseSimulatorExecution) { - const executionPlan = createSimulatorTwoPhaseExecutionPlan({ - extraArgs: params.extraArgs, - preflight: options.preflight, - resultBundlePath, - }); + if (!hasPreparedTestSource) { + const testProductsPath = createDefaultTestProductsPath(toolName); + const executionPlan = createSimulatorTwoPhaseExecutionPlan({ + extraArgs: parsedResultBundleArgs.remainingArgs, + preflight: options.preflight, + }); - const buildForTestingResult = await executeXcodeBuildCommand( - { ...params, extraArgs: executionPlan.buildArgs }, + let buildForTestingResult: Awaited>; + try { + buildForTestingResult = await executeXcodeBuildCommand( + { + ...params, + scheme: params.scheme!, + extraArgs: [...executionPlan.buildArgs, '-testProductsPath', testProductsPath], + }, platformOptions, params.preferXcodebuild, 'build-for-testing', executor, execOpts, started.pipeline, + { propagateInfrastructureErrors: true }, ); + } catch (error) { + markTestProductsPathCompleted(testProductsPath); + throw error; + } - if (buildForTestingResult.isError) { - return createTestDomainResult({ - started, - succeeded: false, - target, - artifacts: createXcodebuildTestArtifacts(params, started), - fallbackErrorMessages: getFallbackErrorMessages( - started.stderrLines, - buildForTestingResult.content, - ), - includeDetectedXcresult: false, - preflight: options.preflight, - request: options.request, - }); - } - - started.pipeline.emitFragment({ - kind: 'test-result', - fragment: 'build-stage', - operation: 'TEST', - stage: 'RUN_TESTS', - message: 'Running tests', - }); - - const testWithoutBuildingResult = await executeXcodeBuildCommand( - { ...params, extraArgs: executionPlan.testArgs }, - platformOptions, - params.preferXcodebuild, - 'test-without-building', - executor, - execOpts, - started.pipeline, - ); - - if (shouldUseDefaultResultBundlePath) { - markResultBundlePathCompleted(executionPlan.resultBundlePath); - } - emitXcresultFailures(started.pipeline); - - return createTestDomainResult({ + if (buildForTestingResult.isError) { + markTestProductsPathCompleted(testProductsPath); + return createDisplayedTestDomainResult({ started, - succeeded: !testWithoutBuildingResult.isError, + succeeded: false, target, - artifacts: createXcodebuildTestArtifacts(params, started, executionPlan.resultBundlePath), + artifacts: createXcodebuildTestArtifacts(params, started), fallbackErrorMessages: getFallbackErrorMessages( started.stderrLines, - testWithoutBuildingResult.content, + buildForTestingResult.content, ), + includeDetectedXcresult: false, preflight: options.preflight, request: options.request, }); } - const singlePhaseParams: SharedTestExecutorParams = { - ...params, - extraArgs: [...parsedResultBundleArgs.remainingArgs, '-resultBundlePath', resultBundlePath], - }; - - const singlePhaseResult = await executeXcodeBuildCommand( - singlePhaseParams, - platformOptions, - params.preferXcodebuild, - 'test', - executor, - execOpts, - started.pipeline, - ); + started.pipeline.emitFragment({ + kind: 'test-result', + fragment: 'build-stage', + operation: 'TEST', + stage: 'RUN_TESTS', + message: 'Running tests', + }); - if (shouldUseDefaultResultBundlePath) { - markResultBundlePathCompleted(resultBundlePath); + let testWithoutBuildingResult: PreparedTestCommandResult; + try { + testWithoutBuildingResult = await executePreparedTestCommand( + { ...params, testProductsPath }, + executionPlan.testArgs, + resultBundlePath, + executor, + execOpts, + started.pipeline, + ); + } finally { + markTestProductsPathCompleted(testProductsPath); + if (shouldUseDefaultResultBundlePath) { + markResultBundlePathCompleted(resultBundlePath); + } } emitXcresultFailures(started.pipeline); - return createTestDomainResult({ + return createDisplayedTestDomainResult({ started, - succeeded: !singlePhaseResult.isError, + succeeded: !testWithoutBuildingResult.isError, target, - artifacts: createXcodebuildTestArtifacts(params, started, resultBundlePath), + artifacts: createXcodebuildTestArtifacts(params, started, resultBundlePath, { + testProductsPath, + }), fallbackErrorMessages: getFallbackErrorMessages( started.stderrLines, - singlePhaseResult.content, + testWithoutBuildingResult.content, ), preflight: options.preflight, request: options.request, }); - } catch (error) { - const errorMessage = toErrorMessage(error); - log('error', `Error during test run: ${errorMessage}`); + } - return createTestDomainResult({ - started, - succeeded: false, - target, - artifacts: createXcodebuildTestArtifacts(params, started), - fallbackErrorMessages: [...started.stderrLines, errorMessage], - preflight: options.preflight, - request: options.request, - }); + started.pipeline.emitFragment({ + kind: 'test-result', + fragment: 'build-stage', + operation: 'TEST', + stage: 'RUN_TESTS', + message: 'Running tests', + }); + + let preparedTestResult: PreparedTestCommandResult; + try { + preparedTestResult = await executePreparedTestCommand( + params, + parsedResultBundleArgs.remainingArgs, + resultBundlePath, + executor, + execOpts, + started.pipeline, + ); + } finally { + if (shouldUseDefaultResultBundlePath) { + markResultBundlePathCompleted(resultBundlePath); + } } + emitXcresultFailures(started.pipeline); + + return createDisplayedTestDomainResult({ + started, + succeeded: !preparedTestResult.isError, + target, + artifacts: createXcodebuildTestArtifacts(params, started, resultBundlePath, params), + fallbackErrorMessages: getFallbackErrorMessages( + started.stderrLines, + preparedTestResult.content, + ), + preflight: options.preflight, + request: options.request, + }); }; } diff --git a/src/utils/test-products-lifecycle.ts b/src/utils/test-products-lifecycle.ts new file mode 100644 index 000000000..c03ddcaaf --- /dev/null +++ b/src/utils/test-products-lifecycle.ts @@ -0,0 +1,111 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { isPidAlive } from './process-liveness.ts'; +import { + getManagedTestProductsOwnerPid, + getTestProductsCompletionMarkerPath, + isXcodeBuildMCPManagedTestProductsName, +} from './test-products-path.ts'; + +export const TEST_PRODUCTS_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000; +export const TEST_PRODUCTS_MAX_COUNT = 100; + +interface RetainedTestProducts { + path: string; + name: string; + mtimeMs: number; +} + +export interface TestProductsProtectionOptions { + now: number; + minVisibleMs: number; +} + +export interface PruneManagedTestProductsOptions extends TestProductsProtectionOptions { + testProductsDir: string; + maxAgeMs?: number; + maxCount?: number; +} + +async function hasCompletionMarker(testProductsPath: string): Promise { + try { + return (await fs.stat(getTestProductsCompletionMarkerPath(testProductsPath))).isFile(); + } catch { + return false; + } +} + +export async function isProtectedManagedTestProducts( + artifact: RetainedTestProducts, + options: TestProductsProtectionOptions, +): Promise { + if (options.now - artifact.mtimeMs < options.minVisibleMs) { + return true; + } + + const ownerPid = getManagedTestProductsOwnerPid(artifact.name); + return Boolean(ownerPid && isPidAlive(ownerPid) && !(await hasCompletionMarker(artifact.path))); +} + +export async function pruneManagedTestProductsDirectory( + options: PruneManagedTestProductsOptions, +): Promise<{ scanned: number; deleted: number }> { + await fs.mkdir(options.testProductsDir, { recursive: true, mode: 0o700 }); + const entries = await fs.readdir(options.testProductsDir, { withFileTypes: true }); + const candidates = entries + .filter((entry) => entry.isDirectory() && isXcodeBuildMCPManagedTestProductsName(entry.name)) + .map((entry) => ({ + name: entry.name, + path: path.join(options.testProductsDir, entry.name), + })); + const stats = await Promise.all( + candidates.map(async (candidate) => { + try { + return { + ...candidate, + mtimeMs: (await fs.stat(candidate.path)).mtimeMs, + } satisfies RetainedTestProducts; + } catch { + return null; + } + }), + ); + + const retained: RetainedTestProducts[] = []; + const expired: RetainedTestProducts[] = []; + for (const artifact of stats) { + if (!artifact || (await isProtectedManagedTestProducts(artifact, options))) { + continue; + } + if (options.now - artifact.mtimeMs > (options.maxAgeMs ?? TEST_PRODUCTS_MAX_AGE_MS)) { + expired.push(artifact); + } else { + retained.push(artifact); + } + } + + const excessCount = retained.length - (options.maxCount ?? TEST_PRODUCTS_MAX_COUNT); + const overflow = + excessCount > 0 + ? retained + .slice() + .sort((left, right) => left.mtimeMs - right.mtimeMs) + .slice(0, excessCount) + : []; + const deletions = await Promise.all( + [...expired, ...overflow].map(async (artifact) => { + try { + await fs.rm(artifact.path, { recursive: true, force: true }); + await fs.rm(getTestProductsCompletionMarkerPath(artifact.path), { force: true }); + return true; + } catch { + return false; + } + }), + ); + + return { + scanned: stats.filter((artifact) => artifact !== null).length, + deleted: deletions.filter(Boolean).length, + }; +} diff --git a/src/utils/test-products-path.ts b/src/utils/test-products-path.ts new file mode 100644 index 000000000..2287cf1c2 --- /dev/null +++ b/src/utils/test-products-path.ts @@ -0,0 +1,113 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { getWorkspaceFilesystemLayout } from './log-paths.ts'; +import { formatLogTimestamp, shortRandomSuffix } from './log-naming.ts'; +import { log } from './logger.ts'; +import { getRuntimeInstanceIfConfigured } from './runtime-instance.ts'; +import { workspaceKeyForRoot } from './workspace-identity.ts'; + +export const TEST_PRODUCTS_COMPLETION_MARKER_SUFFIX = '.xcodebuildmcp-completed'; + +const ISO_TIMESTAMP_PATTERN = '\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z'; +const SUFFIX_PATTERN = '[a-f0-9]{8}'; +const TEST_PRODUCTS_NAME_PATTERN = new RegExp( + `^[A-Za-z0-9][A-Za-z0-9_-]*_${ISO_TIMESTAMP_PATTERN}_pid\\d+_${SUFFIX_PATTERN}\\.xctestproducts$`, +); +const TEST_PRODUCTS_OWNER_PID_PATTERN = /_pid(\d+)_/u; + +function resolveWorkspaceKey(): string { + return getRuntimeInstanceIfConfigured()?.workspaceKey ?? workspaceKeyForRoot(process.cwd()); +} + +export function isXcodeBuildMCPManagedTestProductsName(fileName: string): boolean { + return TEST_PRODUCTS_NAME_PATTERN.test(fileName); +} + +export function getManagedTestProductsOwnerPid(fileName: string): number | null { + const pid = Number(fileName.match(TEST_PRODUCTS_OWNER_PID_PATTERN)?.[1]); + return Number.isInteger(pid) && pid > 0 ? pid : null; +} + +export function getTestProductsCompletionMarkerPath(testProductsPath: string): string { + return `${testProductsPath}${TEST_PRODUCTS_COMPLETION_MARKER_SUFFIX}`; +} + +export function isTestProductsCompletionMarkerTempName(name: string): boolean { + return name.includes(`${TEST_PRODUCTS_COMPLETION_MARKER_SUFFIX}.`) && name.endsWith('.tmp'); +} + +export function createDefaultTestProductsPath(toolName: string): string { + const testProductsDir = getWorkspaceFilesystemLayout(resolveWorkspaceKey()).testProducts; + + try { + fs.mkdirSync(testProductsDir, { recursive: true, mode: 0o700 }); + fs.accessSync(testProductsDir, fs.constants.W_OK); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `Unable to create writable test products directory at ${testProductsDir}: ${message}`, + { cause: error }, + ); + } + + return path.join( + testProductsDir, + `${toolName}_${formatLogTimestamp()}_pid${process.pid}_${shortRandomSuffix()}.xctestproducts`, + ); +} + +async function collectXctestrunPaths(directory: string, paths: string[]): Promise { + const entries = await fs.promises.readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isSymbolicLink()) { + continue; + } + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + await collectXctestrunPaths(entryPath, paths); + } else if (entry.isFile() && entry.name.endsWith('.xctestrun')) { + paths.push(entryPath); + } + } +} + +export async function findXctestrunPaths(testProductsPath: string): Promise { + try { + const stat = await fs.promises.lstat(testProductsPath); + if (!stat.isDirectory() || stat.isSymbolicLink()) { + return []; + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return []; + } + throw error; + } + const paths: string[] = []; + await collectXctestrunPaths(testProductsPath, paths); + return paths.sort((left, right) => left.localeCompare(right)); +} + +export function markTestProductsPathCompleted(testProductsPath: string | undefined): void { + if (!testProductsPath) { + return; + } + + try { + if (!fs.existsSync(testProductsPath) || !fs.statSync(testProductsPath).isDirectory()) { + return; + } + const markerPath = getTestProductsCompletionMarkerPath(testProductsPath); + const tempPath = `${markerPath}.${process.pid}_${shortRandomSuffix()}.tmp`; + fs.writeFileSync(tempPath, `${Date.now()}\n`); + try { + fs.renameSync(tempPath, markerPath); + } catch (renameError) { + fs.rmSync(tempPath, { force: true }); + throw renameError; + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log('warn', `Unable to mark test products completed at ${testProductsPath}: ${message}`); + } +} diff --git a/src/utils/test-source.ts b/src/utils/test-source.ts new file mode 100644 index 000000000..5fa231362 --- /dev/null +++ b/src/utils/test-source.ts @@ -0,0 +1,129 @@ +import * as z from 'zod'; + +type TestSourceParams = { + projectPath?: unknown; + workspacePath?: unknown; + scheme?: unknown; + configuration?: unknown; + derivedDataPath?: unknown; + testProductsPath?: unknown; + xctestrunPath?: unknown; + extraArgs?: unknown; +}; + +const conflictingPreparedTestArgKeys = new Set([ + '-configuration', + '-derivedDataPath', + '-destination', + '-project', + '-scheme', + '-testProductsPath', + '-workspace', + '-xctestrun', +]); + +const conflictingPreparedTestActions = new Set([ + 'build-for-testing', + 'test', + 'test-without-building', +]); + +function getArgumentKey(argument: string): string { + const separatorIndexes = [argument.indexOf(':'), argument.indexOf('=')].filter( + (index) => index !== -1, + ); + return separatorIndexes.length === 0 + ? argument + : argument.slice(0, Math.min(...separatorIndexes)); +} + +function getConflictingPreparedTestArgs(extraArgs: unknown): string[] { + if (!Array.isArray(extraArgs)) { + return []; + } + + return extraArgs.filter( + (argument): argument is string => + typeof argument === 'string' && + (conflictingPreparedTestArgKeys.has(getArgumentKey(argument)) || + conflictingPreparedTestActions.has(argument)), + ); +} + +export function hasPreparedTestSource(params: { + testProductsPath?: string; + xctestrunPath?: string; +}): boolean { + return params.testProductsPath !== undefined || params.xctestrunPath !== undefined; +} + +export function withProjectWorkspaceOrTestArtifact(baseObject: T): T { + return baseObject.superRefine((value: TestSourceParams, context) => { + const preparedSourceCount = + Number(value.testProductsPath !== undefined) + Number(value.xctestrunPath !== undefined); + + if (preparedSourceCount > 1) { + context.addIssue({ + code: 'custom', + message: 'testProductsPath and xctestrunPath are mutually exclusive. Provide only one.', + }); + return; + } + + if (preparedSourceCount === 1) { + const conflictingFields = [ + 'projectPath', + 'workspacePath', + 'scheme', + 'configuration', + 'derivedDataPath', + ].filter((key) => value[key as keyof TestSourceParams] !== undefined); + if (conflictingFields.length > 0) { + context.addIssue({ + code: 'custom', + message: `Prepared test artifacts cannot be combined with source inputs: ${conflictingFields.join(', ')}.`, + }); + } + + const conflictingArgs = getConflictingPreparedTestArgs(value.extraArgs); + if (conflictingArgs.length > 0) { + context.addIssue({ + code: 'custom', + path: ['extraArgs'], + message: `Prepared test artifacts cannot be combined with conflicting xcodebuild arguments: ${conflictingArgs.join(', ')}.`, + }); + } + return; + } + + if (value.scheme === undefined) { + context.addIssue({ code: 'custom', path: ['scheme'], message: 'scheme is required.' }); + } + + const sourceCount = + Number(value.projectPath !== undefined) + Number(value.workspacePath !== undefined); + if (sourceCount === 0) { + context.addIssue({ + code: 'custom', + message: 'Either projectPath or workspacePath is required.', + }); + } else if (sourceCount > 1) { + context.addIssue({ + code: 'custom', + message: 'projectPath and workspacePath are mutually exclusive. Provide only one.', + }); + } + }); +} + +export const TEST_SOURCE_EXCLUSIVE_GROUPS = [ + ['testProductsPath', 'xctestrunPath'], + ['testProductsPath', 'projectPath', 'workspacePath'], + ['xctestrunPath', 'projectPath', 'workspacePath'], + ['testProductsPath', 'scheme'], + ['xctestrunPath', 'scheme'], + ['testProductsPath', 'configuration'], + ['xctestrunPath', 'configuration'], + ['testProductsPath', 'derivedDataPath'], + ['xctestrunPath', 'derivedDataPath'], +] as const; diff --git a/src/utils/workspace-filesystem-lifecycle.ts b/src/utils/workspace-filesystem-lifecycle.ts index 0364bb3c6..998b09b84 100644 --- a/src/utils/workspace-filesystem-lifecycle.ts +++ b/src/utils/workspace-filesystem-lifecycle.ts @@ -15,6 +15,7 @@ import { getRuntimeInstance, getRuntimeInstanceIfConfigured } from './runtime-in import { tryAcquireFsLock, type AcquiredFsLock } from './fs-lock.ts'; import { isPidAlive } from './process-liveness.ts'; import { getResultBundleCompletionMarkerPath } from './result-bundle-path.ts'; +import { pruneManagedTestProductsDirectory } from './test-products-lifecycle.ts'; export const WORKSPACE_FILESYSTEM_LIFECYCLE_LOG_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000; export const WORKSPACE_FILESYSTEM_LIFECYCLE_LOG_MAX_FILES = 10_000; @@ -95,6 +96,7 @@ interface ResolvedWorkspaceFilesystemLifecycleOptions { markerPath: string; lockDir: string; resultBundleDir: string | null; + testProductsDir: string | null; now: number; maxAgeMs: number; maxFiles: number; @@ -174,6 +176,7 @@ function resolveOptions( layout?.filesystemLifecycle.lockDir ?? path.join(logDir, FALLBACK_LOCK_DIR_NAME), resultBundleDir: layout?.resultBundles ?? null, + testProductsDir: layout?.testProducts ?? null, now: options.now ?? Date.now(), maxAgeMs: options.maxAgeMs ?? WORKSPACE_FILESYSTEM_LIFECYCLE_LOG_MAX_AGE_MS, maxFiles: options.maxFiles ?? WORKSPACE_FILESYSTEM_LIFECYCLE_LOG_MAX_FILES, @@ -635,14 +638,21 @@ export async function runWorkspaceFilesystemLifecycleSweep( const protectedPaths = await collectProtectedLogPaths(resolved); const logPrune = await pruneKnownLogDirectory(resolved, protectedPaths); const resultBundlePrune = await pruneKnownResultBundleDirectory(resolved); + const testProductsPrune = resolved.testProductsDir + ? await pruneManagedTestProductsDirectory({ + testProductsDir: resolved.testProductsDir, + now: resolved.now, + minVisibleMs: resolved.minVisibleMs, + }) + : { scanned: 0, deleted: 0 }; await touchCleanupMarker(resolved.markerPath, resolved.now); return { workspaceKey: resolved.workspaceKey, trigger: resolved.trigger, logDir: resolved.logDir, - scanned: logPrune.scanned + resultBundlePrune.scanned, - deleted: logPrune.deleted + resultBundlePrune.deleted, + scanned: logPrune.scanned + resultBundlePrune.scanned + testProductsPrune.scanned, + deleted: logPrune.deleted + resultBundlePrune.deleted + testProductsPrune.deleted, stopped, skippedByCooldown: false, skippedByLock: false, diff --git a/src/utils/xcodebuild-domain-results.ts b/src/utils/xcodebuild-domain-results.ts index ba77deb18..e9ddae174 100644 --- a/src/utils/xcodebuild-domain-results.ts +++ b/src/utils/xcodebuild-domain-results.ts @@ -450,11 +450,12 @@ export function setXcodebuildStructuredOutput, + schemaVersion = '2', ): void { ctx.structuredOutput = { result, schema: XCODEBUILD_STRUCTURED_OUTPUT_SCHEMAS[kind], - schemaVersion: '2', + schemaVersion, }; } From 65b862b6a91806d2e6c1343455cbc37314e7761f Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 16 Jul 2026 16:54:17 +0100 Subject: [PATCH 02/23] feat(manifest): Support conditional next steps Move conditional next-step selection into tool manifests and preserve the behavior across MCP, CLI, daemon, and snapshot outputs. Refs #450 --- CHANGELOG.md | 5 +- manifests/tools/batch.yaml | 5 + manifests/tools/build_device.yaml | 10 ++ manifests/tools/build_macos.yaml | 10 ++ manifests/tools/build_sim.yaml | 10 ++ manifests/tools/button.yaml | 5 + manifests/tools/drag.yaml | 5 + manifests/tools/gesture.yaml | 5 + manifests/tools/key_press.yaml | 5 + manifests/tools/key_sequence.yaml | 5 + manifests/tools/long_press.yaml | 5 + manifests/tools/swipe.yaml | 5 + manifests/tools/tap.yaml | 5 + manifests/tools/touch.yaml | 5 + manifests/tools/type_text.yaml | 5 + src/core/manifest/__tests__/schema.test.ts | 37 +++++ src/core/manifest/schema.ts | 4 + .../__tests__/conditional-next-steps.test.ts | 128 ++++++++++++++++++ .../__tests__/tool-invoke-streaming.test.ts | 2 + src/daemon/daemon-server.ts | 1 + src/daemon/protocol.ts | 4 +- .../device/__tests__/build_device.test.ts | 3 + src/mcp/tools/device/build_device.ts | 11 +- .../tools/macos/__tests__/build_macos.test.ts | 2 + src/mcp/tools/macos/build_macos.ts | 11 +- .../simulator/__tests__/build_sim.test.ts | 2 + src/mcp/tools/simulator/build_sim.ts | 11 +- .../manifest-next-step-condition.test.ts | 37 +++++ .../ui-automation/shared/domain-result.ts | 20 +-- src/rendering/types.ts | 1 + .../__tests__/conditional-next-steps.test.ts | 124 +++++++++++++++++ src/runtime/tool-invoker.ts | 12 +- src/runtime/types.ts | 1 + .../json/device/build--error-compiler.json | 2 +- ...ld--error-prepared-tests-wrong-scheme.json | 33 +++++ .../device/build--error-wrong-scheme.json | 2 +- ...build--success-prepared-tests-generic.json | 33 +++++ .../cli/json/device/build--success.json | 2 +- .../cli/json/macos/build--error-compiler.json | 2 +- ...ld--error-prepared-tests-wrong-scheme.json | 33 +++++ .../json/macos/build--error-wrong-scheme.json | 2 +- .../macos/build--success-prepared-tests.json | 36 +++++ .../cli/json/macos/build--success.json | 2 +- .../cli/json/macos/test--error-compiler.json | 10 +- .../json/macos/test--error-wrong-scheme.json | 10 +- .../cli/json/macos/test--failure.json | 17 +-- .../cli/json/macos/test--success.json | 5 +- .../json/simulator/build--error-compiler.json | 2 +- ...ld--error-prepared-tests-wrong-scheme.json | 33 +++++ .../simulator/build--error-wrong-scheme.json | 2 +- .../build--success-prepared-tests.json | 36 +++++ .../cli/json/simulator/build--success.json | 2 +- .../json/simulator/test--error-compiler.json | 2 +- .../simulator/test--error-wrong-scheme.json | 2 +- .../cli/json/simulator/test--failure.json | 5 +- .../cli/json/simulator/test--success.json | 5 +- ...ild--error-prepared-tests-wrong-scheme.txt | 15 ++ .../build--success-prepared-tests-generic.txt | 13 ++ ...ild--error-prepared-tests-wrong-scheme.txt | 15 ++ .../macos/build--success-prepared-tests.txt | 16 +++ .../cli/text/macos/test--error-compiler.txt | 1 - .../text/macos/test--error-wrong-scheme.txt | 1 - .../cli/text/macos/test--failure.txt | 13 +- .../cli/text/macos/test--success.txt | 3 +- ...ild--error-prepared-tests-wrong-scheme.txt | 16 +++ .../build--success-prepared-tests.txt | 17 +++ .../cli/text/simulator/test--failure.txt | 3 +- .../cli/text/simulator/test--success.txt | 3 +- .../json/device/build--error-compiler.json | 2 +- ...ld--error-prepared-tests-wrong-scheme.json | 24 ++++ .../device/build--error-wrong-scheme.json | 2 +- ...build--success-prepared-tests-generic.json | 24 ++++ .../mcp/json/device/build--success.json | 2 +- .../mcp/json/macos/build--error-compiler.json | 2 +- ...ld--error-prepared-tests-wrong-scheme.json | 24 ++++ .../json/macos/build--error-wrong-scheme.json | 2 +- .../macos/build--success-prepared-tests.json | 27 ++++ .../mcp/json/macos/build--success.json | 2 +- .../mcp/json/macos/test--error-compiler.json | 10 +- .../json/macos/test--error-wrong-scheme.json | 10 +- .../mcp/json/macos/test--failure.json | 17 +-- .../mcp/json/macos/test--success.json | 5 +- .../json/simulator/build--error-compiler.json | 2 +- ...ld--error-prepared-tests-wrong-scheme.json | 24 ++++ .../simulator/build--error-wrong-scheme.json | 2 +- .../build--success-prepared-tests.json | 27 ++++ .../mcp/json/simulator/build--success.json | 2 +- .../json/simulator/test--error-compiler.json | 2 +- .../simulator/test--error-wrong-scheme.json | 2 +- .../mcp/json/simulator/test--failure.json | 5 +- .../mcp/json/simulator/test--success.json | 5 +- ...ild--error-prepared-tests-wrong-scheme.txt | 10 ++ .../build--success-prepared-tests-generic.txt | 8 ++ ...ild--error-prepared-tests-wrong-scheme.txt | 10 ++ .../macos/build--success-prepared-tests.txt | 11 ++ .../mcp/text/macos/test--error-compiler.txt | 4 +- .../text/macos/test--error-wrong-scheme.txt | 4 +- .../mcp/text/macos/test--failure.txt | 9 +- .../mcp/text/macos/test--success.txt | 3 +- ...ild--error-prepared-tests-wrong-scheme.txt | 10 ++ .../build--success-prepared-tests.txt | 11 ++ .../mcp/text/simulator/test--failure.txt | 3 +- .../mcp/text/simulator/test--success.txt | 3 +- .../__tests__/normalize.test.ts | 18 +++ src/snapshot-tests/harness.ts | 3 + src/snapshot-tests/normalize.ts | 9 +- src/snapshot-tests/suites/device-suite.ts | 34 +++++ src/snapshot-tests/suites/macos-suite.ts | 20 +++ src/snapshot-tests/suites/simulator-suite.ts | 38 ++++++ src/test-utils/test-helpers.ts | 4 + .../__tests__/snapshot-normalize.test.ts | 12 +- 111 files changed, 1182 insertions(+), 161 deletions(-) create mode 100644 src/daemon/__tests__/conditional-next-steps.test.ts create mode 100644 src/mcp/tools/ui-automation/__tests__/manifest-next-step-condition.test.ts create mode 100644 src/runtime/__tests__/conditional-next-steps.test.ts create mode 100644 src/snapshot-tests/__fixtures__/cli/json/device/build--error-prepared-tests-wrong-scheme.json create mode 100644 src/snapshot-tests/__fixtures__/cli/json/device/build--success-prepared-tests-generic.json create mode 100644 src/snapshot-tests/__fixtures__/cli/json/macos/build--error-prepared-tests-wrong-scheme.json create mode 100644 src/snapshot-tests/__fixtures__/cli/json/macos/build--success-prepared-tests.json create mode 100644 src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-prepared-tests-wrong-scheme.json create mode 100644 src/snapshot-tests/__fixtures__/cli/json/simulator/build--success-prepared-tests.json create mode 100644 src/snapshot-tests/__fixtures__/cli/text/device/build--error-prepared-tests-wrong-scheme.txt create mode 100644 src/snapshot-tests/__fixtures__/cli/text/device/build--success-prepared-tests-generic.txt create mode 100644 src/snapshot-tests/__fixtures__/cli/text/macos/build--error-prepared-tests-wrong-scheme.txt create mode 100644 src/snapshot-tests/__fixtures__/cli/text/macos/build--success-prepared-tests.txt create mode 100644 src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-prepared-tests-wrong-scheme.txt create mode 100644 src/snapshot-tests/__fixtures__/cli/text/simulator/build--success-prepared-tests.txt create mode 100644 src/snapshot-tests/__fixtures__/mcp/json/device/build--error-prepared-tests-wrong-scheme.json create mode 100644 src/snapshot-tests/__fixtures__/mcp/json/device/build--success-prepared-tests-generic.json create mode 100644 src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-prepared-tests-wrong-scheme.json create mode 100644 src/snapshot-tests/__fixtures__/mcp/json/macos/build--success-prepared-tests.json create mode 100644 src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-prepared-tests-wrong-scheme.json create mode 100644 src/snapshot-tests/__fixtures__/mcp/json/simulator/build--success-prepared-tests.json create mode 100644 src/snapshot-tests/__fixtures__/mcp/text/device/build--error-prepared-tests-wrong-scheme.txt create mode 100644 src/snapshot-tests/__fixtures__/mcp/text/device/build--success-prepared-tests-generic.txt create mode 100644 src/snapshot-tests/__fixtures__/mcp/text/macos/build--error-prepared-tests-wrong-scheme.txt create mode 100644 src/snapshot-tests/__fixtures__/mcp/text/macos/build--success-prepared-tests.txt create mode 100644 src/snapshot-tests/__fixtures__/mcp/text/simulator/build--error-prepared-tests-wrong-scheme.txt create mode 100644 src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success-prepared-tests.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 352993bd3..ce6b1b3f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - Added `extraArgs` as a first-class session-default value. Repo config or runtime defaults can now carry common `xcodebuild` flags (for example `-skipPackagePluginValidation` or `-disableAutomaticPackageResolution`) so they don't need repeating on every build or test call. Per-call `extraArgs` replace matching configured flags or build settings and append after non-matching defaults, while an explicit empty array (`extraArgs: []`) clears the defaults for a single call. The session management tools show, set, sync, and clear `extraArgs` alongside the other defaults. - Added reusable test preparation to the existing simulator, device, and macOS build tools. Builds can now return a portable `.xctestproducts` artifact, and test tools can run that artifact—or an advanced `.xctestrun` input—without rebuilding before producing a new `.xcresult` ([#450](https://github.com/getsentry/XcodeBuildMCP/issues/450)). +### Changed + +- Tool manifests can now conditionally expose static next steps using handler-activated condition keys, while runtime-generated parameters continue to flow through the existing next-step parameter contract. + ### Fixed - Fixed `suppressWarnings` being ignored in settled build, build-run, and test output. The flag was honored only while streaming, so warnings still reached the final MCP tool response ([#447](https://github.com/getsentry/XcodeBuildMCP/issues/447)). @@ -693,4 +697,3 @@ Please note that the UI automation features are an early preview and currently i ## [v1.0.1] - 2025-04-02 - Initial release of XcodeBuildMCP - Basic support for building iOS and macOS applications - diff --git a/manifests/tools/batch.yaml b/manifests/tools/batch.yaml index 699eab740..ca7297816 100644 --- a/manifests/tools/batch.yaml +++ b/manifests/tools/batch.yaml @@ -15,3 +15,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/build_device.yaml b/manifests/tools/build_device.yaml index e02d03180..85a040228 100644 --- a/manifests/tools/build_device.yaml +++ b/manifests/tools/build_device.yaml @@ -14,3 +14,13 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false +nextSteps: + - label: Get built device app path + toolId: get_device_app_path + priority: 1 + when: success + condition: app_build_succeeded + - label: Run prepared tests + toolId: test_device + when: success + condition: prepared_tests_available diff --git a/manifests/tools/build_macos.yaml b/manifests/tools/build_macos.yaml index a7de53cf6..04bb08d2c 100644 --- a/manifests/tools/build_macos.yaml +++ b/manifests/tools/build_macos.yaml @@ -14,3 +14,13 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false +nextSteps: + - label: Get built macOS app path + toolId: get_mac_app_path + priority: 1 + when: success + condition: app_build_succeeded + - label: Run prepared tests + toolId: test_macos + when: success + condition: prepared_tests_available diff --git a/manifests/tools/build_sim.yaml b/manifests/tools/build_sim.yaml index 2e06bbfa6..43b265578 100644 --- a/manifests/tools/build_sim.yaml +++ b/manifests/tools/build_sim.yaml @@ -14,3 +14,13 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false +nextSteps: + - label: Get built app path in simulator derived data + toolId: get_sim_app_path + priority: 1 + when: success + condition: app_build_succeeded + - label: Run prepared tests + toolId: test_sim + when: success + condition: prepared_tests_available diff --git a/manifests/tools/button.yaml b/manifests/tools/button.yaml index 426659dba..13d1d5dbe 100644 --- a/manifests/tools/button.yaml +++ b/manifests/tools/button.yaml @@ -14,3 +14,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/drag.yaml b/manifests/tools/drag.yaml index 3ed9078dc..d96135d87 100644 --- a/manifests/tools/drag.yaml +++ b/manifests/tools/drag.yaml @@ -15,3 +15,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/gesture.yaml b/manifests/tools/gesture.yaml index ad9a2f5f8..f14b27790 100644 --- a/manifests/tools/gesture.yaml +++ b/manifests/tools/gesture.yaml @@ -14,3 +14,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/key_press.yaml b/manifests/tools/key_press.yaml index 56336773e..11cff5eab 100644 --- a/manifests/tools/key_press.yaml +++ b/manifests/tools/key_press.yaml @@ -14,3 +14,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/key_sequence.yaml b/manifests/tools/key_sequence.yaml index 8550b6396..ab45e36e3 100644 --- a/manifests/tools/key_sequence.yaml +++ b/manifests/tools/key_sequence.yaml @@ -14,3 +14,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/long_press.yaml b/manifests/tools/long_press.yaml index 7aee1112e..e7360e747 100644 --- a/manifests/tools/long_press.yaml +++ b/manifests/tools/long_press.yaml @@ -14,3 +14,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/swipe.yaml b/manifests/tools/swipe.yaml index 5643adbe2..851c6562d 100644 --- a/manifests/tools/swipe.yaml +++ b/manifests/tools/swipe.yaml @@ -18,3 +18,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/tap.yaml b/manifests/tools/tap.yaml index d05be833e..9ffc8bb6c 100644 --- a/manifests/tools/tap.yaml +++ b/manifests/tools/tap.yaml @@ -15,3 +15,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/touch.yaml b/manifests/tools/touch.yaml index 3849c5bc8..e08aacb40 100644 --- a/manifests/tools/touch.yaml +++ b/manifests/tools/touch.yaml @@ -14,3 +14,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/manifests/tools/type_text.yaml b/manifests/tools/type_text.yaml index c2cd64ec7..1cc50c2db 100644 --- a/manifests/tools/type_text.yaml +++ b/manifests/tools/type_text.yaml @@ -15,3 +15,8 @@ annotations: readOnlyHint: true destructiveHint: false openWorldHint: false +nextSteps: + - label: Refresh after UI action + toolId: snapshot_ui + when: success + condition: ui_action_needs_refresh diff --git a/src/core/manifest/__tests__/schema.test.ts b/src/core/manifest/__tests__/schema.test.ts index 528339bd3..232cd474c 100644 --- a/src/core/manifest/__tests__/schema.test.ts +++ b/src/core/manifest/__tests__/schema.test.ts @@ -98,6 +98,43 @@ describe('schema', () => { }); }); + it('parses a custom next-step condition key', () => { + const result = toolManifestEntrySchema.safeParse({ + id: 'build_sim', + module: 'mcp/tools/simulator/build_sim', + names: { mcp: 'build_sim' }, + nextSteps: [ + { + label: 'Run prepared tests', + toolId: 'test_sim', + when: 'success', + condition: 'prepared_tests_available', + }, + ], + }); + + expect(result.success).toBe(true); + if (!result.success) throw new Error('Expected custom next-step condition to parse'); + expect(result.data.nextSteps[0]).toMatchObject({ + when: 'success', + condition: 'prepared_tests_available', + }); + }); + + it.each(['', 'PreparedTests', 'prepared-tests', 'prepared tests'])( + 'rejects invalid next-step condition key %j', + (condition) => { + const result = toolManifestEntrySchema.safeParse({ + id: 'build_sim', + module: 'mcp/tools/simulator/build_sim', + names: { mcp: 'build_sim' }, + nextSteps: [{ label: 'Run prepared tests', condition }], + }); + + expect(result.success).toBe(false); + }, + ); + it('rejects invalid output schema metadata', () => { const result = toolManifestEntrySchema.safeParse({ id: 'list_sims', diff --git a/src/core/manifest/schema.ts b/src/core/manifest/schema.ts index d75be3219..9a5be2769 100644 --- a/src/core/manifest/schema.ts +++ b/src/core/manifest/schema.ts @@ -73,6 +73,10 @@ export const manifestNextStepTemplateSchema = z params: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).default({}), priority: z.number().optional(), when: z.enum(['always', 'success', 'failure']).default('always'), + condition: z + .string() + .regex(/^[a-z][a-z0-9_]*$/) + .optional(), }) .strict(); diff --git a/src/daemon/__tests__/conditional-next-steps.test.ts b/src/daemon/__tests__/conditional-next-steps.test.ts new file mode 100644 index 000000000..8a974fd66 --- /dev/null +++ b/src/daemon/__tests__/conditional-next-steps.test.ts @@ -0,0 +1,128 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import path from 'node:path'; +import { tmpdir } from 'node:os'; +import type net from 'node:net'; +import { afterEach, describe, expect, it } from 'vitest'; +import { createRenderSession } from '../../rendering/render.ts'; +import { createToolCatalog } from '../../runtime/tool-catalog.ts'; +import { DefaultToolInvoker } from '../../runtime/tool-invoker.ts'; +import type { ToolDefinition } from '../../runtime/types.ts'; +import { startDaemonServer } from '../daemon-server.ts'; + +function createTool(overrides: Partial = {}): ToolDefinition { + return { + id: 'source', + cliName: 'source', + mcpName: 'source', + workflow: 'simulator', + cliSchema: {}, + mcpSchema: {}, + stateful: true, + handler: async () => {}, + ...overrides, + }; +} + +async function createSocketPath(): Promise { + const directory = await mkdtemp(path.join(tmpdir(), 'xcodebuildmcp-daemon-conditions-')); + return path.join(directory, 'daemon.sock'); +} + +async function listen(server: net.Server, socketPath: string): Promise { + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(socketPath, () => { + server.off('error', reject); + resolve(); + }); + }); +} + +describe('daemon conditional next steps', () => { + const cleanupPaths: string[] = []; + const cleanupServers: net.Server[] = []; + + afterEach(async () => { + await Promise.all( + cleanupServers.splice(0).map( + (server) => + new Promise((resolve) => { + server.close(() => resolve()); + }), + ), + ); + await Promise.all( + cleanupPaths.splice(0).map(async (socketPath) => { + await rm(path.dirname(socketPath), { recursive: true, force: true }); + }), + ); + }); + + it('selects a manifest template using condition metadata returned by the daemon', async () => { + const daemonSource = createTool({ + handler: async (_args, ctx) => { + ctx.nextStepConditionKeys = ['prepared_tests_available']; + ctx.nextStepParams = { + test_sim: { testProductsPath: '/tmp/App.xctestproducts' }, + }; + ctx.structuredOutput = { + schema: 'xcodebuildmcp.output.simulator-list', + schemaVersion: '1', + result: { + kind: 'simulator-list', + didError: false, + error: null, + simulators: [], + }, + }; + }, + }); + const clientSource = createTool({ + nextStepTemplates: [ + { + label: 'Run prepared tests', + toolId: 'test_sim', + when: 'success', + condition: 'prepared_tests_available', + }, + ], + }); + const testTool = createTool({ + id: 'test_sim', + cliName: 'test', + mcpName: 'test_sim', + stateful: false, + }); + const socketPath = await createSocketPath(); + cleanupPaths.push(socketPath); + + const server = startDaemonServer({ + socketPath, + startedAt: new Date().toISOString(), + enabledWorkflows: ['simulator'], + catalog: createToolCatalog([daemonSource, testTool]), + workspaceRoot: '/repo', + workspaceKey: 'repo-key', + xcodeIdeWorkflowEnabled: false, + requestShutdown: () => {}, + }); + cleanupServers.push(server); + await listen(server, socketPath); + + const session = createRenderSession('text'); + const invoker = new DefaultToolInvoker(createToolCatalog([clientSource, testTool])); + await invoker.invoke('source', {}, { runtime: 'cli', renderSession: session, socketPath }); + + expect(session.getNextSteps?.()).toEqual([ + { + tool: 'test_sim', + cliTool: 'test', + workflow: 'simulator', + label: 'Run prepared tests', + params: { testProductsPath: '/tmp/App.xctestproducts' }, + priority: undefined, + when: 'success', + }, + ]); + }); +}); diff --git a/src/daemon/__tests__/tool-invoke-streaming.test.ts b/src/daemon/__tests__/tool-invoke-streaming.test.ts index 50d1f104f..f1e11a0f6 100644 --- a/src/daemon/__tests__/tool-invoke-streaming.test.ts +++ b/src/daemon/__tests__/tool-invoke-streaming.test.ts @@ -93,6 +93,7 @@ describe('daemon tool.invoke streaming', () => { stream: 'stderr', line: 'Build Log: /tmp/build.log', }); + ctx.nextStepConditionKeys = ['build_artifact_available']; ctx.nextSteps = [{ label: 'Open the build log' }]; ctx.structuredOutput = { schema: 'xcodebuildmcp.output.simulator-list', @@ -147,6 +148,7 @@ describe('daemon tool.invoke streaming', () => { simulators: [], }, }, + nextStepConditionKeys: ['build_artifact_available'], nextSteps: [{ label: 'Open the build log' }], }); }); diff --git a/src/daemon/daemon-server.ts b/src/daemon/daemon-server.ts index b9c81869f..bdd0d1dad 100644 --- a/src/daemon/daemon-server.ts +++ b/src/daemon/daemon-server.ts @@ -221,6 +221,7 @@ export function startDaemonServer(ctx: DaemonServerContext): net.Server { result: { structuredOutput: handlerContext.structuredOutput ?? null, nextStepParams: handlerContext.nextStepParams, + nextStepConditionKeys: handlerContext.nextStepConditionKeys, nextSteps: handlerContext.nextSteps, }, }; diff --git a/src/daemon/protocol.ts b/src/daemon/protocol.ts index d3c38e920..976640504 100644 --- a/src/daemon/protocol.ts +++ b/src/daemon/protocol.ts @@ -2,7 +2,7 @@ import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; import type { StructuredToolOutput } from '../rendering/types.ts'; import type { NextStep, NextStepParamsMap } from '../types/common.ts'; import type { AnyFragment } from '../types/domain-fragments.ts'; -export const DAEMON_PROTOCOL_VERSION = 7 as const; +export const DAEMON_PROTOCOL_VERSION = 8 as const; export type DaemonMethod = | 'daemon.status' @@ -49,6 +49,7 @@ export type ToolInvokeProgressStream = { kind: 'fragment'; fragment: AnyFragment export interface ToolInvokeResult { structuredOutput: StructuredToolOutput | null; nextStepParams?: NextStepParamsMap; + nextStepConditionKeys?: string[]; nextSteps?: NextStep[]; } @@ -68,6 +69,7 @@ export interface DaemonToolResult { structuredOutput: StructuredToolOutput | null; isError: boolean; nextStepParams?: NextStepParamsMap; + nextStepConditionKeys?: string[]; nextSteps?: NextStep[]; } diff --git a/src/mcp/tools/device/__tests__/build_device.test.ts b/src/mcp/tools/device/__tests__/build_device.test.ts index a59923fb8..7def39064 100644 --- a/src/mcp/tools/device/__tests__/build_device.test.ts +++ b/src/mcp/tools/device/__tests__/build_device.test.ts @@ -291,6 +291,7 @@ describe('build_device plugin', () => { scheme: 'MyScheme', derivedDataPath: '/tmp/derived-data', }); + expect(result.nextStepConditionKeys).toEqual(['app_build_succeeded']); }); it('should return exact build failure response', async () => { @@ -411,6 +412,7 @@ describe('build_device plugin', () => { platform: 'iOS', }, }); + expect(result.nextStepConditionKeys).toEqual(['prepared_tests_available']); }); it('should not suggest running generic device test products without a device', async () => { @@ -431,6 +433,7 @@ describe('build_device plugin', () => { expect(spy.commandCalls[0].args).toContain('generic/platform=iOS'); expect(result.nextStepParams).toBeUndefined(); + expect(result.nextStepConditionKeys).toBeUndefined(); }); }); }); diff --git a/src/mcp/tools/device/build_device.ts b/src/mcp/tools/device/build_device.ts index 536f6f3e0..dd3d97478 100644 --- a/src/mcp/tools/device/build_device.ts +++ b/src/mcp/tools/device/build_device.ts @@ -199,7 +199,7 @@ export async function buildDeviceLogic( ...(params.platform ? { platform: String(mapDevicePlatform(params.platform)) } : {}), }; ctx.nextStepParams = { test_device: nextParams }; - ctx.nextSteps = [{ tool: 'test_device', label: 'Run prepared tests', params: nextParams }]; + ctx.nextStepConditionKeys = ['prepared_tests_available']; } else if (!prepared.testProductsPath) { const nextParams = { scheme: params.scheme, @@ -211,14 +211,7 @@ export async function buildDeviceLogic( : {}), }; ctx.nextStepParams = { get_device_app_path: nextParams }; - ctx.nextSteps = [ - { - tool: 'get_device_app_path', - label: 'Get built device app path', - params: nextParams, - priority: 1, - }, - ]; + ctx.nextStepConditionKeys = ['app_build_succeeded']; } } } diff --git a/src/mcp/tools/macos/__tests__/build_macos.test.ts b/src/mcp/tools/macos/__tests__/build_macos.test.ts index 5380c2d20..5cb94ac1a 100644 --- a/src/mcp/tools/macos/__tests__/build_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/build_macos.test.ts @@ -165,6 +165,7 @@ describe('build_macos plugin', () => { derivedDataPath: '/path/to/derived-data', }, }); + expect(result.nextStepConditionKeys).toEqual(['app_build_succeeded']); }); it('should return exact exception handling response', async () => { @@ -407,6 +408,7 @@ describe('build_macos plugin', () => { 'build-for-testing', ]); expect(result.nextStepParams).toEqual({ test_macos: { testProductsPath } }); + expect(result.nextStepConditionKeys).toEqual(['prepared_tests_available']); }); }); diff --git a/src/mcp/tools/macos/build_macos.ts b/src/mcp/tools/macos/build_macos.ts index b66e15f4e..e19a72222 100644 --- a/src/mcp/tools/macos/build_macos.ts +++ b/src/mcp/tools/macos/build_macos.ts @@ -224,7 +224,7 @@ export async function buildMacOSLogic( if (prepared.testProductsPath) { const nextParams = { testProductsPath: displayPath(prepared.testProductsPath) }; ctx.nextStepParams = { test_macos: nextParams }; - ctx.nextSteps = [{ tool: 'test_macos', label: 'Run prepared tests', params: nextParams }]; + ctx.nextStepConditionKeys = ['prepared_tests_available']; } else { const nextParams = { scheme: params.scheme, @@ -233,14 +233,7 @@ export async function buildMacOSLogic( : {}), }; ctx.nextStepParams = { get_mac_app_path: nextParams }; - ctx.nextSteps = [ - { - tool: 'get_mac_app_path', - label: 'Get built macOS app path', - params: nextParams, - priority: 1, - }, - ]; + ctx.nextStepConditionKeys = ['app_build_succeeded']; } } } diff --git a/src/mcp/tools/simulator/__tests__/build_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_sim.test.ts index 6b0df7560..4748d6de8 100644 --- a/src/mcp/tools/simulator/__tests__/build_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_sim.test.ts @@ -475,6 +475,7 @@ describe('build_sim tool', () => { expect(result.nextStepParams?.get_sim_app_path).toMatchObject({ derivedDataPath: '/path/to/derived', }); + expect(result.nextStepConditionKeys).toEqual(['app_build_succeeded']); }); it('should handle build failure', async () => { @@ -589,6 +590,7 @@ describe('build_sim tool', () => { expect(result.nextStepParams).toEqual({ test_sim: { testProductsPath, simulatorName: 'iPhone 17' }, }); + expect(result.nextStepConditionKeys).toEqual(['prepared_tests_available']); }); }); diff --git a/src/mcp/tools/simulator/build_sim.ts b/src/mcp/tools/simulator/build_sim.ts index ea73921ea..42f62fed4 100644 --- a/src/mcp/tools/simulator/build_sim.ts +++ b/src/mcp/tools/simulator/build_sim.ts @@ -271,7 +271,7 @@ export async function build_simLogic( : { simulatorName: params.simulatorName ?? '' }), }; ctx.nextStepParams = { test_sim: nextParams }; - ctx.nextSteps = [{ tool: 'test_sim', label: 'Run prepared tests', params: nextParams }]; + ctx.nextStepConditionKeys = ['prepared_tests_available']; } else { const nextParams = { ...(params.simulatorId @@ -284,14 +284,7 @@ export async function build_simLogic( : {}), }; ctx.nextStepParams = { get_sim_app_path: nextParams }; - ctx.nextSteps = [ - { - tool: 'get_sim_app_path', - label: 'Get built app path in simulator derived data', - params: nextParams, - priority: 1, - }, - ]; + ctx.nextStepConditionKeys = ['app_build_succeeded']; } } } diff --git a/src/mcp/tools/ui-automation/__tests__/manifest-next-step-condition.test.ts b/src/mcp/tools/ui-automation/__tests__/manifest-next-step-condition.test.ts new file mode 100644 index 000000000..db4eb103c --- /dev/null +++ b/src/mcp/tools/ui-automation/__tests__/manifest-next-step-condition.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts'; +import { + createUiActionFailureResult, + createUiActionSuccessResult, + setUiActionStructuredOutput, +} from '../shared/domain-result.ts'; + +describe('UI action manifest next-step condition', () => { + it('activates the refresh template when no runtime snapshot is available', () => { + const result = createUiActionSuccessResult({ type: 'button', button: 'home' }, 'SIMULATOR-ID'); + const { ctx } = createMockToolHandlerContext(); + + setUiActionStructuredOutput(ctx, result); + + expect(ctx.nextSteps).toBeUndefined(); + expect(ctx.nextStepConditionKeys).toEqual(['ui_action_needs_refresh']); + expect(ctx.nextStepParams).toEqual({ + snapshot_ui: { simulatorId: 'SIMULATOR-ID' }, + }); + }); + + it('does not activate the refresh template after a failed action', () => { + const result = createUiActionFailureResult( + { type: 'button', button: 'home' }, + 'SIMULATOR-ID', + 'Action failed', + ); + const { ctx } = createMockToolHandlerContext(); + + setUiActionStructuredOutput(ctx, result); + + expect(ctx.nextSteps).toBeUndefined(); + expect(ctx.nextStepConditionKeys).toBeUndefined(); + expect(ctx.nextStepParams).toBeUndefined(); + }); +}); diff --git a/src/mcp/tools/ui-automation/shared/domain-result.ts b/src/mcp/tools/ui-automation/shared/domain-result.ts index 469f24c20..9994e1a80 100644 --- a/src/mcp/tools/ui-automation/shared/domain-result.ts +++ b/src/mcp/tools/ui-automation/shared/domain-result.ts @@ -1,5 +1,4 @@ import type { RenderHints, ToolHandlerContext } from '../../../../rendering/types.ts'; -import type { NextStep } from '../../../../types/common.ts'; import type { BasicDiagnostics, CapturePayload, @@ -51,20 +50,6 @@ function compact(values: Array): string[] { return values.filter((value): value is string => typeof value === 'string' && value.length > 0); } -function createUiActionSuccessNextSteps(result: UiActionResultDomainResult): NextStep[] { - if (result.didError) { - return []; - } - - return [ - { - label: 'Refresh after UI action', - tool: 'snapshot_ui', - params: { simulatorId: result.artifacts.simulatorId }, - }, - ]; -} - function getUiActionTargetRef(action: UiAction): string | null { switch (action.type) { case 'tap': @@ -326,7 +311,10 @@ export function setUiActionStructuredOutput( schema: UI_ACTION_SCHEMA, schemaVersion: '2', }; - ctx.nextSteps = createUiActionSuccessNextSteps(result); + if (!result.didError) { + ctx.nextStepParams = { snapshot_ui: { simulatorId: result.artifacts.simulatorId } }; + ctx.nextStepConditionKeys = ['ui_action_needs_refresh']; + } } export function setCaptureStructuredOutput( diff --git a/src/rendering/types.ts b/src/rendering/types.ts index 1b3819d4e..07c395d3c 100644 --- a/src/rendering/types.ts +++ b/src/rendering/types.ts @@ -40,6 +40,7 @@ export interface ToolHandlerContext { emit: (fragment: AnyFragment) => void; attach: (image: ImageAttachment) => void; nextStepParams?: NextStepParamsMap; + nextStepConditionKeys?: string[]; nextSteps?: NextStep[]; structuredOutput?: StructuredToolOutput; } diff --git a/src/runtime/__tests__/conditional-next-steps.test.ts b/src/runtime/__tests__/conditional-next-steps.test.ts new file mode 100644 index 000000000..1ee8f6857 --- /dev/null +++ b/src/runtime/__tests__/conditional-next-steps.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; +import { createRenderSession } from '../../rendering/render.ts'; +import type { ToolHandlerContext } from '../../rendering/types.ts'; +import { createToolCatalog } from '../tool-catalog.ts'; +import { postProcessSession } from '../tool-invoker.ts'; +import type { ToolDefinition } from '../types.ts'; +import { createStructuredErrorOutput } from '../../utils/structured-error.ts'; + +function createTool(overrides: Partial = {}): ToolDefinition { + return { + id: 'source', + cliName: 'source', + mcpName: 'source', + workflow: 'simulator', + cliSchema: {}, + mcpSchema: {}, + stateful: false, + handler: async () => {}, + ...overrides, + }; +} + +function createContext(overrides: Partial = {}): ToolHandlerContext { + return { + emit: () => {}, + attach: () => {}, + ...overrides, + }; +} + +describe('conditional manifest next steps', () => { + const getAppPath = createTool({ + id: 'get_app_path', + cliName: 'get-app-path', + mcpName: 'get_app_path', + }); + const testTool = createTool({ + id: 'test_sim', + cliName: 'test', + mcpName: 'test_sim', + }); + const source = createTool({ + nextStepTemplates: [ + { + label: 'Get app path', + toolId: 'get_app_path', + when: 'success', + condition: 'app_build_succeeded', + }, + { + label: 'Run prepared tests', + toolId: 'test_sim', + when: 'success', + condition: 'prepared_tests_available', + }, + ], + }); + const catalog = createToolCatalog([source, getAppPath, testTool]); + + it('selects an active condition and merges its runtime parameters', () => { + const session = createRenderSession('text'); + + postProcessSession({ + tool: source, + session, + catalog, + runtime: 'mcp', + ctx: createContext({ + nextStepConditionKeys: ['prepared_tests_available'], + nextStepParams: { + test_sim: { testProductsPath: '/tmp/App.xctestproducts' }, + }, + }), + }); + + expect(session.getNextSteps?.()).toEqual([ + { + tool: 'test_sim', + cliTool: 'test', + workflow: 'simulator', + label: 'Run prepared tests', + params: { testProductsPath: '/tmp/App.xctestproducts' }, + priority: undefined, + when: 'success', + }, + ]); + expect(session.getNextSteps?.()[0]).not.toHaveProperty('condition'); + }); + + it('does not render templates whose condition is inactive', () => { + const session = createRenderSession('text'); + + postProcessSession({ + tool: source, + session, + catalog, + runtime: 'mcp', + ctx: createContext(), + }); + + expect(session.getNextSteps?.()).toEqual([]); + }); + + it('requires both the status and custom condition to match', () => { + const session = createRenderSession('text'); + session.setStructuredOutput?.( + createStructuredErrorOutput({ + category: 'runtime', + code: 'BUILD_FAILED', + message: 'Build failed', + }), + ); + + postProcessSession({ + tool: source, + session, + catalog, + runtime: 'mcp', + ctx: createContext({ nextStepConditionKeys: ['app_build_succeeded'] }), + }); + + expect(session.getNextSteps?.()).toEqual([]); + }); +}); diff --git a/src/runtime/tool-invoker.ts b/src/runtime/tool-invoker.ts index 1f82f2abe..2c0024c43 100644 --- a/src/runtime/tool-invoker.ts +++ b/src/runtime/tool-invoker.ts @@ -23,6 +23,7 @@ import { createStructuredErrorOutput } from '../utils/structured-error.ts'; type BuiltTemplateNextStep = { step: NextStep; templateToolId?: string; + condition?: string; }; function emitExplicitRuntimeError(params: { @@ -61,6 +62,7 @@ function buildTemplateNextSteps( priority: template.priority, when: template.when, }, + condition: template.condition, }); continue; } @@ -79,6 +81,7 @@ function buildTemplateNextSteps( when: template.when, }, templateToolId: template.toolId, + condition: template.condition, }); } @@ -222,6 +225,7 @@ export function postProcessSession(params: { const isError = session.isError(); const nextStepParams = ctx.nextStepParams; + const nextStepConditionKeys = new Set(ctx.nextStepConditionKeys ?? []); const handlerNextSteps = ctx.nextSteps; const suppressNextStepsForStructuredFailure = isError && isStructuredXcodebuildFailureSession(session); @@ -241,9 +245,9 @@ export function postProcessSession(params: { const allTemplateSteps = buildTemplateNextSteps(tool, catalog); const templateSteps = allTemplateSteps.filter((t) => { const when = t.step.when ?? 'always'; - if (when === 'success') return !isError; - if (when === 'failure') return isError; - return true; + const statusMatches = when === 'success' ? !isError : when === 'failure' ? isError : true; + const conditionMatches = !t.condition || nextStepConditionKeys.has(t.condition); + return statusMatches && conditionMatches; }); let finalSteps: NextStep[]; @@ -502,6 +506,7 @@ export class DefaultToolInvoker implements ToolInvoker { }, attach: (image) => opts.renderSession!.attach(image), nextStepParams: daemonResult.nextStepParams, + nextStepConditionKeys: daemonResult.nextStepConditionKeys, nextSteps: daemonResult.nextSteps, structuredOutput, }; @@ -547,6 +552,7 @@ export class DefaultToolInvoker implements ToolInvoker { }, attach: (image) => session.attach(image), nextStepParams: daemonResult.nextStepParams, + nextStepConditionKeys: daemonResult.nextStepConditionKeys, nextSteps: daemonResult.nextSteps, structuredOutput: daemonResult.structuredOutput ?? undefined, }; diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 0b7e9eb16..b382a3475 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -14,6 +14,7 @@ export interface NextStepTemplate { params?: Record; priority?: number; when?: 'always' | 'success' | 'failure'; + condition?: string; } export type RuntimeKind = 'cli' | 'daemon' | 'mcp'; diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-compiler.json index 2713f592f..8f36176aa 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-prepared-tests-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-prepared-tests-wrong-scheme.json new file mode 100644 index 000000000..a150f5f4d --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-prepared-tests-wrong-scheme.json @@ -0,0 +1,33 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": true, + "error": "Build failed", + "data": { + "request": { + "buildForTesting": true, + "scheme": "NONEXISTENT", + "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", + "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", + "platform": "iOS", + "target": "device", + "testProductsPath": "/Invalid CalculatorApp Tests.xctestproducts" + }, + "summary": { + "status": "FAILED", + "durationMs": 1234, + "target": "device" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log" + }, + "diagnostics": { + "warnings": [], + "errors": [ + { + "message": "The workspace named \"CalculatorApp\" does not contain a scheme named \"NONEXISTENT\". The \"-list\" option can be used to find the names of the schemes in the workspace." + } + ] + } + } +} diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-wrong-scheme.json index dbc1d4175..4707feb58 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-wrong-scheme.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--success-prepared-tests-generic.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--success-prepared-tests-generic.json new file mode 100644 index 000000000..d256e5cc4 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--success-prepared-tests-generic.json @@ -0,0 +1,33 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": false, + "error": null, + "data": { + "request": { + "buildForTesting": true, + "scheme": "CalculatorApp", + "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", + "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", + "platform": "iOS", + "target": "device", + "testProductsPath": "/Generic CalculatorApp Tests.xctestproducts" + }, + "summary": { + "status": "SUCCEEDED", + "durationMs": 1234, + "target": "device" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log", + "testProductsPath": "/Generic CalculatorApp Tests.xctestproducts", + "xctestrunPaths": [ + "/Generic CalculatorApp Tests.xctestproducts/Tests/0/CalculatorApp.xctestrun" + ] + }, + "diagnostics": { + "warnings": [], + "errors": [] + } + } +} diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--success.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--success.json index f4e221ecb..09731fd7e 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/build--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-compiler.json index 5d0b0e411..26d58e83d 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-prepared-tests-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-prepared-tests-wrong-scheme.json new file mode 100644 index 000000000..614556dc6 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-prepared-tests-wrong-scheme.json @@ -0,0 +1,33 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": true, + "error": "Build failed", + "data": { + "request": { + "buildForTesting": true, + "scheme": "NONEXISTENT", + "projectPath": "example_projects/macOS/MCPTest.xcodeproj", + "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", + "platform": "macOS", + "target": "macos", + "testProductsPath": "/Invalid MCPTest Tests.xctestproducts" + }, + "summary": { + "status": "FAILED", + "durationMs": 1234, + "target": "macos" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log" + }, + "diagnostics": { + "warnings": [], + "errors": [ + { + "message": "The project named \"MCPTest\" does not contain a scheme named \"NONEXISTENT\". The \"-list\" option can be used to find the names of the schemes in the project." + } + ] + } + } +} diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-wrong-scheme.json index 923b347af..d57658bdb 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-wrong-scheme.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--success-prepared-tests.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--success-prepared-tests.json new file mode 100644 index 000000000..1945e5ff3 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--success-prepared-tests.json @@ -0,0 +1,36 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": false, + "error": null, + "data": { + "request": { + "buildForTesting": true, + "scheme": "MCPTest", + "projectPath": "example_projects/macOS/MCPTest.xcodeproj", + "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", + "platform": "macOS", + "target": "macos", + "testProductsPath": "/MCPTest Tests.xctestproducts" + }, + "summary": { + "status": "SUCCEEDED", + "durationMs": 1234, + "target": "macos" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log", + "testProductsPath": "/MCPTest Tests.xctestproducts", + "xctestrunPaths": [ + "/MCPTest Tests.xctestproducts/Tests/0/MCPTest.xctestrun" + ] + }, + "diagnostics": { + "warnings": [], + "errors": [] + } + }, + "nextSteps": [ + "Run prepared tests: xcodebuildmcp macos test --test-products-path '/MCPTest Tests.xctestproducts'" + ] +} diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--success.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--success.json index 32d13aff1..52e8f533d 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-compiler.json index 6fdabe48c..4033faff0 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { @@ -19,16 +19,10 @@ "summary": { "status": "FAILED", "durationMs": 1234, - "counts": { - "passed": 0, - "failed": 0, - "skipped": 0 - }, "target": "macos" }, "artifacts": { - "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult" + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log" }, "tests": { "selected": [ diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-wrong-scheme.json index a3a436aee..4e820215f 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-wrong-scheme.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { @@ -16,16 +16,10 @@ "summary": { "status": "FAILED", "durationMs": 1234, - "counts": { - "passed": 0, - "failed": 0, - "skipped": 0 - }, "target": "macos" }, "artifacts": { - "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult" + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log" }, "diagnostics": { "warnings": [], diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/test--failure.json b/src/snapshot-tests/__fixtures__/cli/json/macos/test--failure.json index 0f0e5083c..5eb651c3e 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/test--failure.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/test--failure.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { @@ -25,7 +25,8 @@ }, "artifacts": { "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult" + "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult", + "testProductsPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_macos__pid.xctestproducts" }, "tests": { "discovered": { @@ -42,17 +43,17 @@ "warnings": [], "errors": [], "testFailures": [ - { - "suite": "MCPTestsXCTests", - "test": "testDeliberateFailure()", - "message": "XCTAssertTrue failed - This test is designed to fail for snapshot testing", - "location": "MCPTestsXCTests.swift:11" - }, { "suite": "MCPTestTests", "test": "deliberateFailure()", "message": "Expectation failed: 1 == 2: This test is designed to fail for snapshot testing", "location": "MCPTestTests.swift:11" + }, + { + "suite": "MCPTestsXCTests", + "test": "testDeliberateFailure()", + "message": "XCTAssertTrue failed - This test is designed to fail for snapshot testing", + "location": "MCPTestsXCTests.swift:11" } ] }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/test--success.json b/src/snapshot-tests/__fixtures__/cli/json/macos/test--success.json index b09d39b4d..3c359a699 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/test--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/test--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { @@ -28,7 +28,8 @@ }, "artifacts": { "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult" + "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult", + "testProductsPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_macos__pid.xctestproducts" }, "tests": { "selected": [ diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-compiler.json index d86866109..ea81bbe63 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-prepared-tests-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-prepared-tests-wrong-scheme.json new file mode 100644 index 000000000..ebd7f2d34 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-prepared-tests-wrong-scheme.json @@ -0,0 +1,33 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": true, + "error": "Build failed", + "data": { + "request": { + "buildForTesting": true, + "scheme": "NONEXISTENT", + "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", + "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", + "platform": "iOS Simulator", + "simulatorName": "iPhone 17", + "testProductsPath": "/Invalid CalculatorApp Tests.xctestproducts" + }, + "summary": { + "status": "FAILED", + "durationMs": 1234, + "target": "simulator" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log" + }, + "diagnostics": { + "warnings": [], + "errors": [ + { + "message": "The workspace named \"CalculatorApp\" does not contain a scheme named \"NONEXISTENT\". The \"-list\" option can be used to find the names of the schemes in the workspace." + } + ] + } + } +} diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-wrong-scheme.json index cb912b99e..a491f6c0a 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-wrong-scheme.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success-prepared-tests.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success-prepared-tests.json new file mode 100644 index 000000000..1afb8ba5d --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success-prepared-tests.json @@ -0,0 +1,36 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": false, + "error": null, + "data": { + "request": { + "buildForTesting": true, + "scheme": "CalculatorApp", + "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", + "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", + "platform": "iOS Simulator", + "simulatorName": "iPhone 17", + "testProductsPath": "/CalculatorApp Tests.xctestproducts" + }, + "summary": { + "status": "SUCCEEDED", + "durationMs": 1234, + "target": "simulator" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log", + "testProductsPath": "/CalculatorApp Tests.xctestproducts", + "xctestrunPaths": [ + "/CalculatorApp Tests.xctestproducts/Tests/0/CalculatorApp.xctestrun" + ] + }, + "diagnostics": { + "warnings": [], + "errors": [] + } + }, + "nextSteps": [ + "Run prepared tests: xcodebuildmcp simulator test --test-products-path '/CalculatorApp Tests.xctestproducts' --simulator-name 'iPhone 17'" + ] +} diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success.json index a4d7bbeb2..886feae56 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-compiler.json index 98648c37e..389e7bdab 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-wrong-scheme.json index b4ffc4ae7..00c66ee0f 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-wrong-scheme.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--failure.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--failure.json index e56bfefca..f4de9c9a7 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--failure.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--failure.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { @@ -26,7 +26,8 @@ }, "artifacts": { "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_sim__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult" + "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult", + "testProductsPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_sim__pid.xctestproducts" }, "tests": { "discovered": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--success.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--success.json index 68d45018e..519bd70d0 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { @@ -28,7 +28,8 @@ }, "artifacts": { "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_sim__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult" + "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult", + "testProductsPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_sim__pid.xctestproducts" }, "tests": { "selected": [ diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build--error-prepared-tests-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build--error-prepared-tests-wrong-scheme.txt new file mode 100644 index 000000000..5ecb6b4e8 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build--error-prepared-tests-wrong-scheme.txt @@ -0,0 +1,15 @@ + +🔨 Build + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Platform: iOS + Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + +Errors (1): + + ✗ The workspace named "CalculatorApp" does not contain a scheme named "NONEXISTENT". The "-list" option can be used to find the names of the schemes in the workspace. + +❌ Build failed. (⏱️ ) + └ Files: + └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build--success-prepared-tests-generic.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build--success-prepared-tests-generic.txt new file mode 100644 index 000000000..cb318c66e --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build--success-prepared-tests-generic.txt @@ -0,0 +1,13 @@ + +🔨 Build + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Platform: iOS + Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + +✅ Build succeeded. (⏱️ ) + └ Files: + ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log + ├ Test Products: /Generic CalculatorApp Tests.xctestproducts + └ XCTest Run: /Generic CalculatorApp Tests.xctestproducts/Tests/0/CalculatorApp.xctestrun diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-prepared-tests-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-prepared-tests-wrong-scheme.txt new file mode 100644 index 000000000..4238aea69 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-prepared-tests-wrong-scheme.txt @@ -0,0 +1,15 @@ + +🔨 Build + + Scheme: NONEXISTENT + Project: example_projects/macOS/MCPTest.xcodeproj + Platform: macOS + Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + +Errors (1): + + ✗ The project named "MCPTest" does not contain a scheme named "NONEXISTENT". The "-list" option can be used to find the names of the schemes in the project. + +❌ Build failed. (⏱️ ) + └ Files: + └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build--success-prepared-tests.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build--success-prepared-tests.txt new file mode 100644 index 000000000..666cd8bf9 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build--success-prepared-tests.txt @@ -0,0 +1,16 @@ + +🔨 Build + + Scheme: MCPTest + Project: example_projects/macOS/MCPTest.xcodeproj + Platform: macOS + Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + +✅ Build succeeded. (⏱️ ) + └ Files: + ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log + ├ Test Products: /MCPTest Tests.xctestproducts + └ XCTest Run: /MCPTest Tests.xctestproducts/Tests/0/MCPTest.xctestrun + +Next steps: +1. Run prepared tests: xcodebuildmcp macos test --test-products-path '/MCPTest Tests.xctestproducts' diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-compiler.txt index 0a357bdbe..f44082a27 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-compiler.txt @@ -21,5 +21,4 @@ Compiler Errors (1): ❌ Test failed. (⏱️ ) └ Files: - ├ Result Bundle: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-wrong-scheme.txt index 795e8d84e..e3b67a6f0 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-wrong-scheme.txt @@ -13,5 +13,4 @@ Errors (1): ❌ Test failed. (⏱️ ) └ Files: - ├ Result Bundle: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/test--failure.txt index 2a220f7aa..64320a171 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/test--failure.txt @@ -17,17 +17,18 @@ Running tests (2 completed, 1 failure, 0 skipped) Running tests (3 completed, 1 failure, 0 skipped) Running tests (4 completed, 2 failures, 0 skipped) -MCPTestsXCTests - ✗ testDeliberateFailure(): - - XCTAssertTrue failed - This test is designed to fail for snapshot testing - example_projects/macOS/MCPTestTests/MCPTestsXCTests.swift:11 - MCPTestTests ✗ deliberateFailure(): - Expectation failed: 1 == 2: This test is designed to fail for snapshot testing example_projects/macOS/MCPTestTests/MCPTestTests.swift:11 +MCPTestsXCTests + ✗ testDeliberateFailure(): + - XCTAssertTrue failed - This test is designed to fail for snapshot testing + example_projects/macOS/MCPTestTests/MCPTestsXCTests.swift:11 + ❌ tests failed, passed, skipped (⏱️ ) └ Files: ├ Result Bundle: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult - └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log + ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log + └ Test Products: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_macos__pid.xctestproducts diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/test--success.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/test--success.txt index cb54b9b7a..c151ec21c 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/test--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/test--success.txt @@ -19,4 +19,5 @@ Running tests (2 completed, 0 failures, 0 skipped) ✅ 2 tests passed, 0 failed, 0 skipped (⏱️ ) └ Files: ├ Result Bundle: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult - └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log + ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log + └ Test Products: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_macos__pid.xctestproducts diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-prepared-tests-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-prepared-tests-wrong-scheme.txt new file mode 100644 index 000000000..f9d10500d --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-prepared-tests-wrong-scheme.txt @@ -0,0 +1,16 @@ + +🔨 Build + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Platform: iOS Simulator + Simulator: iPhone 17 + Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + +Errors (1): + + ✗ The workspace named "CalculatorApp" does not contain a scheme named "NONEXISTENT". The "-list" option can be used to find the names of the schemes in the workspace. + +❌ Build failed. (⏱️ ) + └ Files: + └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success-prepared-tests.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success-prepared-tests.txt new file mode 100644 index 000000000..49db1d402 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success-prepared-tests.txt @@ -0,0 +1,17 @@ + +🔨 Build + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Platform: iOS Simulator + Simulator: iPhone 17 + Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + +✅ Build succeeded. (⏱️ ) + └ Files: + ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log + ├ Test Products: /CalculatorApp Tests.xctestproducts + └ XCTest Run: /CalculatorApp Tests.xctestproducts/Tests/0/CalculatorApp.xctestrun + +Next steps: +1. Run prepared tests: xcodebuildmcp simulator test --test-products-path '/CalculatorApp Tests.xctestproducts' --simulator-name 'iPhone 17' diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--failure.txt index 6500f0f76..83d5b4e3b 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--failure.txt @@ -38,4 +38,5 @@ IntentionalFailureTests ❌ tests failed, passed, skipped (⏱️ ) └ Files: ├ Result Bundle: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult - └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_sim__pid.log + ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_sim__pid.log + └ Test Products: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_sim__pid.xctestproducts diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--success.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--success.txt index 6f2c5e1aa..e39ed1898 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--success.txt @@ -17,4 +17,5 @@ Running tests (1 completed, 0 failures, 0 skipped) ✅ 1 test passed, 0 failed, 0 skipped (⏱️ ) └ Files: ├ Result Bundle: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult - └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_sim__pid.log + ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_sim__pid.log + └ Test Products: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_sim__pid.xctestproducts diff --git a/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-compiler.json b/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-compiler.json index df6c7eafd..92f20bd62 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-prepared-tests-wrong-scheme.json b/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-prepared-tests-wrong-scheme.json new file mode 100644 index 000000000..d1f2c3ed7 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-prepared-tests-wrong-scheme.json @@ -0,0 +1,24 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": true, + "error": "Build failed", + "data": { + "summary": { + "status": "FAILED", + "durationMs": 1234, + "target": "device" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log" + }, + "diagnostics": { + "warnings": [], + "errors": [ + { + "message": "The workspace named \"CalculatorApp\" does not contain a scheme named \"NONEXISTENT\". The \"-list\" option can be used to find the names of the schemes in the workspace." + } + ] + } + } +} diff --git a/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-wrong-scheme.json index 0799e0040..d1f2c3ed7 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/device/build--error-wrong-scheme.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/device/build--success-prepared-tests-generic.json b/src/snapshot-tests/__fixtures__/mcp/json/device/build--success-prepared-tests-generic.json new file mode 100644 index 000000000..9a8844bae --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/json/device/build--success-prepared-tests-generic.json @@ -0,0 +1,24 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": false, + "error": null, + "data": { + "summary": { + "status": "SUCCEEDED", + "durationMs": 1234, + "target": "device" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log", + "testProductsPath": "/Generic CalculatorApp Tests.xctestproducts", + "xctestrunPaths": [ + "/Generic CalculatorApp Tests.xctestproducts/Tests/0/CalculatorApp.xctestrun" + ] + }, + "diagnostics": { + "warnings": [], + "errors": [] + } + } +} diff --git a/src/snapshot-tests/__fixtures__/mcp/json/device/build--success.json b/src/snapshot-tests/__fixtures__/mcp/json/device/build--success.json index c29c38cff..017050f3a 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/device/build--success.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/device/build--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-compiler.json b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-compiler.json index 056b18fa3..444bb5864 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-prepared-tests-wrong-scheme.json b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-prepared-tests-wrong-scheme.json new file mode 100644 index 000000000..0910b24a1 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-prepared-tests-wrong-scheme.json @@ -0,0 +1,24 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": true, + "error": "Build failed", + "data": { + "summary": { + "status": "FAILED", + "durationMs": 1234, + "target": "macos" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log" + }, + "diagnostics": { + "warnings": [], + "errors": [ + { + "message": "The project named \"MCPTest\" does not contain a scheme named \"NONEXISTENT\". The \"-list\" option can be used to find the names of the schemes in the project." + } + ] + } + } +} diff --git a/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-wrong-scheme.json index 8d4556828..0910b24a1 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--error-wrong-scheme.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/macos/build--success-prepared-tests.json b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--success-prepared-tests.json new file mode 100644 index 000000000..bfc39eb75 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--success-prepared-tests.json @@ -0,0 +1,27 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": false, + "error": null, + "data": { + "summary": { + "status": "SUCCEEDED", + "durationMs": 1234, + "target": "macos" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log", + "testProductsPath": "/MCPTest Tests.xctestproducts", + "xctestrunPaths": [ + "/MCPTest Tests.xctestproducts/Tests/0/MCPTest.xctestrun" + ] + }, + "diagnostics": { + "warnings": [], + "errors": [] + } + }, + "nextSteps": [ + "Run prepared tests: test_macos({ testProductsPath: \"/MCPTest Tests.xctestproducts\" })" + ] +} diff --git a/src/snapshot-tests/__fixtures__/mcp/json/macos/build--success.json b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--success.json index 5e7318280..e6a64a9a6 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/macos/build--success.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/macos/build--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/macos/test--error-compiler.json b/src/snapshot-tests/__fixtures__/mcp/json/macos/test--error-compiler.json index 78c2aa7cd..a070027e3 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/macos/test--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/macos/test--error-compiler.json @@ -1,22 +1,16 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { "summary": { "status": "FAILED", "durationMs": 1234, - "counts": { - "passed": 0, - "failed": 0, - "skipped": 0 - }, "target": "macos" }, "artifacts": { - "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult" + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log" }, "tests": { "selected": [ diff --git a/src/snapshot-tests/__fixtures__/mcp/json/macos/test--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/mcp/json/macos/test--error-wrong-scheme.json index 46da0089b..9ae7221af 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/macos/test--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/macos/test--error-wrong-scheme.json @@ -1,22 +1,16 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { "summary": { "status": "FAILED", "durationMs": 1234, - "counts": { - "passed": 0, - "failed": 0, - "skipped": 0 - }, "target": "macos" }, "artifacts": { - "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult" + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log" }, "diagnostics": { "warnings": [], diff --git a/src/snapshot-tests/__fixtures__/mcp/json/macos/test--failure.json b/src/snapshot-tests/__fixtures__/mcp/json/macos/test--failure.json index 8db733c47..ebccfb4f7 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/macos/test--failure.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/macos/test--failure.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { @@ -16,7 +16,8 @@ }, "artifacts": { "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult" + "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult", + "testProductsPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_macos__pid.xctestproducts" }, "tests": { "discovered": { @@ -33,17 +34,17 @@ "warnings": [], "errors": [], "testFailures": [ - { - "suite": "MCPTestsXCTests", - "test": "testDeliberateFailure()", - "message": "XCTAssertTrue failed - This test is designed to fail for snapshot testing", - "location": "MCPTestsXCTests.swift:11" - }, { "suite": "MCPTestTests", "test": "deliberateFailure()", "message": "Expectation failed: 1 == 2: This test is designed to fail for snapshot testing", "location": "MCPTestTests.swift:11" + }, + { + "suite": "MCPTestsXCTests", + "test": "testDeliberateFailure()", + "message": "XCTAssertTrue failed - This test is designed to fail for snapshot testing", + "location": "MCPTestsXCTests.swift:11" } ] }, diff --git a/src/snapshot-tests/__fixtures__/mcp/json/macos/test--success.json b/src/snapshot-tests/__fixtures__/mcp/json/macos/test--success.json index 031d4f8d0..42262778b 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/macos/test--success.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/macos/test--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { @@ -16,7 +16,8 @@ }, "artifacts": { "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult" + "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult", + "testProductsPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_macos__pid.xctestproducts" }, "tests": { "selected": [ diff --git a/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-compiler.json b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-compiler.json index 19d7929c4..adc84fd04 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-prepared-tests-wrong-scheme.json b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-prepared-tests-wrong-scheme.json new file mode 100644 index 000000000..2e9725ab5 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-prepared-tests-wrong-scheme.json @@ -0,0 +1,24 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": true, + "error": "Build failed", + "data": { + "summary": { + "status": "FAILED", + "durationMs": 1234, + "target": "simulator" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log" + }, + "diagnostics": { + "warnings": [], + "errors": [ + { + "message": "The workspace named \"CalculatorApp\" does not contain a scheme named \"NONEXISTENT\". The \"-list\" option can be used to find the names of the schemes in the workspace." + } + ] + } + } +} diff --git a/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-wrong-scheme.json index c0c6d8f27..2e9725ab5 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--error-wrong-scheme.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Build failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--success-prepared-tests.json b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--success-prepared-tests.json new file mode 100644 index 000000000..7ec0d1ab1 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--success-prepared-tests.json @@ -0,0 +1,27 @@ +{ + "schema": "xcodebuildmcp.output.build-result", + "schemaVersion": "3", + "didError": false, + "error": null, + "data": { + "summary": { + "status": "SUCCEEDED", + "durationMs": 1234, + "target": "simulator" + }, + "artifacts": { + "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log", + "testProductsPath": "/CalculatorApp Tests.xctestproducts", + "xctestrunPaths": [ + "/CalculatorApp Tests.xctestproducts/Tests/0/CalculatorApp.xctestrun" + ] + }, + "diagnostics": { + "warnings": [], + "errors": [] + } + }, + "nextSteps": [ + "Run prepared tests: test_sim({ testProductsPath: \"/CalculatorApp Tests.xctestproducts\", simulatorName: \"iPhone 17\" })" + ] +} diff --git a/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--success.json b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--success.json index 79e2ad1cf..564ccfddb 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--success.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/simulator/build--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.build-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--error-compiler.json b/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--error-compiler.json index 8bc0b483f..17415bac4 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--error-wrong-scheme.json index 8658e3ec8..1b6fbb54a 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--error-wrong-scheme.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--failure.json b/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--failure.json index d3acd12fc..2d5a4d2dc 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--failure.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--failure.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { @@ -16,7 +16,8 @@ }, "artifacts": { "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_sim__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult" + "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult", + "testProductsPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_sim__pid.xctestproducts" }, "tests": { "discovered": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--success.json b/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--success.json index 5dabc5eb5..424b1438f 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--success.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/simulator/test--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { @@ -16,7 +16,8 @@ }, "artifacts": { "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_sim__pid.log", - "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult" + "xcresultPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_sim__pid.xcresult", + "testProductsPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_sim__pid.xctestproducts" }, "tests": { "selected": [ diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/build--error-prepared-tests-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/build--error-prepared-tests-wrong-scheme.txt new file mode 100644 index 000000000..a65c33fe1 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/build--error-prepared-tests-wrong-scheme.txt @@ -0,0 +1,10 @@ + +🔨 Build + +Errors (1): + + ✗ The workspace named "CalculatorApp" does not contain a scheme named "NONEXISTENT". The "-list" option can be used to find the names of the schemes in the workspace. + +❌ Build failed. (⏱️ ) + └ Files: + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log — Build Logs diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/build--success-prepared-tests-generic.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/build--success-prepared-tests-generic.txt new file mode 100644 index 000000000..9aaa9f513 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/build--success-prepared-tests-generic.txt @@ -0,0 +1,8 @@ + +🔨 Build + +✅ Build succeeded. (⏱️ ) + └ Files: + ├── /Generic CalculatorApp Tests.xctestproducts/ — Test Products + │ └── Tests/0/CalculatorApp.xctestrun — XCTest Run + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log — Build Logs diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/build--error-prepared-tests-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/build--error-prepared-tests-wrong-scheme.txt new file mode 100644 index 000000000..0da1f22f8 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/build--error-prepared-tests-wrong-scheme.txt @@ -0,0 +1,10 @@ + +🔨 Build + +Errors (1): + + ✗ The project named "MCPTest" does not contain a scheme named "NONEXISTENT". The "-list" option can be used to find the names of the schemes in the project. + +❌ Build failed. (⏱️ ) + └ Files: + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log — Build Logs diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/build--success-prepared-tests.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/build--success-prepared-tests.txt new file mode 100644 index 000000000..7aae427ba --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/build--success-prepared-tests.txt @@ -0,0 +1,11 @@ + +🔨 Build + +✅ Build succeeded. (⏱️ ) + └ Files: + ├── /MCPTest Tests.xctestproducts/ — Test Products + │ └── Tests/0/MCPTest.xctestrun — XCTest Run + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log — Build Logs + +Next steps: +1. Run prepared tests: test_macos({ testProductsPath: "/MCPTest Tests.xctestproducts" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/test--error-compiler.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/test--error-compiler.txt index 12a1f9e69..a88ecf3e7 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/macos/test--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/test--error-compiler.txt @@ -12,6 +12,4 @@ Errors (1): ❌ Test failed. (⏱️ ) └ Files: - └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ - ├── logs/test_macos__pid.log — Build Logs - └── result-bundles/test_macos__pid.xcresult — Result Bundle + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log — Build Logs diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/test--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/test--error-wrong-scheme.txt index 28957ac8b..f252f8301 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/macos/test--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/test--error-wrong-scheme.txt @@ -7,6 +7,4 @@ Errors (1): ❌ Test failed. (⏱️ ) └ Files: - └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ - ├── logs/test_macos__pid.log — Build Logs - └── result-bundles/test_macos__pid.xcresult — Result Bundle + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_macos__pid.log — Build Logs diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/test--failure.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/test--failure.txt index c7db60d85..f68d9e253 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/macos/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/test--failure.txt @@ -9,14 +9,15 @@ Discovered 4 test(s): Test Failures (2): - ✗ MCPTestsXCTests / testDeliberateFailure(): XCTAssertTrue failed - This test is designed to fail for snapshot testing - MCPTestsXCTests.swift:11 - ✗ MCPTestTests / deliberateFailure(): Expectation failed: 1 == 2: This test is designed to fail for snapshot testing MCPTestTests.swift:11 + ✗ MCPTestsXCTests / testDeliberateFailure(): XCTAssertTrue failed - This test is designed to fail for snapshot testing + MCPTestsXCTests.swift:11 + ❌ tests failed, passed, skipped (⏱️ ) └ Files: └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ ├── logs/test_macos__pid.log — Build Logs - └── result-bundles/test_macos__pid.xcresult — Result Bundle + ├── result-bundles/test_macos__pid.xcresult — Result Bundle + └── test-products/test_macos__pid.xctestproducts — Test Products diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/test--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/test--success.txt index bef7e6229..03ced74f9 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/macos/test--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/test--success.txt @@ -9,4 +9,5 @@ Discovered 2 test(s): └ Files: └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ ├── logs/test_macos__pid.log — Build Logs - └── result-bundles/test_macos__pid.xcresult — Result Bundle + ├── result-bundles/test_macos__pid.xcresult — Result Bundle + └── test-products/test_macos__pid.xctestproducts — Test Products diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--error-prepared-tests-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--error-prepared-tests-wrong-scheme.txt new file mode 100644 index 000000000..2bf85ad02 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--error-prepared-tests-wrong-scheme.txt @@ -0,0 +1,10 @@ + +🔨 Build + +Errors (1): + + ✗ The workspace named "CalculatorApp" does not contain a scheme named "NONEXISTENT". The "-list" option can be used to find the names of the schemes in the workspace. + +❌ Build failed. (⏱️ ) + └ Files: + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log — Build Logs diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success-prepared-tests.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success-prepared-tests.txt new file mode 100644 index 000000000..550e86618 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success-prepared-tests.txt @@ -0,0 +1,11 @@ + +🔨 Build + +✅ Build succeeded. (⏱️ ) + └ Files: + ├── /CalculatorApp Tests.xctestproducts/ — Test Products + │ └── Tests/0/CalculatorApp.xctestrun — XCTest Run + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log — Build Logs + +Next steps: +1. Run prepared tests: test_sim({ testProductsPath: "/CalculatorApp Tests.xctestproducts", simulatorName: "iPhone 17" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--failure.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--failure.txt index 6edf231d7..513c42ce0 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--failure.txt @@ -27,4 +27,5 @@ Test Failures (3): └ Files: └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ ├── logs/test_sim__pid.log — Build Logs - └── result-bundles/test_sim__pid.xcresult — Result Bundle + ├── result-bundles/test_sim__pid.xcresult — Result Bundle + └── test-products/test_sim__pid.xctestproducts — Test Products diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--success.txt index d32b486b8..7b5337002 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--success.txt @@ -8,4 +8,5 @@ Discovered 1 test(s): └ Files: └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ ├── logs/test_sim__pid.log — Build Logs - └── result-bundles/test_sim__pid.xcresult — Result Bundle + ├── result-bundles/test_sim__pid.xcresult — Result Bundle + └── test-products/test_sim__pid.xctestproducts — Test Products diff --git a/src/snapshot-tests/__tests__/normalize.test.ts b/src/snapshot-tests/__tests__/normalize.test.ts index 65af7a5fb..5ee6fd9da 100644 --- a/src/snapshot-tests/__tests__/normalize.test.ts +++ b/src/snapshot-tests/__tests__/normalize.test.ts @@ -175,6 +175,24 @@ describe('normalizeSnapshotOutput', () => { ).toBe('~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build.log\n'); }); + it('normalizes worktree-specific workspace names to the canonical fixture label', () => { + expect( + normalizeSnapshotOutput( + '~/Library/Developer/XcodeBuildMCP/workspaces/issue-450-c5da0cbe19a7/logs/build.log\n', + ), + ).toBe('~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build.log\n'); + }); + + it('normalizes generated test products process and random suffixes', () => { + expect( + normalizeSnapshotOutput( + '~/Library/Developer/XcodeBuildMCP/workspaces/issue-450-c5da0cbe19a7/test-products/test_sim_2026-07-16T13-20-13-467Z_pid31212_06abe32f.xctestproducts\n', + ), + ).toBe( + '~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_sim__pid.xctestproducts\n', + ); + }); + it('normalizes absolute home XcodeBuildMCP paths to ~/', () => { expect( normalizeSnapshotOutput( diff --git a/src/snapshot-tests/harness.ts b/src/snapshot-tests/harness.ts index 712bdfef8..6a2b46009 100644 --- a/src/snapshot-tests/harness.ts +++ b/src/snapshot-tests/harness.ts @@ -15,6 +15,9 @@ const SNAPSHOT_COMMAND_TIMEOUT_MS = 120_000; const SIMULATOR_STATE_WAIT_TIMEOUT_MS = 15_000; const SIMULATOR_STATE_POLL_INTERVAL_MS = 250; +// Snapshot suites intentionally run serially because this harness shares simulator and filesystem +// state across scenarios. Keep vitest.snapshot.config.ts at one worker unless those resources are +// isolated per worker. export type SnapshotHarness = WorkflowSnapshotHarness; export type { SnapshotResult }; diff --git a/src/snapshot-tests/normalize.ts b/src/snapshot-tests/normalize.ts index ed2fbd363..f4b669b57 100644 --- a/src/snapshot-tests/normalize.ts +++ b/src/snapshot-tests/normalize.ts @@ -13,6 +13,7 @@ const PID_NAME_REGEX = /\bPID \d+\b/g; const KILL_PID_REGEX = /(\bkill:\s*)\d+(?=:)/g; const PID_FILENAME_SUFFIX_REGEX = /_pid\d+(?:_[0-9a-f]{8})?\.log/g; const XCRESULT_FILENAME_PID_SUFFIX_REGEX = /_pid\d+_[0-9a-f]{8}\.xcresult/g; +const XCTESTPRODUCTS_FILENAME_PID_SUFFIX_REGEX = /_pid\d+_[0-9a-f]{8}\.xctestproducts/g; const HELPER_PID_FILENAME_SUFFIX_REGEX = /_(?:helperpid\d+_ownerpid\d+|ownerpid\d+)_[0-9a-f]{8}\.log/g; const PID_JSON_REGEX = /"pid"\s*:\s*\d+/g; @@ -74,7 +75,7 @@ const CODEX_WORKTREE_NODE_MODULES_REGEX = /\/\.codex\/worktrees\/[^/:]+\/node_modules\/\.bin/g; const XCODEBUILDMCP_HOME_PREFIX_REGEX = /(?=\/Library\/Developer\/XcodeBuildMCP(?:\/|$))/g; const XCODEBUILDMCP_WORKSPACE_KEY_REGEX = - /(~\/Library\/Developer\/XcodeBuildMCP\/workspaces\/[^/\n]+)-[0-9a-f]{12}(?=\/|$)/g; + /(~\/Library\/Developer\/XcodeBuildMCP\/workspaces\/)[^/\n]+-[0-9a-f]{12}(?=\/|$)/g; const XCODE_IDE_ARTIFACT_OWNER_PID_REGEX = /(\/state\/xcode-ide\/call-tool\/ownerpid)\d+_/g; const XCODE_IDE_ARTIFACT_HASH_REGEX = /(\/state\/xcode-ide\/call-tool\/[^/\n]+\/[^/\n]+-)[0-9a-f]{8}(?=\.json)/g; @@ -226,7 +227,7 @@ export function normalizeSnapshotOutput( '', ); normalized = normalized.replace(XCODEBUILDMCP_HOME_PREFIX_REGEX, '~'); - normalized = normalized.replace(XCODEBUILDMCP_WORKSPACE_KEY_REGEX, '$1-'); + normalized = normalized.replace(XCODEBUILDMCP_WORKSPACE_KEY_REGEX, '$1XcodeBuildMCP-'); normalized = normalized.replace(XCODE_IDE_ARTIFACT_OWNER_PID_REGEX, '$1_'); normalized = normalized.replace(XCODE_IDE_ARTIFACT_HASH_REGEX, '$1'); normalized = normalized.replace( @@ -258,6 +259,10 @@ export function normalizeSnapshotOutput( normalized = normalized.replace(HELPER_PID_FILENAME_SUFFIX_REGEX, '_pid.log'); normalized = normalized.replace(PID_FILENAME_SUFFIX_REGEX, '_pid.log'); normalized = normalized.replace(XCRESULT_FILENAME_PID_SUFFIX_REGEX, '_pid.xcresult'); + normalized = normalized.replace( + XCTESTPRODUCTS_FILENAME_PID_SUFFIX_REGEX, + '_pid.xctestproducts', + ); normalized = normalized.replace(PID_JSON_REGEX, '"pid" : '); normalized = normalized.replace(PROCESS_ID_REGEX, 'Process ID: '); normalized = normalized.replace(PROCESS_INLINE_PID_REGEX, 'process '); diff --git a/src/snapshot-tests/suites/device-suite.ts b/src/snapshot-tests/suites/device-suite.ts index 4fe90bf66..5beffa810 100644 --- a/src/snapshot-tests/suites/device-suite.ts +++ b/src/snapshot-tests/suites/device-suite.ts @@ -1,4 +1,7 @@ import { describe, it, beforeAll, afterAll, vi } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import type { SnapshotRuntime, WorkflowSnapshotHarness } from '../contracts.ts'; import { isDeviceAvailable } from '../device-availability.ts'; import { @@ -28,14 +31,19 @@ export function registerDeviceSnapshotSuite(runtime: SnapshotRuntime): void { describe(`${runtime} device workflow`, () => { let harness: WorkflowSnapshotHarness; + let testProductsDir: string; beforeAll(async () => { vi.setConfig({ testTimeout: 120_000 }); harness = await createHarnessForRuntime(runtime); + testProductsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'device-build-products-')); }, 120_000); afterAll(async () => { await harness.cleanup(); + if (testProductsDir) { + fs.rmSync(testProductsDir, { recursive: true, force: true }); + } }); describe('list', () => { @@ -54,6 +62,19 @@ export function registerDeviceSnapshotSuite(runtime: SnapshotRuntime): void { expectFixture(text, 'build--success'); }); + it('success - prepared tests without a selected device', async () => { + const { text } = await harness.invoke('device', 'build', { + workspacePath: WORKSPACE, + scheme: 'CalculatorApp', + buildForTesting: true, + testProductsPath: path.join( + testProductsDir, + 'Generic CalculatorApp Tests.xctestproducts', + ), + }); + expectFixture(text, 'build--success-prepared-tests-generic'); + }); + it('error - wrong scheme', async () => { const { text } = await harness.invoke('device', 'build', { workspacePath: WORKSPACE, @@ -70,6 +91,19 @@ export function registerDeviceSnapshotSuite(runtime: SnapshotRuntime): void { }); expectFixture(text, 'build--error-compiler'); }); + + it('error - prepared tests with wrong scheme', async () => { + const { text } = await harness.invoke('device', 'build', { + workspacePath: WORKSPACE, + scheme: 'NONEXISTENT', + buildForTesting: true, + testProductsPath: path.join( + testProductsDir, + 'Invalid CalculatorApp Tests.xctestproducts', + ), + }); + expectFixture(text, 'build--error-prepared-tests-wrong-scheme'); + }); }); describe('get-app-path', () => { diff --git a/src/snapshot-tests/suites/macos-suite.ts b/src/snapshot-tests/suites/macos-suite.ts index 3b7094a1d..d6ec5de36 100644 --- a/src/snapshot-tests/suites/macos-suite.ts +++ b/src/snapshot-tests/suites/macos-suite.ts @@ -62,6 +62,16 @@ export function registerMacosSnapshotSuite(runtime: SnapshotRuntime): void { expectFixture(text, 'build--success'); }); + it('success - prepared tests', { timeout: 120000 }, async () => { + const { text } = await harness.invoke('macos', 'build', { + projectPath: PROJECT, + scheme: 'MCPTest', + buildForTesting: true, + testProductsPath: path.join(tmpDir, 'MCPTest Tests.xctestproducts'), + }); + expectFixture(text, 'build--success-prepared-tests'); + }); + it('error - wrong scheme', { timeout: 120000 }, async () => { const { text } = await harness.invoke('macos', 'build', { projectPath: PROJECT, @@ -78,6 +88,16 @@ export function registerMacosSnapshotSuite(runtime: SnapshotRuntime): void { }); expectFixture(text, 'build--error-compiler'); }); + + it('error - prepared tests with wrong scheme', { timeout: 120000 }, async () => { + const { text } = await harness.invoke('macos', 'build', { + projectPath: PROJECT, + scheme: 'NONEXISTENT', + buildForTesting: true, + testProductsPath: path.join(tmpDir, 'Invalid MCPTest Tests.xctestproducts'), + }); + expectFixture(text, 'build--error-prepared-tests-wrong-scheme'); + }); }); describe('build-and-run', () => { diff --git a/src/snapshot-tests/suites/simulator-suite.ts b/src/snapshot-tests/suites/simulator-suite.ts index 06d3c1009..e50205032 100644 --- a/src/snapshot-tests/suites/simulator-suite.ts +++ b/src/snapshot-tests/suites/simulator-suite.ts @@ -30,15 +30,20 @@ export function registerSimulatorSnapshotSuite(runtime: SnapshotRuntime): void { describe(`${runtime} simulator workflow`, () => { let harness: WorkflowSnapshotHarness; let simulatorUdid: string; + let testProductsDir: string; beforeAll(async () => { vi.setConfig({ testTimeout: TEST_TIMEOUT_MS }); harness = await createHarnessForRuntime(runtime); simulatorUdid = await ensureSimulatorBooted(PRIMARY_BOOTED_SIMULATOR); + testProductsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sim-build-products-')); }, TEST_TIMEOUT_MS); afterAll(async () => { await harness.cleanup(); + if (testProductsDir) { + fs.rmSync(testProductsDir, { recursive: true, force: true }); + } }); describe('build', () => { @@ -55,6 +60,21 @@ export function registerSimulatorSnapshotSuite(runtime: SnapshotRuntime): void { TEST_TIMEOUT_MS, ); + it( + 'success - prepared tests', + async () => { + const { text } = await harness.invoke('simulator', 'build', { + workspacePath: WORKSPACE, + scheme: SCHEME, + simulatorName: SIMULATOR_NAME, + buildForTesting: true, + testProductsPath: path.join(testProductsDir, 'CalculatorApp Tests.xctestproducts'), + }); + expectFixture(text, 'build--success-prepared-tests'); + }, + TEST_TIMEOUT_MS, + ); + it( 'error - wrong scheme', async () => { @@ -81,6 +101,24 @@ export function registerSimulatorSnapshotSuite(runtime: SnapshotRuntime): void { }, TEST_TIMEOUT_MS, ); + + it( + 'error - prepared tests with wrong scheme', + async () => { + const { text } = await harness.invoke('simulator', 'build', { + workspacePath: WORKSPACE, + scheme: INVALID_SCHEME, + simulatorName: SIMULATOR_NAME, + buildForTesting: true, + testProductsPath: path.join( + testProductsDir, + 'Invalid CalculatorApp Tests.xctestproducts', + ), + }); + expectFixture(text, 'build--error-prepared-tests-wrong-scheme'); + }, + TEST_TIMEOUT_MS, + ); }); describe('build-and-run', () => { diff --git a/src/test-utils/test-helpers.ts b/src/test-utils/test-helpers.ts index 7285077d0..26d76b643 100644 --- a/src/test-utils/test-helpers.ts +++ b/src/test-utils/test-helpers.ts @@ -34,6 +34,7 @@ export interface MockToolHandlerResult { events: AnyFragment[]; attachments: ImageAttachment[]; nextStepParams?: NextStepParamsMap; + nextStepConditionKeys?: string[]; text(): string; isError(): boolean; } @@ -62,6 +63,9 @@ export function createMockToolHandlerContext(): { get nextStepParams() { return ctx.nextStepParams; }, + get nextStepConditionKeys() { + return ctx.nextStepConditionKeys; + }, text() { return renderCliTextTranscript({ items: [], diff --git a/src/utils/__tests__/snapshot-normalize.test.ts b/src/utils/__tests__/snapshot-normalize.test.ts index 2448d58e4..6a1d033e5 100644 --- a/src/utils/__tests__/snapshot-normalize.test.ts +++ b/src/utils/__tests__/snapshot-normalize.test.ts @@ -6,7 +6,9 @@ describe('normalizeSnapshotOutput tilde handling', () => { const input = 'Workspace Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/Weather-abc123def456/logs\n'; const result = normalizeSnapshotOutput(input); - expect(result).toContain('~/Library/Developer/XcodeBuildMCP/workspaces/Weather-/logs'); + expect(result).toContain( + '~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs', + ); expect(result).not.toContain('Weather-abc123def456'); }); @@ -50,10 +52,10 @@ describe('normalizeSnapshotOutput tilde handling', () => { const result = normalizeSnapshotOutput(input); expect(result).toContain( - 'Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/Weather-/logs/build_sim__pid.log', + 'Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log', ); expect(result).toContain( - 'Runtime Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/Weather-/logs/io.app__pid.log', + 'Runtime Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/io.app__pid.log', ); }); @@ -64,7 +66,7 @@ describe('normalizeSnapshotOutput tilde handling', () => { const result = normalizeSnapshotOutput(input); expect(result).toContain( - '~/Library/Developer/XcodeBuildMCP/workspaces/Weather-/result-bundles/test_macos__pid.xcresult', + '~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_macos__pid.xcresult', ); expect(result).not.toContain('Weather-abc123def456'); expect(result).not.toContain('abcd1234'); @@ -77,7 +79,7 @@ describe('normalizeSnapshotOutput tilde handling', () => { const result = normalizeSnapshotOutput(input); expect(result).toContain( - '~/Library/Developer/XcodeBuildMCP/workspaces/Weather-/DerivedData/CalculatorApp-', + '~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-', ); expect(result).not.toContain('Weather-abc123def456'); expect(result).not.toContain('22d700c6d603'); From e359897358145fcdc64161d8b53f9cb47cb9c013 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 16 Jul 2026 17:48:26 +0100 Subject: [PATCH 03/23] fix(testing): Stabilize prepared test execution Keep source-only xcodebuild arguments out of the generated test-without-building phase and sort final test failures independently of emission order. --- .../__tests__/prepared-test-execution.test.ts | 45 +++++++++++++++++++ .../xcodebuild-domain-results.test.ts | 35 +++++++++++++++ src/utils/test-common.ts | 3 +- src/utils/test-source.ts | 24 ++++++++++ src/utils/xcodebuild-domain-results.ts | 18 +++++--- 5 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/utils/__tests__/prepared-test-execution.test.ts b/src/utils/__tests__/prepared-test-execution.test.ts index f9e5399f6..aca8b7ddd 100644 --- a/src/utils/__tests__/prepared-test-execution.test.ts +++ b/src/utils/__tests__/prepared-test-execution.test.ts @@ -107,4 +107,49 @@ describe('prepared test execution', () => { expect(commands[0]!.at(-1)).toBe('test-without-building'); expect(result.artifacts.xctestrunPath).toBe('/tmp/Weather.xctestrun'); }); + + it('does not forward source-only arguments to the prepared test phase', async () => { + const commands: string[][] = []; + const executor: CommandExecutor = async (command) => { + commands.push(command); + return createMockCommandResponse({ success: true, output: '', exitCode: 0 }); + }; + const executeTest = createTestExecutor(executor, { + toolName: 'test_macos', + target: 'macos', + request: { + scheme: 'Weather', + projectPath: 'Weather.xcodeproj', + platform: XcodePlatform.macOS, + }, + }); + + await executeTest( + { + scheme: 'Weather', + projectPath: 'Weather.xcodeproj', + platform: XcodePlatform.macOS, + extraArgs: [ + '-destination', + 'platform=macOS,arch=arm64', + '-scheme=Injected', + '-derivedDataPath', + '/tmp/OtherDerivedData', + '-quiet', + '-only-testing:WeatherTests/testWeather', + ], + }, + new DefaultStreamingExecutionContext(), + ); + + expect(commands).toHaveLength(2); + expect(commands[0]).toContain('-destination'); + expect(commands[0]).toContain('-scheme=Injected'); + expect(commands[0]).toContain('-derivedDataPath'); + expect(commands[1]).not.toContain('platform=macOS,arch=arm64'); + expect(commands[1]).not.toContain('-scheme=Injected'); + expect(commands[1]).not.toContain('/tmp/OtherDerivedData'); + expect(commands[1]).toContain('-quiet'); + expect(commands[1]).toContain('-only-testing:WeatherTests/testWeather'); + }); }); diff --git a/src/utils/__tests__/xcodebuild-domain-results.test.ts b/src/utils/__tests__/xcodebuild-domain-results.test.ts index e1dd9bed4..517d567f3 100644 --- a/src/utils/__tests__/xcodebuild-domain-results.test.ts +++ b/src/utils/__tests__/xcodebuild-domain-results.test.ts @@ -126,6 +126,41 @@ describe('xcodebuild-domain-results', () => { expect(result.summary.counts).toEqual({ passed: 16, failed: 0, skipped: 0 }); }); + it('sorts test failures independently of xcodebuild emission order', () => { + const runState = createXcodebuildRunState({ operation: 'TEST' }); + runState.push({ + kind: 'test-result', + fragment: 'test-failure', + operation: 'TEST', + suite: 'MCPTestsXCTests', + test: 'testDeliberateFailure()', + message: 'XCTAssertTrue failed', + location: 'MCPTestsXCTests.swift:11', + }); + runState.push({ + kind: 'test-result', + fragment: 'test-failure', + operation: 'TEST', + suite: 'MCPTestTests', + test: 'deliberateFailure()', + message: 'Expectation failed', + location: 'MCPTestTests.swift:11', + }); + + const result = createTestDomainResult({ + started: createStartedPipelineWithState(runState.finalize(false, 1000)), + succeeded: false, + target: 'macos', + artifacts: { buildLogPath: '/tmp/build.log' }, + request: { scheme: 'MCPTest' }, + }); + + expect(result.diagnostics.testFailures.map((failure) => failure.suite)).toEqual([ + 'MCPTestTests', + 'MCPTestsXCTests', + ]); + }); + it('does not duplicate fallback lines represented by multi-line parsed errors', () => { const runState = createXcodebuildRunState({ operation: 'BUILD' }); runState.push({ diff --git a/src/utils/test-common.ts b/src/utils/test-common.ts index bafee6513..8c7d7c316 100644 --- a/src/utils/test-common.ts +++ b/src/utils/test-common.ts @@ -27,6 +27,7 @@ import { } from './test-products-path.ts'; import { resolvePathFromCwd } from './path.ts'; import { displayPath } from './build-preflight.ts'; +import { filterPreparedTestExtraArgs } from './test-source.ts'; import type { BuildTarget, @@ -312,7 +313,7 @@ export function createTestExecutor( try { testWithoutBuildingResult = await executePreparedTestCommand( { ...params, testProductsPath }, - executionPlan.testArgs, + filterPreparedTestExtraArgs(executionPlan.testArgs), resultBundlePath, executor, execOpts, diff --git a/src/utils/test-source.ts b/src/utils/test-source.ts index 5fa231362..b3fbbe14c 100644 --- a/src/utils/test-source.ts +++ b/src/utils/test-source.ts @@ -50,6 +50,30 @@ function getConflictingPreparedTestArgs(extraArgs: unknown): string[] { ); } +export function filterPreparedTestExtraArgs(extraArgs: string[]): string[] { + const filteredArgs: string[] = []; + + for (let index = 0; index < extraArgs.length; index += 1) { + const argument = extraArgs[index]!; + const argumentKey = getArgumentKey(argument); + + if (conflictingPreparedTestActions.has(argument)) { + continue; + } + + if (conflictingPreparedTestArgKeys.has(argumentKey)) { + if (argument === argumentKey && index + 1 < extraArgs.length) { + index += 1; + } + continue; + } + + filteredArgs.push(argument); + } + + return filteredArgs; +} + export function hasPreparedTestSource(params: { testProductsPath?: string; xctestrunPath?: string; diff --git a/src/utils/xcodebuild-domain-results.ts b/src/utils/xcodebuild-domain-results.ts index e9ddae174..0f8a7bfb0 100644 --- a/src/utils/xcodebuild-domain-results.ts +++ b/src/utils/xcodebuild-domain-results.ts @@ -156,12 +156,18 @@ function createTestDiagnostics( didError, state.testFailures.length === 0 ? fallbackErrorMessages : undefined, ), - testFailures: state.testFailures.map((failure) => ({ - suite: failure.suite ?? '(Unknown Suite)', - test: failure.test ?? 'test', - message: failure.message, - location: failure.location, - })), + testFailures: state.testFailures + .map((failure) => ({ + suite: failure.suite ?? '(Unknown Suite)', + test: failure.test ?? 'test', + message: failure.message, + location: failure.location, + })) + .sort((left, right) => { + const leftKey = `${left.suite}\0${left.test}\0${left.location}\0${left.message}`; + const rightKey = `${right.suite}\0${right.test}\0${right.location}\0${right.message}`; + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; + }), }; } From 88da78296088c2fa0fa79ae53167173ef0ca2b97 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 16 Jul 2026 18:39:34 +0100 Subject: [PATCH 04/23] test(snapshot): Align device test schema versions --- .../__fixtures__/cli/json/device/test--error-compiler.json | 2 +- .../__fixtures__/cli/json/device/test--failure.json | 2 +- .../__fixtures__/cli/json/device/test--success.json | 2 +- .../__fixtures__/mcp/json/device/test--error-compiler.json | 2 +- .../__fixtures__/mcp/json/device/test--failure.json | 2 +- .../__fixtures__/mcp/json/device/test--success.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/test--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/device/test--error-compiler.json index be5d20863..171802f2e 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/test--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/test--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/test--failure.json b/src/snapshot-tests/__fixtures__/cli/json/device/test--failure.json index 3d943d75c..a0256775c 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/test--failure.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/test--failure.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/test--success.json b/src/snapshot-tests/__fixtures__/cli/json/device/test--success.json index fa8687bb8..deb900d65 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/test--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/test--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/device/test--error-compiler.json b/src/snapshot-tests/__fixtures__/mcp/json/device/test--error-compiler.json index 1a2283fbc..593fb6b19 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/device/test--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/device/test--error-compiler.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/device/test--failure.json b/src/snapshot-tests/__fixtures__/mcp/json/device/test--failure.json index 4ba8d6390..517f6e8dc 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/device/test--failure.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/device/test--failure.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": true, "error": "Tests failed", "data": { diff --git a/src/snapshot-tests/__fixtures__/mcp/json/device/test--success.json b/src/snapshot-tests/__fixtures__/mcp/json/device/test--success.json index 9f2267bd5..487100a4d 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/device/test--success.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/device/test--success.json @@ -1,6 +1,6 @@ { "schema": "xcodebuildmcp.output.test-result", - "schemaVersion": "2", + "schemaVersion": "3", "didError": false, "error": null, "data": { From e7b8d5388f8965208dbf1d5c3598b87dcbb5e92f Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 16 Jul 2026 19:57:07 +0100 Subject: [PATCH 05/23] fix(test): Extract failures from managed result bundles --- src/utils/__tests__/test-common.test.ts | 8 ++++++++ src/utils/test-common.ts | 14 ++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/utils/__tests__/test-common.test.ts b/src/utils/__tests__/test-common.test.ts index 1678abbc3..828af3c7f 100644 --- a/src/utils/__tests__/test-common.test.ts +++ b/src/utils/__tests__/test-common.test.ts @@ -16,6 +16,12 @@ import { import { setRuntimeInstanceForTests } from '../runtime-instance.ts'; import { resetWorkspaceFilesystemLifecycleStateForTests } from '../workspace-filesystem-lifecycle.ts'; import { getTestProductsCompletionMarkerPath } from '../test-products-path.ts'; +import { extractTestFailuresFromXcresult } from '../xcresult-test-failures.ts'; + +vi.mock('../xcresult-test-failures.ts', () => ({ + extractTestFailuresFromXcresult: vi.fn(() => []), + extractTestSummaryCountsFromXcresult: vi.fn(() => null), +})); function createSuccessfulCommandResponse(): CommandResponse { return { @@ -107,6 +113,7 @@ describe('createTestExecutor', () => { let tempAppDir: string; beforeEach(() => { + vi.clearAllMocks(); tempAppDir = mkdtempSync(join(tmpdir(), 'xcodebuildmcp-result-bundles-')); setXcodeBuildMCPAppDirOverrideForTests(tempAppDir); setRuntimeInstanceForTests({ @@ -226,6 +233,7 @@ describe('createTestExecutor', () => { expect(commands[1]).not.toContain('-project'); expect(commands[1]).not.toContain('-scheme'); expect(commands[1]).not.toContain('-derivedDataPath'); + expect(extractTestFailuresFromXcresult).toHaveBeenCalledWith(resultBundlePath); expect(result.artifacts.xcresultPath).toBe(resultBundlePath); expect(result.artifacts.testProductsPath).toEqual( expect.stringContaining(getWorkspaceFilesystemLayout('workspace-a').testProducts), diff --git a/src/utils/test-common.ts b/src/utils/test-common.ts index 8c7d7c316..5403d4f3e 100644 --- a/src/utils/test-common.ts +++ b/src/utils/test-common.ts @@ -44,13 +44,11 @@ import { function emitXcresultFailures( pipeline: ReturnType['pipeline'], + xcresultPath: string, ): void { - const xcresultPath = pipeline.xcresultPath; - if (xcresultPath) { - const failures = extractTestFailuresFromXcresult(xcresultPath); - for (const event of failures) { - pipeline.emitFragment(event); - } + const failures = extractTestFailuresFromXcresult(xcresultPath); + for (const event of failures) { + pipeline.emitFragment(event); } } @@ -325,7 +323,7 @@ export function createTestExecutor( markResultBundlePathCompleted(resultBundlePath); } } - emitXcresultFailures(started.pipeline); + emitXcresultFailures(started.pipeline, resultBundlePath); return createDisplayedTestDomainResult({ started, @@ -366,7 +364,7 @@ export function createTestExecutor( markResultBundlePathCompleted(resultBundlePath); } } - emitXcresultFailures(started.pipeline); + emitXcresultFailures(started.pipeline, resultBundlePath); return createDisplayedTestDomainResult({ started, From f95b573f401eb85ba9089801a95673d5d9f233c6 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 16 Jul 2026 20:08:46 +0100 Subject: [PATCH 06/23] fix(test): Preserve explicit test destinations --- .../__tests__/prepared-test-execution.test.ts | 3 ++- src/utils/test-common.ts | 9 ++++++--- src/utils/test-source.ts | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/utils/__tests__/prepared-test-execution.test.ts b/src/utils/__tests__/prepared-test-execution.test.ts index aca8b7ddd..cfc5b838a 100644 --- a/src/utils/__tests__/prepared-test-execution.test.ts +++ b/src/utils/__tests__/prepared-test-execution.test.ts @@ -146,7 +146,8 @@ describe('prepared test execution', () => { expect(commands[0]).toContain('-destination'); expect(commands[0]).toContain('-scheme=Injected'); expect(commands[0]).toContain('-derivedDataPath'); - expect(commands[1]).not.toContain('platform=macOS,arch=arm64'); + expect(commands[1]).toContain('platform=macOS,arch=arm64'); + expect(commands[1].filter((argument) => argument === '-destination')).toHaveLength(1); expect(commands[1]).not.toContain('-scheme=Injected'); expect(commands[1]).not.toContain('/tmp/OtherDerivedData'); expect(commands[1]).toContain('-quiet'); diff --git a/src/utils/test-common.ts b/src/utils/test-common.ts index 5403d4f3e..098163ad5 100644 --- a/src/utils/test-common.ts +++ b/src/utils/test-common.ts @@ -27,7 +27,7 @@ import { } from './test-products-path.ts'; import { resolvePathFromCwd } from './path.ts'; import { displayPath } from './build-preflight.ts'; -import { filterPreparedTestExtraArgs } from './test-source.ts'; +import { filterPreparedTestExtraArgs, getPreparedTestDestinationArgs } from './test-source.ts'; import type { BuildTarget, @@ -167,6 +167,7 @@ async function executePreparedTestCommand( executor: CommandExecutor, execOpts: CommandExecOptions | undefined, pipeline: ReturnType['pipeline'], + destinationArgs?: string[], ): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> { const sourceArgs = params.testProductsPath ? ['-testProductsPath', resolvePathFromCwd(params.testProductsPath)] @@ -190,8 +191,9 @@ async function executePreparedTestCommand( const command = [ 'xcodebuild', ...sourceArgs, - '-destination', - destination, + ...(destinationArgs && destinationArgs.length > 0 + ? destinationArgs + : ['-destination', destination]), '-collect-test-diagnostics', 'never', ...extraArgs, @@ -316,6 +318,7 @@ export function createTestExecutor( executor, execOpts, started.pipeline, + getPreparedTestDestinationArgs(executionPlan.testArgs), ); } finally { markTestProductsPathCompleted(testProductsPath); diff --git a/src/utils/test-source.ts b/src/utils/test-source.ts index b3fbbe14c..96c0c98d0 100644 --- a/src/utils/test-source.ts +++ b/src/utils/test-source.ts @@ -74,6 +74,25 @@ export function filterPreparedTestExtraArgs(extraArgs: string[]): string[] { return filteredArgs; } +export function getPreparedTestDestinationArgs(extraArgs: string[]): string[] { + const destinationArgs: string[] = []; + + for (let index = 0; index < extraArgs.length; index += 1) { + const argument = extraArgs[index]!; + if (getArgumentKey(argument) !== '-destination') { + continue; + } + + destinationArgs.push(argument); + if (argument === '-destination' && index + 1 < extraArgs.length) { + destinationArgs.push(extraArgs[index + 1]!); + index += 1; + } + } + + return destinationArgs; +} + export function hasPreparedTestSource(params: { testProductsPath?: string; xctestrunPath?: string; From 6c45d0b42aa93c0994c44ff1ee4d50af5701b5c8 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Fri, 17 Jul 2026 19:00:25 +0100 Subject: [PATCH 07/23] test(snapshot): isolate snapshot preflight state Share per-test environment setup and cleanup across snapshot suites, cache suite-level simulator builds, and normalize volatile output so fixtures remain stable. Update non-environmental fixtures and add coverage for cleanup and normalization behavior. --- .../CalculatorApp/CalculatorApp.swift | 22 +- package.json | 6 +- .../get-coverage-report--success.json | 8 +- .../coverage/get-file-coverage--success.json | 117 +++++- .../json/debugging/lldb-command--success.json | 7 +- .../json/device/build--error-compiler.json | 5 +- ...ld--error-prepared-tests-wrong-scheme.json | 2 +- .../device/build--error-wrong-scheme.json | 3 +- ...build--success-prepared-tests-generic.json | 2 +- .../cli/json/device/build--success.json | 5 +- .../get-app-path--error-wrong-scheme.json | 1 - .../json/device/get-app-path--success.json | 7 +- .../cli/json/macos/build--error-compiler.json | 3 +- ...ld--error-prepared-tests-wrong-scheme.json | 2 +- .../json/macos/build--error-wrong-scheme.json | 3 +- .../macos/build--success-prepared-tests.json | 2 +- .../cli/json/macos/build--success.json | 5 +- .../macos/build-and-run--error-compiler.json | 3 +- .../build-and-run--error-wrong-scheme.json | 3 +- .../json/macos/build-and-run--success.json | 5 +- .../get-app-path--error-wrong-scheme.json | 1 - .../cli/json/macos/get-app-path--success.json | 7 +- ...et-macos-bundle-id--error-missing-app.json | 4 +- .../cli/json/macos/launch--success.json | 2 +- .../cli/json/macos/stop--success.json | 3 +- .../cli/json/macos/test--error-compiler.json | 3 +- .../json/macos/test--error-wrong-scheme.json | 3 +- .../cli/json/macos/test--failure.json | 3 +- .../cli/json/macos/test--success.json | 3 +- .../simulator-management/list--success.json | 266 ++++++++++++ .../json/simulator/build--error-compiler.json | 7 +- ...ld--error-prepared-tests-wrong-scheme.json | 4 +- .../simulator/build--error-wrong-scheme.json | 5 +- .../build--success-prepared-tests.json | 6 +- .../cli/json/simulator/build--success.json | 7 +- .../build-and-run--error-compiler.json | 7 +- .../build-and-run--error-wrong-scheme.json | 5 +- .../simulator/build-and-run--success.json | 7 +- .../get-app-path--error-wrong-scheme.json | 3 +- .../json/simulator/get-app-path--success.json | 9 +- .../cli/json/simulator/install--success.json | 2 +- .../json/simulator/test--error-compiler.json | 7 +- .../simulator/test--error-wrong-scheme.json | 5 +- .../cli/json/simulator/test--failure.json | 5 +- .../cli/json/simulator/test--success.json | 5 +- .../swift-package/build--error-bad-path.json | 6 +- .../json/swift-package/build--success.json | 4 +- .../swift-package/clean--error-bad-path.json | 4 +- .../json/swift-package/clean--success.json | 2 +- .../cli/json/swift-package/list--success.json | 2 +- .../run--error-bad-executable.json | 4 +- .../cli/json/swift-package/run--success.json | 15 +- .../swift-package/test--error-bad-path.json | 4 +- .../cli/json/swift-package/test--failure.json | 12 +- .../ui-automation/long-press--success.json | 4 +- .../swipe--error-not-actionable.json | 4 +- .../json/ui-automation/swipe--success.json | 54 +-- .../ui-automation/tap--success-verbose.json | 6 +- .../cli/json/ui-automation/tap--success.json | 2 +- .../json/ui-automation/touch--success.json | 4 +- .../type-text--error-not-actionable.json | 4 +- .../coverage/get-coverage-report--success.txt | 4 +- .../coverage/get-file-coverage--success.txt | 25 +- .../text/debugging/lldb-command--success.txt | 7 +- .../cli/text/device/build--error-compiler.txt | 5 +- ...ild--error-prepared-tests-wrong-scheme.txt | 2 +- .../text/device/build--error-wrong-scheme.txt | 3 +- .../build--success-prepared-tests-generic.txt | 2 +- .../cli/text/device/build--success.txt | 5 +- .../device/build-and-run--error-compiler.txt | 7 +- .../build-and-run--error-wrong-scheme.txt | 5 +- .../text/device/build-and-run--success.txt | 7 +- .../get-app-path--error-wrong-scheme.txt | 1 - .../cli/text/device/get-app-path--success.txt | 7 +- .../cli/text/device/install--success.txt | 4 +- .../cli/text/device/launch--success.txt | 2 +- .../cli/text/device/stop--success.txt | 2 +- .../cli/text/device/test--error-compiler.txt | 8 +- .../cli/text/device/test--failure.txt | 14 +- .../cli/text/device/test--success.txt | 8 +- .../cli/text/macos/build--error-compiler.txt | 5 +- ...ild--error-prepared-tests-wrong-scheme.txt | 2 +- .../text/macos/build--error-wrong-scheme.txt | 3 +- .../macos/build--success-prepared-tests.txt | 2 +- .../cli/text/macos/build--success.txt | 5 +- .../macos/build-and-run--error-compiler.txt | 5 +- .../build-and-run--error-wrong-scheme.txt | 3 +- .../cli/text/macos/build-and-run--success.txt | 5 +- .../get-app-path--error-wrong-scheme.txt | 1 - .../cli/text/macos/get-app-path--success.txt | 7 +- ...get-macos-bundle-id--error-missing-app.txt | 4 +- .../cli/text/macos/launch--success.txt | 2 +- .../cli/text/macos/stop--success.txt | 2 +- .../cli/text/macos/test--error-compiler.txt | 5 +- .../text/macos/test--error-wrong-scheme.txt | 3 +- .../cli/text/macos/test--failure.txt | 3 +- .../cli/text/macos/test--success.txt | 3 +- .../text/simulator/build--error-compiler.txt | 7 +- ...ild--error-prepared-tests-wrong-scheme.txt | 4 +- .../simulator/build--error-wrong-scheme.txt | 5 +- .../build--success-prepared-tests.txt | 6 +- .../cli/text/simulator/build--success.txt | 7 +- .../build-and-run--error-compiler.txt | 7 +- .../build-and-run--error-wrong-scheme.txt | 5 +- .../text/simulator/build-and-run--success.txt | 7 +- .../get-app-path--error-wrong-scheme.txt | 3 +- .../text/simulator/get-app-path--success.txt | 9 +- .../cli/text/simulator/install--success.txt | 2 +- .../text/simulator/test--error-compiler.txt | 7 +- .../simulator/test--error-wrong-scheme.txt | 5 +- .../cli/text/simulator/test--failure.txt | 9 +- .../cli/text/simulator/test--success.txt | 5 +- .../swift-package/build--error-bad-path.txt | 4 +- .../cli/text/swift-package/build--success.txt | 2 +- .../swift-package/clean--error-bad-path.txt | 4 +- .../cli/text/swift-package/clean--success.txt | 2 +- .../cli/text/swift-package/list--success.txt | 2 +- .../run--error-bad-executable.txt | 2 +- .../cli/text/swift-package/run--success.txt | 4 +- .../swift-package/test--error-bad-path.txt | 2 +- .../cli/text/swift-package/test--failure.txt | 2 +- .../ui-automation/snapshot-ui--success.txt | 2 +- .../swipe--error-not-actionable.txt | 6 +- .../cli/text/ui-automation/swipe--success.txt | 3 +- .../type-text--error-not-actionable.txt | 6 +- .../ui-automation/wait-for-ui--success.txt | 2 +- .../json/ui-automation/swipe--success.json | 54 +-- .../coverage/get-coverage-report--success.txt | 4 +- .../coverage/get-file-coverage--success.txt | 25 +- .../text/debugging/lldb-command--success.txt | 7 +- .../mcp/text/device/build--error-compiler.txt | 2 +- .../mcp/text/device/build--success.txt | 2 +- .../device/build-and-run--error-compiler.txt | 2 +- .../text/device/build-and-run--success.txt | 5 +- .../mcp/text/device/get-app-path--success.txt | 6 +- .../mcp/text/device/test--error-compiler.txt | 6 +- .../mcp/text/device/test--failure.txt | 3 +- .../mcp/text/device/test--success.txt | 3 +- .../mcp/text/macos/build--success.txt | 2 +- .../mcp/text/macos/build-and-run--success.txt | 5 +- .../mcp/text/macos/get-app-path--success.txt | 6 +- ...get-macos-bundle-id--error-missing-app.txt | 2 +- .../text/simulator/build--error-compiler.txt | 2 +- .../build--success-prepared-tests.txt | 2 +- .../mcp/text/simulator/build--success.txt | 2 +- .../build-and-run--error-compiler.txt | 2 +- .../text/simulator/build-and-run--success.txt | 11 +- .../text/simulator/get-app-path--success.txt | 6 +- .../text/simulator/test--error-compiler.txt | 2 +- .../swift-package/build--error-bad-path.txt | 2 +- .../swift-package/clean--error-bad-path.txt | 2 +- .../mcp/text/swift-package/list--success.txt | 2 +- .../mcp/text/swift-package/run--success.txt | 4 +- .../swift-package/test--error-bad-path.txt | 2 +- .../mcp/text/swift-package/test--failure.txt | 6 +- .../ui-automation/snapshot-ui--success.txt | 2 +- .../swipe--error-not-actionable.txt | 6 +- .../mcp/text/ui-automation/swipe--success.txt | 3 +- .../type-text--error-not-actionable.txt | 6 +- .../ui-automation/wait-for-ui--success.txt | 2 +- .../__tests__/fixture-io.test.ts | 112 +++++- .../json-harness-error-state.test.ts | 35 +- .../__tests__/json-normalize.test.ts | 21 +- .../__tests__/mcp-harness.test.ts | 19 + .../__tests__/normalize-project-root.test.ts | 17 + .../__tests__/normalize.test.ts | 82 ++++ .../xcode-ide-artifact-cleanup.test.ts | 97 +++++ src/snapshot-tests/contracts.ts | 7 + src/snapshot-tests/device-availability.ts | 22 +- src/snapshot-tests/fixture-io.ts | 37 ++ src/snapshot-tests/harness.ts | 225 +++++------ src/snapshot-tests/json-harness.ts | 3 +- src/snapshot-tests/json-normalize.ts | 12 + src/snapshot-tests/mcp-harness.ts | 52 ++- src/snapshot-tests/normalize.ts | 65 ++- .../preflight/__tests__/cleanup.test.ts | 41 ++ .../__tests__/command-runner.test.ts | 76 ++++ .../preflight/__tests__/device.test.ts | 119 ++++++ .../__tests__/simulator-state.test.ts | 82 ++++ .../preflight/__tests__/simulator.test.ts | 138 +++++++ .../preflight/__tests__/xcodebuild.test.ts | 46 +++ src/snapshot-tests/preflight/cleanup.ts | 32 ++ .../preflight/command-runner.ts | 141 +++++++ src/snapshot-tests/preflight/device.ts | 162 ++++++++ .../preflight/simulator-state.ts | 109 +++++ src/snapshot-tests/preflight/simulator.ts | 245 +++++++++++ src/snapshot-tests/preflight/xcode-state.ts | 15 + src/snapshot-tests/preflight/xcodebuild.ts | 63 +++ src/snapshot-tests/resource-harness.ts | 2 +- src/snapshot-tests/suites/coverage-suite.ts | 125 +++--- src/snapshot-tests/suites/debugging-suite.ts | 319 ++++++++++----- src/snapshot-tests/suites/device-suite.ts | 208 ++++++---- src/snapshot-tests/suites/helpers.ts | 25 +- src/snapshot-tests/suites/macos-suite.ts | 294 +++++++++----- .../suites/project-discovery-suite.ts | 121 +++--- .../suites/project-scaffolding-suite.ts | 55 ++- src/snapshot-tests/suites/resources-suite.ts | 6 +- .../suites/session-management-suite.ts | 171 ++++---- .../suites/simulator-management-suite.ts | 313 +++++++++------ src/snapshot-tests/suites/simulator-suite.ts | 233 ++++++----- .../suites/swift-package-suite.ts | 238 ++++++----- .../suites/ui-automation-suite.ts | 379 +++++++++--------- src/snapshot-tests/suites/utilities-suite.ts | 33 +- src/snapshot-tests/suites/xcode-ide-suite.ts | 297 ++++++++------ src/snapshot-tests/xcode-ide-availability.ts | 12 + src/utils/__tests__/build-preflight.test.ts | 12 +- src/utils/build-preflight.ts | 15 +- 207 files changed, 4393 insertions(+), 1768 deletions(-) create mode 100644 src/snapshot-tests/__tests__/mcp-harness.test.ts create mode 100644 src/snapshot-tests/__tests__/normalize-project-root.test.ts create mode 100644 src/snapshot-tests/__tests__/xcode-ide-artifact-cleanup.test.ts create mode 100644 src/snapshot-tests/preflight/__tests__/cleanup.test.ts create mode 100644 src/snapshot-tests/preflight/__tests__/command-runner.test.ts create mode 100644 src/snapshot-tests/preflight/__tests__/device.test.ts create mode 100644 src/snapshot-tests/preflight/__tests__/simulator-state.test.ts create mode 100644 src/snapshot-tests/preflight/__tests__/simulator.test.ts create mode 100644 src/snapshot-tests/preflight/__tests__/xcodebuild.test.ts create mode 100644 src/snapshot-tests/preflight/cleanup.ts create mode 100644 src/snapshot-tests/preflight/command-runner.ts create mode 100644 src/snapshot-tests/preflight/device.ts create mode 100644 src/snapshot-tests/preflight/simulator-state.ts create mode 100644 src/snapshot-tests/preflight/simulator.ts create mode 100644 src/snapshot-tests/preflight/xcode-state.ts create mode 100644 src/snapshot-tests/preflight/xcodebuild.ts diff --git a/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift b/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift index 6125b83cd..ff5d4d55e 100644 --- a/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift +++ b/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift @@ -3,6 +3,7 @@ import OSLog import CalculatorAppFeature private let logger = Logger(subsystem: "io.sentry.calculatorapp", category: "lifecycle") +private let snapshotScrollSurfaceArgument = "--snapshot-scroll-surface" @main struct CalculatorApp: App { @@ -10,7 +11,11 @@ struct CalculatorApp: App { var body: some Scene { WindowGroup { - ContentView() + if ProcessInfo.processInfo.arguments.contains(snapshotScrollSurfaceArgument) { + SnapshotScrollSurface() + } else { + ContentView() + } } .onChange(of: scenePhase) { _, newPhase in switch newPhase { @@ -25,6 +30,21 @@ struct CalculatorApp: App { } } +private struct SnapshotScrollSurface: View { + var body: some View { + ScrollView { + LazyVStack(spacing: 16) { + ForEach(0..<30, id: \.self) { index in + Text("Snapshot row \(index)") + .frame(maxWidth: .infinity, minHeight: 44) + } + } + .padding() + } + .accessibilityIdentifier("snapshot-scroll-surface") + } +} + #Preview { ContentView() } diff --git a/package.json b/package.json index 7872e1c4b..235f8fc3f 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,10 @@ "knip": "knip", "test": "vitest run", "test:schema-fixtures": "vitest run src/snapshot-tests/__tests__/json-fixture-schema.test.ts", - "test:snapshot": "npm run build && node build/cli.js daemon stop 2>/dev/null; vitest run --config vitest.snapshot.config.ts", + "test:snapshot": "npm run build && vitest run --config vitest.snapshot.config.ts", "test:snapshots": "npm run test:snapshot", - "test:snapshot:device": "npm run build && node build/cli.js daemon stop 2>/dev/null; vitest run --config vitest.snapshot.config.ts src/snapshot-tests/__tests__/device.snapshot.test.ts && vitest run --config vitest.snapshot.config.ts src/snapshot-tests/__tests__/cli-json-fixture-parity.snapshot.test.ts src/snapshot-tests/__tests__/mcp-json-fixture-parity.snapshot.test.ts -t 'device workflow'", - "test:snapshot:update": "npm run build && node build/cli.js daemon stop 2>/dev/null; UPDATE_SNAPSHOTS=1 vitest run --config vitest.snapshot.config.ts", + "test:snapshot:device": "npm run build && vitest run --config vitest.snapshot.config.ts src/snapshot-tests/__tests__/device.snapshot.test.ts && vitest run --config vitest.snapshot.config.ts src/snapshot-tests/__tests__/cli-json.snapshot.test.ts src/snapshot-tests/__tests__/mcp-json.snapshot.test.ts -t 'device workflow'", + "test:snapshot:update": "npm run build && UPDATE_SNAPSHOTS=1 vitest run --config vitest.snapshot.config.ts", "test:snapshots:update": "npm run test:snapshot:update", "test:smoke": "npm run build && vitest run --config vitest.smoke.config.ts", "test:watch": "vitest", diff --git a/src/snapshot-tests/__fixtures__/cli/json/coverage/get-coverage-report--success.json b/src/snapshot-tests/__fixtures__/cli/json/coverage/get-coverage-report--success.json index 587947ba3..2ecc445e4 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/coverage/get-coverage-report--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/coverage/get-coverage-report--success.json @@ -6,8 +6,8 @@ "data": { "summary": { "status": "SUCCEEDED", - "coveragePct": 94.9, - "coveredLines": 371, + "coveragePct": 4.6, + "coveredLines": 18, "executableLines": 391 }, "coverageScope": "report", @@ -18,8 +18,8 @@ "targets": [ { "name": "CalculatorAppTests.xctest", - "coveragePct": 94.9, - "coveredLines": 371, + "coveragePct": 4.6, + "coveredLines": 18, "executableLines": 391 } ] diff --git a/src/snapshot-tests/__fixtures__/cli/json/coverage/get-file-coverage--success.json b/src/snapshot-tests/__fixtures__/cli/json/coverage/get-file-coverage--success.json index ecc52c88c..34594ab0e 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/coverage/get-file-coverage--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/coverage/get-file-coverage--success.json @@ -6,8 +6,8 @@ "data": { "summary": { "status": "SUCCEEDED", - "coveragePct": 83.1, - "coveredLines": 157, + "coveragePct": 55, + "coveredLines": 104, "executableLines": 189 }, "coverageScope": "file", @@ -24,12 +24,54 @@ "coveredLines": 0, "executableLines": 16 }, + { + "line": 63, + "name": "CalculatorService.inputDecimal()", + "coveredLines": 0, + "executableLines": 14 + }, + { + "line": 148, + "name": "CalculatorService.clear()", + "coveredLines": 0, + "executableLines": 10 + }, + { + "line": 134, + "name": "CalculatorService.toggleSign()", + "coveredLines": 0, + "executableLines": 6 + }, + { + "line": 141, + "name": "CalculatorService.percentage()", + "coveredLines": 0, + "executableLines": 6 + }, + { + "line": 178, + "name": "CalculatorService.setError(_:)", + "coveredLines": 0, + "executableLines": 5 + }, { "line": 58, "name": "implicit closure #2 in CalculatorService.inputNumber(_:)", "coveredLines": 0, "executableLines": 1 }, + { + "line": 68, + "name": "implicit closure #1 in CalculatorService.inputDecimal()", + "coveredLines": 0, + "executableLines": 1 + }, + { + "line": 94, + "name": "implicit closure #1 in CalculatorService.calculate()", + "coveredLines": 0, + "executableLines": 1 + }, { "line": 98, "name": "implicit closure #3 in CalculatorService.calculate()", @@ -54,11 +96,41 @@ "coveredLines": 0, "executableLines": 1 }, + { + "line": 209, + "name": "implicit closure #3 in CalculatorService.formatNumber(_:)", + "coveredLines": 0, + "executableLines": 1 + }, { "line": 214, "name": "implicit closure #4 in CalculatorService.formatNumber(_:)", "coveredLines": 0, "executableLines": 1 + }, + { + "line": 221, + "name": "CalculatorService.currentValue.getter", + "coveredLines": 0, + "executableLines": 1 + }, + { + "line": 222, + "name": "CalculatorService.previousValue.getter", + "coveredLines": 0, + "executableLines": 1 + }, + { + "line": 223, + "name": "CalculatorService.currentOperation.getter", + "coveredLines": 0, + "executableLines": 1 + }, + { + "line": 224, + "name": "CalculatorService.willResetDisplay.getter", + "coveredLines": 0, + "executableLines": 1 } ], "partialCoverage": [ @@ -69,32 +141,39 @@ "coveredLines": 8, "executableLines": 10 }, - { - "line": 195, - "name": "CalculatorService.formatNumber(_:)", - "coveragePct": 85.7, - "coveredLines": 18, - "executableLines": 21 - }, { "line": 93, "name": "CalculatorService.calculate()", - "coveragePct": 89.5, - "coveredLines": 34, + "coveragePct": 84.2, + "coveredLines": 32, "executableLines": 38 }, { - "line": 63, - "name": "CalculatorService.inputDecimal()", - "coveragePct": 92.9, - "coveredLines": 13, + "line": 47, + "name": "CalculatorService.inputNumber(_:)", + "coveragePct": 85.7, + "coveredLines": 12, "executableLines": 14 + }, + { + "line": 78, + "name": "CalculatorService.setOperation(_:)", + "coveragePct": 85.7, + "coveredLines": 12, + "executableLines": 14 + }, + { + "line": 195, + "name": "CalculatorService.formatNumber(_:)", + "coveragePct": 85.7, + "coveredLines": 18, + "executableLines": 21 } ], - "fullCoverageCount": 28, - "notCoveredFunctionCount": 7, - "notCoveredLineCount": 22, - "partialCoverageFunctionCount": 4 + "fullCoverageCount": 15, + "notCoveredFunctionCount": 19, + "notCoveredLineCount": 70, + "partialCoverageFunctionCount": 5 } }, "nextSteps": [ diff --git a/src/snapshot-tests/__fixtures__/cli/json/debugging/lldb-command--success.json b/src/snapshot-tests/__fixtures__/cli/json/debugging/lldb-command--success.json index bc3e32531..98ef44cef 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/debugging/lldb-command--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/debugging/lldb-command--success.json @@ -6,12 +6,7 @@ "data": { "command": "breakpoint list", "outputLines": [ - "Current breakpoints:", - "1: file = 'ContentView.swift', line = 42, exact_match = 0, locations = ", - " Names:", - " dap", - "", - " 1.1: where = CalculatorApp.debug.dylib`closure #1 in closure #1 in closure #1 in ContentView.body.getter + at ContentView.swift:42:31, address = , resolved, hit count = 0" + "No breakpoints currently set." ] } } diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-compiler.json index 8f36176aa..7ed092d46 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-compiler.json @@ -7,8 +7,7 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS", "target": "device" }, @@ -25,7 +24,7 @@ "errors": [ { "message": "cannot convert value of type 'String' to specified type 'Int'", - "location": "/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33" + "location": "/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53" } ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-prepared-tests-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-prepared-tests-wrong-scheme.json index a150f5f4d..678e6cbfb 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-prepared-tests-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-prepared-tests-wrong-scheme.json @@ -8,7 +8,7 @@ "buildForTesting": true, "scheme": "NONEXISTENT", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", + "derivedDataPath": "", "platform": "iOS", "target": "device", "testProductsPath": "/Invalid CalculatorApp Tests.xctestproducts" diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-wrong-scheme.json index 4707feb58..258722b42 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/build--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--error-wrong-scheme.json @@ -7,8 +7,7 @@ "request": { "scheme": "NONEXISTENT", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS", "target": "device" }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--success-prepared-tests-generic.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--success-prepared-tests-generic.json index d256e5cc4..31ecf6e1b 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/build--success-prepared-tests-generic.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--success-prepared-tests-generic.json @@ -8,7 +8,7 @@ "buildForTesting": true, "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", + "derivedDataPath": "", "platform": "iOS", "target": "device", "testProductsPath": "/Generic CalculatorApp Tests.xctestproducts" diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/build--success.json b/src/snapshot-tests/__fixtures__/cli/json/device/build--success.json index 09731fd7e..91c6c1582 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/build--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/build--success.json @@ -7,8 +7,7 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS", "target": "device" }, @@ -26,6 +25,6 @@ } }, "nextSteps": [ - "Get built device app path: xcodebuildmcp device get-app-path --scheme CalculatorApp" + "Get built device app path: xcodebuildmcp device get-app-path --scheme CalculatorApp --derived-data-path " ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/get-app-path--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/device/get-app-path--error-wrong-scheme.json index 75a3a6c60..fdd1b8750 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/get-app-path--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/get-app-path--error-wrong-scheme.json @@ -7,7 +7,6 @@ "request": { "scheme": "NONEXISTENT", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "configuration": "Debug", "platform": "iOS" }, "summary": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/device/get-app-path--success.json b/src/snapshot-tests/__fixtures__/cli/json/device/get-app-path--success.json index 2ad03d5b5..26b81b162 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/device/get-app-path--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/device/get-app-path--success.json @@ -7,7 +7,6 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "configuration": "Debug", "platform": "iOS" }, "summary": { @@ -15,12 +14,12 @@ "target": "device" }, "artifacts": { - "appPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app" + "appPath": "/Build/Products/Debug-iphoneos/CalculatorApp.app" } }, "nextSteps": [ - "Get bundle ID: xcodebuildmcp device get-app-bundle-id --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app", - "Install app on device: xcodebuildmcp device install --device-id DEVICE_UDID --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app", + "Get bundle ID: xcodebuildmcp device get-app-bundle-id --app-path /Build/Products/Debug-iphoneos/CalculatorApp.app", + "Install app on device: xcodebuildmcp device install --device-id DEVICE_UDID --app-path /Build/Products/Debug-iphoneos/CalculatorApp.app", "Launch app on device: xcodebuildmcp device launch --device-id DEVICE_UDID --bundle-id BUNDLE_ID" ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-compiler.json index 26d58e83d..649a3d57a 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-compiler.json @@ -7,8 +7,7 @@ "request": { "scheme": "MCPTest", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "target": "macos" }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-prepared-tests-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-prepared-tests-wrong-scheme.json index 614556dc6..9c3a0b940 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-prepared-tests-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-prepared-tests-wrong-scheme.json @@ -8,7 +8,7 @@ "buildForTesting": true, "scheme": "NONEXISTENT", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", + "derivedDataPath": "/DerivedData", "platform": "macOS", "target": "macos", "testProductsPath": "/Invalid MCPTest Tests.xctestproducts" diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-wrong-scheme.json index d57658bdb..8d5465013 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--error-wrong-scheme.json @@ -7,8 +7,7 @@ "request": { "scheme": "NONEXISTENT", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "target": "macos" }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--success-prepared-tests.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--success-prepared-tests.json index 1945e5ff3..713eefed3 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build--success-prepared-tests.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--success-prepared-tests.json @@ -8,7 +8,7 @@ "buildForTesting": true, "scheme": "MCPTest", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", + "derivedDataPath": "/DerivedData", "platform": "macOS", "target": "macos", "testProductsPath": "/MCPTest Tests.xctestproducts" diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build--success.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build--success.json index 52e8f533d..332c4d5ec 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build--success.json @@ -7,8 +7,7 @@ "request": { "scheme": "MCPTest", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "target": "macos" }, @@ -27,6 +26,6 @@ } }, "nextSteps": [ - "Get built macOS app path: xcodebuildmcp macos get-app-path --scheme MCPTest" + "Get built macOS app path: xcodebuildmcp macos get-app-path --scheme MCPTest --derived-data-path /DerivedData" ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--error-compiler.json index 7ab611eab..5770a4ef0 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--error-compiler.json @@ -7,8 +7,7 @@ "request": { "scheme": "MCPTest", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "target": "macos" }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--error-wrong-scheme.json index 6c0e76acd..f2bd53240 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--error-wrong-scheme.json @@ -7,8 +7,7 @@ "request": { "scheme": "NONEXISTENT", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "target": "macos" }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--success.json b/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--success.json index 42c291daf..ea8e076b0 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/build-and-run--success.json @@ -7,8 +7,7 @@ "request": { "scheme": "MCPTest", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "target": "macos" }, @@ -18,7 +17,7 @@ "target": "macos" }, "artifacts": { - "appPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app", + "appPath": "/DerivedData/Build/Products/Debug/MCPTest.app", "bundleId": "io.sentry.MCPTest.macOS", "processId": 99999, "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_macos__pid.log" diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/get-app-path--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/macos/get-app-path--error-wrong-scheme.json index 6fffee19b..80c604d24 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/get-app-path--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/get-app-path--error-wrong-scheme.json @@ -7,7 +7,6 @@ "request": { "scheme": "NONEXISTENT", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "configuration": "Debug", "platform": "macOS" }, "summary": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/get-app-path--success.json b/src/snapshot-tests/__fixtures__/cli/json/macos/get-app-path--success.json index 184f86869..e072989c4 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/get-app-path--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/get-app-path--success.json @@ -7,7 +7,6 @@ "request": { "scheme": "MCPTest", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "configuration": "Debug", "platform": "macOS" }, "summary": { @@ -15,11 +14,11 @@ "target": "macos" }, "artifacts": { - "appPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app" + "appPath": "/DerivedData/Build/Products/Debug/MCPTest.app" } }, "nextSteps": [ - "Get bundle ID: xcodebuildmcp macos get-macos-bundle-id --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app", - "Launch app: xcodebuildmcp macos launch --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app" + "Get bundle ID: xcodebuildmcp macos get-macos-bundle-id --app-path /DerivedData/Build/Products/Debug/MCPTest.app", + "Launch app: xcodebuildmcp macos launch --app-path /DerivedData/Build/Products/Debug/MCPTest.app" ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/get-macos-bundle-id--error-missing-app.json b/src/snapshot-tests/__fixtures__/cli/json/macos/get-macos-bundle-id--error-missing-app.json index 590082ed6..26b8d253b 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/get-macos-bundle-id--error-missing-app.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/get-macos-bundle-id--error-missing-app.json @@ -5,13 +5,13 @@ "error": "Failed to get macOS bundle ID.", "data": { "artifacts": { - "appPath": "/nonexistent/path/Fake.app" + "appPath": "/missing.app" }, "diagnostics": { "warnings": [], "errors": [ { - "message": "File not found: '/nonexistent/path/Fake.app'. Please check the path and try again." + "message": "File not found: '/missing.app'. Please check the path and try again." } ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/launch--success.json b/src/snapshot-tests/__fixtures__/cli/json/macos/launch--success.json index 05656efca..8765f555b 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/launch--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/launch--success.json @@ -8,7 +8,7 @@ "status": "SUCCEEDED" }, "artifacts": { - "appPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app", + "appPath": "/DerivedData/Build/Products/Debug/MCPTest.app", "bundleId": "io.sentry.MCPTest.macOS", "processId": 99999 }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/stop--success.json b/src/snapshot-tests/__fixtures__/cli/json/macos/stop--success.json index 7c3ed4c45..13933bed8 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/stop--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/stop--success.json @@ -8,7 +8,8 @@ "status": "SUCCEEDED" }, "artifacts": { - "appName": "MCPTest" + "processId": 99999, + "appName": "PID " }, "diagnostics": { "warnings": [], diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-compiler.json index 4033faff0..d19c4fb86 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-compiler.json @@ -7,8 +7,7 @@ "request": { "scheme": "MCPTest", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "onlyTesting": [ "MCPTestTests/MCPTestTests/appNameIsCorrect()", diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-wrong-scheme.json index 4e820215f..ddc6f3ac1 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/test--error-wrong-scheme.json @@ -7,8 +7,7 @@ "request": { "scheme": "NONEXISTENT", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "onlyTesting": [], "skipTesting": [] diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/test--failure.json b/src/snapshot-tests/__fixtures__/cli/json/macos/test--failure.json index 5eb651c3e..4aff7746b 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/test--failure.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/test--failure.json @@ -7,8 +7,7 @@ "request": { "scheme": "MCPTest", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "onlyTesting": [], "skipTesting": [] diff --git a/src/snapshot-tests/__fixtures__/cli/json/macos/test--success.json b/src/snapshot-tests/__fixtures__/cli/json/macos/test--success.json index 3c359a699..9b3342d58 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/macos/test--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/macos/test--success.json @@ -7,8 +7,7 @@ "request": { "scheme": "MCPTest", "projectPath": "example_projects/macOS/MCPTest.xcodeproj", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-", - "configuration": "Debug", + "derivedDataPath": "/DerivedData", "platform": "macOS", "onlyTesting": [ "MCPTestTests/MCPTestTests/appNameIsCorrect()", diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator-management/list--success.json b/src/snapshot-tests/__fixtures__/cli/json/simulator-management/list--success.json index 1135f65d1..dd20a0621 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator-management/list--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator-management/list--success.json @@ -5,6 +5,272 @@ "error": null, "data": { "simulators": [ + { + "name": "Apple Watch Series 11 (46mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 27.0" + }, + { + "name": "Apple Watch Series 11 (42mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 27.0" + }, + { + "name": "Apple Watch Ultra 3 (49mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 27.0" + }, + { + "name": "Apple Watch SE 3 (44mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 27.0" + }, + { + "name": "Apple Watch SE 3 (40mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 27.0" + }, + { + "name": "iPhone 17 Pro", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPhone 17 Pro Max", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPhone 17e", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPhone Air", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPhone 17", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPad Pro 13-inch (M5)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPad Pro 11-inch (M5)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPad mini (A17 Pro)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPad Air 13-inch (M4)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPad Air 11-inch (M4)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPad (A16)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "iOS " + }, + { + "name": "iPhone 17 Pro", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPhone 17 Pro", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPhone 17 Pro Max", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPhone 17e", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPhone Air", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPhone 17", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPad Pro 13-inch (M5)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPad Pro 11-inch (M5)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPad mini (A17 Pro)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPad Air 13-inch (M4)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPad Air 11-inch (M4)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "iPad (A16)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "iOS " + }, + { + "name": "Apple Watch Series 11 (46mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 26.5" + }, + { + "name": "Apple Watch Series 11 (42mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 26.5" + }, + { + "name": "Apple Watch Ultra 3 (49mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 26.5" + }, + { + "name": "Apple Watch SE 3 (44mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 26.5" + }, + { + "name": "Apple Watch SE 3 (40mm)", + "simulatorId": "", + "state": "", + "isAvailable": true, + "runtime": "watchOS 26.5" + }, + { + "name": "Apple Watch Series 11 (46mm)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "watchOS 26.4" + }, + { + "name": "Apple Watch Series 11 (42mm)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "watchOS 26.4" + }, + { + "name": "Apple Watch Ultra 3 (49mm)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "watchOS 26.4" + }, + { + "name": "Apple Watch SE 3 (44mm)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "watchOS 26.4" + }, + { + "name": "Apple Watch SE 3 (40mm)", + "simulatorId": "", + "state": "", + "isAvailable": false, + "runtime": "watchOS 26.4" + }, { "name": "iPhone 17 Pro", "simulatorId": "", diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-compiler.json index ea81bbe63..729e2d4b1 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-compiler.json @@ -7,10 +7,9 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17" + "simulatorId": "" }, "summary": { "status": "FAILED", @@ -25,7 +24,7 @@ "errors": [ { "message": "cannot convert value of type 'String' to specified type 'Int'", - "location": "/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33" + "location": "/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53" } ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-prepared-tests-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-prepared-tests-wrong-scheme.json index ebd7f2d34..41002606b 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-prepared-tests-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-prepared-tests-wrong-scheme.json @@ -8,9 +8,9 @@ "buildForTesting": true, "scheme": "NONEXISTENT", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17", + "simulatorId": "", "testProductsPath": "/Invalid CalculatorApp Tests.xctestproducts" }, "summary": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-wrong-scheme.json index a491f6c0a..1c4d27f55 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--error-wrong-scheme.json @@ -7,10 +7,9 @@ "request": { "scheme": "NONEXISTENT", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17" + "simulatorId": "" }, "summary": { "status": "FAILED", diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success-prepared-tests.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success-prepared-tests.json index 1afb8ba5d..b91950011 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success-prepared-tests.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success-prepared-tests.json @@ -8,9 +8,9 @@ "buildForTesting": true, "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17", + "simulatorId": "", "testProductsPath": "/CalculatorApp Tests.xctestproducts" }, "summary": { @@ -31,6 +31,6 @@ } }, "nextSteps": [ - "Run prepared tests: xcodebuildmcp simulator test --test-products-path '/CalculatorApp Tests.xctestproducts' --simulator-name 'iPhone 17'" + "Run prepared tests: xcodebuildmcp simulator test --test-products-path '/CalculatorApp Tests.xctestproducts' --simulator-id " ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success.json index 886feae56..211f4f4da 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build--success.json @@ -7,10 +7,9 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17" + "simulatorId": "" }, "summary": { "status": "SUCCEEDED", @@ -26,6 +25,6 @@ } }, "nextSteps": [ - "Get built app path in simulator derived data: xcodebuildmcp simulator get-app-path --simulator-name 'iPhone 17' --scheme CalculatorApp --platform 'iOS Simulator'" + "Get built app path in simulator derived data: xcodebuildmcp simulator get-app-path --simulator-id --scheme CalculatorApp --platform 'iOS Simulator' --derived-data-path " ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--error-compiler.json index 437ba453d..ce90e77da 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--error-compiler.json @@ -7,10 +7,9 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17" + "simulatorId": "" }, "summary": { "status": "FAILED", @@ -25,7 +24,7 @@ "errors": [ { "message": "cannot convert value of type 'String' to specified type 'Int'", - "location": "/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33" + "location": "/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53" } ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--error-wrong-scheme.json index 4398d22e0..5d918f52b 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--error-wrong-scheme.json @@ -7,10 +7,9 @@ "request": { "scheme": "NONEXISTENT", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17" + "simulatorId": "" }, "summary": { "status": "FAILED", diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--success.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--success.json index b11f85670..add9ef07d 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/build-and-run--success.json @@ -7,10 +7,9 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17" + "simulatorId": "" }, "summary": { "status": "SUCCEEDED", @@ -18,7 +17,7 @@ "target": "simulator" }, "artifacts": { - "appPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app", + "appPath": "/Build/Products/Debug-iphonesimulator/CalculatorApp.app", "bundleId": "io.sentry.calculatorapp", "processId": 99999, "simulatorId": "", diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/get-app-path--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/get-app-path--error-wrong-scheme.json index f0c0c75bb..d5e760dec 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/get-app-path--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/get-app-path--error-wrong-scheme.json @@ -7,9 +7,8 @@ "request": { "scheme": "NONEXISTENT", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "configuration": "Debug", "platform": "iOS Simulator", - "simulator": "iPhone 17" + "simulator": "" }, "summary": { "status": "FAILED", diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/get-app-path--success.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/get-app-path--success.json index 9e970484c..79055c835 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/get-app-path--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/get-app-path--success.json @@ -7,9 +7,8 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "configuration": "Debug", "platform": "iOS Simulator", - "simulator": "iPhone 17" + "simulator": "" }, "summary": { "status": "SUCCEEDED", @@ -17,13 +16,13 @@ "durationMs": 1234 }, "artifacts": { - "appPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app" + "appPath": "/Build/Products/Debug-iphonesimulator/CalculatorApp.app" } }, "nextSteps": [ - "Get bundle ID: xcodebuildmcp simulator get-app-bundle-id --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app", + "Get bundle ID: xcodebuildmcp simulator get-app-bundle-id --app-path /Build/Products/Debug-iphonesimulator/CalculatorApp.app", "Boot simulator: xcodebuildmcp simulator boot --simulator-id SIMULATOR_UUID", - "Install app: xcodebuildmcp simulator install --simulator-id SIMULATOR_UUID --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app", + "Install app: xcodebuildmcp simulator install --simulator-id SIMULATOR_UUID --app-path /Build/Products/Debug-iphonesimulator/CalculatorApp.app", "Launch app: xcodebuildmcp simulator launch-app --simulator-id SIMULATOR_UUID --bundle-id BUNDLE_ID" ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/install--success.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/install--success.json index fecef3d42..0d101cdaa 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/install--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/install--success.json @@ -9,7 +9,7 @@ }, "artifacts": { "simulatorId": "", - "appPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app" + "appPath": "/Build/Products/Debug-iphonesimulator/CalculatorApp.app" }, "diagnostics": { "warnings": [], diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-compiler.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-compiler.json index 389e7bdab..eda10eb91 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-compiler.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-compiler.json @@ -7,10 +7,9 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17", + "simulatorId": "", "onlyTesting": [ "CalculatorAppTests/CalculatorAppTests/testAddition" ], @@ -40,7 +39,7 @@ "errors": [ { "message": "cannot convert value of type 'String' to specified type 'Int'", - "location": "/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33" + "location": "/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53" } ], "testFailures": [] diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-wrong-scheme.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-wrong-scheme.json index 00c66ee0f..4e0fe13e7 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-wrong-scheme.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--error-wrong-scheme.json @@ -7,10 +7,9 @@ "request": { "scheme": "NONEXISTENT", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17", + "simulatorId": "", "onlyTesting": [], "skipTesting": [] }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--failure.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--failure.json index f4de9c9a7..af4f82629 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--failure.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--failure.json @@ -7,10 +7,9 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17", + "simulatorId": "", "onlyTesting": [], "skipTesting": [] }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--success.json b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--success.json index 519bd70d0..85b606059 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/simulator/test--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/simulator/test--success.json @@ -7,10 +7,9 @@ "request": { "scheme": "CalculatorApp", "workspacePath": "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", - "derivedDataPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-", - "configuration": "Debug", + "derivedDataPath": "", "platform": "iOS Simulator", - "simulatorName": "iPhone 17", + "simulatorId": "", "onlyTesting": [ "CalculatorAppTests/CalculatorAppTests/testAddition" ], diff --git a/src/snapshot-tests/__fixtures__/cli/json/swift-package/build--error-bad-path.json b/src/snapshot-tests/__fixtures__/cli/json/swift-package/build--error-bad-path.json index 67daafb8c..293ba8c5a 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/swift-package/build--error-bad-path.json +++ b/src/snapshot-tests/__fixtures__/cli/json/swift-package/build--error-bad-path.json @@ -5,7 +5,7 @@ "error": "Build failed", "data": { "request": { - "packagePath": "/example_projects/NONEXISTENT", + "packagePath": "/spm/NONEXISTENT", "target": "swift-package" }, "summary": { @@ -14,14 +14,14 @@ "target": "swift-package" }, "artifacts": { - "packagePath": "/example_projects/NONEXISTENT", + "packagePath": "/spm/NONEXISTENT", "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_spm__pid.log" }, "diagnostics": { "warnings": [], "errors": [ { - "message": "chdir error: No such file or directory (2): /example_projects/NONEXISTENT" + "message": "chdir error: No such file or directory (2): /spm/NONEXISTENT" } ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/swift-package/build--success.json b/src/snapshot-tests/__fixtures__/cli/json/swift-package/build--success.json index d993cf664..2b7d310cd 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/swift-package/build--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/swift-package/build--success.json @@ -5,7 +5,7 @@ "error": null, "data": { "request": { - "packagePath": "/example_projects/spm", + "packagePath": "/spm", "target": "swift-package" }, "summary": { @@ -14,7 +14,7 @@ "target": "swift-package" }, "artifacts": { - "packagePath": "/example_projects/spm", + "packagePath": "/spm", "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_spm__pid.log" }, "diagnostics": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/swift-package/clean--error-bad-path.json b/src/snapshot-tests/__fixtures__/cli/json/swift-package/clean--error-bad-path.json index 7f29e308a..893061af3 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/swift-package/clean--error-bad-path.json +++ b/src/snapshot-tests/__fixtures__/cli/json/swift-package/clean--error-bad-path.json @@ -9,13 +9,13 @@ "target": "swift-package" }, "artifacts": { - "packagePath": "/example_projects/NONEXISTENT" + "packagePath": "/spm/NONEXISTENT" }, "diagnostics": { "warnings": [], "errors": [ { - "message": "chdir error: No such file or directory (2): /example_projects/NONEXISTENT" + "message": "chdir error: No such file or directory (2): /spm/NONEXISTENT" } ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/swift-package/clean--success.json b/src/snapshot-tests/__fixtures__/cli/json/swift-package/clean--success.json index 75b69af0e..1461bac35 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/swift-package/clean--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/swift-package/clean--success.json @@ -9,7 +9,7 @@ "target": "swift-package" }, "artifacts": { - "packagePath": "/example_projects/spm" + "packagePath": "/spm" }, "diagnostics": { "warnings": [], diff --git a/src/snapshot-tests/__fixtures__/cli/json/swift-package/list--success.json b/src/snapshot-tests/__fixtures__/cli/json/swift-package/list--success.json index 82f194fc5..018c30d24 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/swift-package/list--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/swift-package/list--success.json @@ -13,7 +13,7 @@ "processId": 99999, "uptimeSeconds": 3600, "artifacts": { - "packagePath": "/example_projects/spm" + "packagePath": "/spm" } } ] diff --git a/src/snapshot-tests/__fixtures__/cli/json/swift-package/run--error-bad-executable.json b/src/snapshot-tests/__fixtures__/cli/json/swift-package/run--error-bad-executable.json index 48b7983e7..745fc8277 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/swift-package/run--error-bad-executable.json +++ b/src/snapshot-tests/__fixtures__/cli/json/swift-package/run--error-bad-executable.json @@ -5,7 +5,7 @@ "error": "Build failed", "data": { "request": { - "packagePath": "/example_projects/spm", + "packagePath": "/spm", "executableName": "nonexistent-executable", "target": "swift-package" }, @@ -15,7 +15,7 @@ "target": "swift-package" }, "artifacts": { - "packagePath": "/example_projects/spm", + "packagePath": "/spm", "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_spm__pid.log" }, "diagnostics": { diff --git a/src/snapshot-tests/__fixtures__/cli/json/swift-package/run--success.json b/src/snapshot-tests/__fixtures__/cli/json/swift-package/run--success.json index 2d6aafe5b..c2af842f9 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/swift-package/run--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/swift-package/run--success.json @@ -5,7 +5,7 @@ "error": null, "data": { "request": { - "packagePath": "/example_projects/spm", + "packagePath": "/spm", "executableName": "spm", "target": "swift-package" }, @@ -15,8 +15,8 @@ "target": "swift-package" }, "artifacts": { - "packagePath": "/example_projects/spm", - "executablePath": "/example_projects/spm/.build/arm64-apple-macosx/debug/spm", + "packagePath": "/spm", + "executablePath": "/spm/.build//debug/spm", "processId": 99999, "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_spm__pid.log" }, @@ -25,6 +25,13 @@ "Hello, world!" ], "stderr": [ + "Fetching https://github.com/apple/swift-argument-parser.git from cache", + "Fetched https://github.com/apple/swift-argument-parser.git from cache ()", + "Computing version for https://github.com/apple/swift-argument-parser.git", + "Computed https://github.com/apple/swift-argument-parser.git at 1.5.1 ()", + "Computed https://github.com/apple/swift-argument-parser.git at 1.5.1 ()", + "Creating working copy for https://github.com/apple/swift-argument-parser.git", + "Working copy of https://github.com/apple/swift-argument-parser.git resolved at 1.5.1", "Building for debugging...", "[] Applying spm", "[] Compiling spm main.swift", @@ -33,7 +40,7 @@ "[] Write Objects.LinkFileList", "[] Write sources", "[] Write spm-entitlement.plist", - "[] Write swift-version--.txt", + "[] Write swift-version-69A768CDF2A0BEE1.txt", "Build of product 'spm' complete! ()" ] }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/swift-package/test--error-bad-path.json b/src/snapshot-tests/__fixtures__/cli/json/swift-package/test--error-bad-path.json index 5c87db045..98a6d8e6a 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/swift-package/test--error-bad-path.json +++ b/src/snapshot-tests/__fixtures__/cli/json/swift-package/test--error-bad-path.json @@ -17,13 +17,13 @@ }, "artifacts": { "buildLogPath": "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/swift_package_test__pid.log", - "packagePath": "/example_projects/NONEXISTENT" + "packagePath": "/spm/NONEXISTENT" }, "diagnostics": { "warnings": [], "errors": [ { - "message": "chdir error: No such file or directory (2): /example_projects/NONEXISTENT" + "message": "chdir error: No such file or directory (2): /spm/NONEXISTENT" } ], "testFailures": [] diff --git a/src/snapshot-tests/__fixtures__/cli/json/swift-package/test--failure.json b/src/snapshot-tests/__fixtures__/cli/json/swift-package/test--failure.json index e060a3b35..45c8b0de3 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/swift-package/test--failure.json +++ b/src/snapshot-tests/__fixtures__/cli/json/swift-package/test--failure.json @@ -27,17 +27,17 @@ "warnings": [], "errors": [], "testFailures": [ - { - "suite": "CalculatorAppTests", - "test": "testCalculatorServiceFailure", - "message": "XCTAssertEqual failed: (\"0\") is not equal to (\"999\") - This test should fail - display should be 0, not 999", - "location": "/example_projects/spm/Tests/TestLibTests/SimpleTests.swift:49" - }, { "suite": "(Unknown Suite)", "test": "test", "message": "Expectation failed: Bool(false)\nTest failed", "location": "SimpleTests.swift:57" + }, + { + "suite": "CalculatorAppTests", + "test": "testCalculatorServiceFailure", + "message": "XCTAssertEqual failed: (\"0\") is not equal to (\"999\") - This test should fail - display should be 0, not 999", + "location": "/spm/Tests/TestLibTests/SimpleTests.swift:49" } ] }, diff --git a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/long-press--success.json b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/long-press--success.json index ca0589497..d560ee5ba 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/long-press--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/long-press--success.json @@ -11,8 +11,8 @@ "type": "long-press", "elementRef": "", "durationMs": 500, - "x": 201, - "y": 437 + "x": 61, + "y": 490 }, "artifacts": { "simulatorId": "" diff --git a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/swipe--error-not-actionable.json b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/swipe--error-not-actionable.json index e3071ffbc..978744a3b 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/swipe--error-not-actionable.json +++ b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/swipe--error-not-actionable.json @@ -2,7 +2,7 @@ "schema": "xcodebuildmcp.output.ui-action-result", "schemaVersion": "2", "didError": true, - "error": "Element ref 'e3' does not support 'swipeWithin'.", + "error": "Element ref '' does not support 'swipeWithin'.", "data": { "summary": { "status": "FAILED" @@ -17,7 +17,7 @@ }, "uiError": { "code": "TARGET_NOT_ACTIONABLE", - "message": "Element ref 'e3' does not support 'swipeWithin'.", + "message": "Element ref '' does not support 'swipeWithin'.", "recoveryHint": "Choose an elementRef that lists the required action, or refresh with snapshot_ui.", "elementRef": "", "candidates": [], diff --git a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/swipe--success.json b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/swipe--success.json index 5c40383a0..9b6d1fec7 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/swipe--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/swipe--success.json @@ -13,11 +13,11 @@ "direction": "up", "from": { "x": 201, - "y": 743 + "y": 468 }, "to": { "x": 201, - "y": 131 + "y": 406 } }, "artifacts": { @@ -28,45 +28,31 @@ "rs": "1", "screenHash": "", "seq": 1, - "count": 144, - "targets": [ - "|tap|button|Home Screen & App Library||com.apple.settings.homeScreen", - "|typeText|text-field||Search|", - "|tap|button|Camera||com.apple.settings.camera", - "|tap|button|Search||com.apple.settings.search", - "|tap|button|StandBy||com.apple.settings.standBy", - "|tap|button|Screen Time||com.apple.settings.screenTime", - "|tap|button|Passcode||com.apple.settings.passcodeAndBiometrics", - "|tap|button|Privacy & Security||com.apple.settings.privacyAndSecurity", - "|tap|button|Game Center||com.apple.settings.gameCenter", - "|tap|button|iCloud||com.apple.settings.iCloud", - "|tap|button|Apps||com.apple.settings.apps", - "|tap|button|Developer||com.apple.settings.developer", - "|tap|button|Dictate||Dictate", - "|tap|button|Dictate||Dictate" - ], + "count": 99999, + "targets": [], "scroll": [ - "|swipe|application|Settings||" + "|swipe|scroll-view|||snapshot-scroll-surface" ], "text": [ - "|text|text|Apple Intelligence & Siri||", - "|text|text|Camera||", - "|text|text|Home Screen & App Library||", - "|text|text|Search||", - "|text|text|StandBy||", - "|text|text|Screen Time||", - "|text|text|Passcode||", - "|text|text|Privacy & Security||", - "|text|text|Game Center||", - "|text|text|iCloud||", - "|text|text|Apps||", - "|text|text|Developer||" + "|text|text|Snapshot row 0||", + "|text|text|Snapshot row 1||", + "|text|text|Snapshot row 2||", + "|text|text|Snapshot row 3||", + "|text|text|Snapshot row 4||", + "|text|text|Snapshot row 5||", + "|text|text|Snapshot row 6||", + "|text|text|Snapshot row 7||", + "|text|text|Snapshot row 8||", + "|text|text|Snapshot row 9||", + "|text|text|Snapshot row 10||", + "|text|text|Snapshot row 11||", + "|text|text|Snapshot row 12||", + "|text|text|Snapshot row 13||" ], "udid": "" } }, "nextSteps": [ - "Batch same-screen taps: xcodebuildmcp ui-automation batch --json '{\"simulatorId\":\"\",\"steps\":[{\"action\":\"tap\",\"elementRef\":\"\"},{\"action\":\"tap\",\"elementRef\":\"\"}]}'", - "Tap an elementRef: xcodebuildmcp ui-automation tap --simulator-id --element-ref " + "" ] } diff --git a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/tap--success-verbose.json b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/tap--success-verbose.json index c8eb7775f..0403ab758 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/tap--success-verbose.json +++ b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/tap--success-verbose.json @@ -149,7 +149,7 @@ { "ref": "e8", "role": "text", - "label": "7", + "label": "0", "frame": { "x": 0, "y": 0, @@ -608,12 +608,12 @@ { "action": "longPress", "elementRef": "e8", - "label": "7" + "label": "0" }, { "action": "touch", "elementRef": "e8", - "label": "7" + "label": "0" }, { "action": "longPress", diff --git a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/tap--success.json b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/tap--success.json index 8b17e5398..91ebbee0e 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/tap--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/tap--success.json @@ -45,7 +45,7 @@ ], "scroll": [], "text": [ - "|text|text|7||" + "|text|text|0||" ], "udid": "" } diff --git a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/touch--success.json b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/touch--success.json index 824f4e4a3..ee825ee9d 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/touch--success.json +++ b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/touch--success.json @@ -11,8 +11,8 @@ "type": "touch", "elementRef": "", "event": "touch down+up", - "x": 201, - "y": 437 + "x": 61, + "y": 490 }, "artifacts": { "simulatorId": "" diff --git a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/type-text--error-not-actionable.json b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/type-text--error-not-actionable.json index 69ac8a784..caf6f8a68 100644 --- a/src/snapshot-tests/__fixtures__/cli/json/ui-automation/type-text--error-not-actionable.json +++ b/src/snapshot-tests/__fixtures__/cli/json/ui-automation/type-text--error-not-actionable.json @@ -2,7 +2,7 @@ "schema": "xcodebuildmcp.output.ui-action-result", "schemaVersion": "2", "didError": true, - "error": "Element ref 'e3' does not support 'typeText'.", + "error": "Element ref '' does not support 'typeText'.", "data": { "summary": { "status": "FAILED" @@ -17,7 +17,7 @@ }, "uiError": { "code": "TARGET_NOT_ACTIONABLE", - "message": "Element ref 'e3' does not support 'typeText'.", + "message": "Element ref '' does not support 'typeText'.", "recoveryHint": "Choose an elementRef that lists the required action, or refresh with snapshot_ui.", "elementRef": "", "candidates": [], diff --git a/src/snapshot-tests/__fixtures__/cli/text/coverage/get-coverage-report--success.txt b/src/snapshot-tests/__fixtures__/cli/text/coverage/get-coverage-report--success.txt index 07df5e959..f063b0b6c 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/coverage/get-coverage-report--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/coverage/get-coverage-report--success.txt @@ -4,10 +4,10 @@ xcresult: /TestResults.xcresult Target Filter: CalculatorAppTests -ℹ️ Overall: 94.9% (371/391 lines) +ℹ️ Overall: 4.6% (18/391 lines) Targets - CalculatorAppTests.xctest: 94.9% (371/391 lines) + CalculatorAppTests.xctest: 4.6% (18/391 lines) Next steps: 1. View file-level coverage: xcodebuildmcp coverage get-file-coverage --xcresult-path /TestResults.xcresult diff --git a/src/snapshot-tests/__fixtures__/cli/text/coverage/get-file-coverage--success.txt b/src/snapshot-tests/__fixtures__/cli/text/coverage/get-file-coverage--success.txt index b6e55da8b..4628cb641 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/coverage/get-file-coverage--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/coverage/get-file-coverage--success.txt @@ -6,24 +6,37 @@ File: example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorService.swift -ℹ️ Coverage: 83.1% (157/189 lines) +ℹ️ Coverage: 55.0% (104/189 lines) -🔴 Not Covered (7 functions, 22 lines) +🔴 Not Covered (19 functions, 70 lines) L159 CalculatorService.deleteLastDigit() -- 0/16 lines + L63 CalculatorService.inputDecimal() -- 0/14 lines + L148 CalculatorService.clear() -- 0/10 lines + L134 CalculatorService.toggleSign() -- 0/6 lines + L141 CalculatorService.percentage() -- 0/6 lines + L178 CalculatorService.setError(_:) -- 0/5 lines L58 implicit closure #2 in CalculatorService.inputNumber(_:) -- 0/1 lines + L68 implicit closure #1 in CalculatorService.inputDecimal() -- 0/1 lines + L94 implicit closure #1 in CalculatorService.calculate() -- 0/1 lines L98 implicit closure #3 in CalculatorService.calculate() -- 0/1 lines L99 implicit closure #4 in CalculatorService.calculate() -- 0/1 lines L162 implicit closure #1 in CalculatorService.deleteLastDigit() -- 0/1 lines L172 implicit closure #2 in CalculatorService.deleteLastDigit() -- 0/1 lines + L209 implicit closure #3 in CalculatorService.formatNumber(_:) -- 0/1 lines L214 implicit closure #4 in CalculatorService.formatNumber(_:) -- 0/1 lines + L221 CalculatorService.currentValue.getter -- 0/1 lines + L222 CalculatorService.previousValue.getter -- 0/1 lines + L223 CalculatorService.currentOperation.getter -- 0/1 lines + L224 CalculatorService.willResetDisplay.getter -- 0/1 lines -🟡 Partial Coverage (4 functions) +🟡 Partial Coverage (5 functions) L184 CalculatorService.updateExpressionDisplay() -- 80.0% (8/10 lines) + L93 CalculatorService.calculate() -- 84.2% (32/38 lines) + L47 CalculatorService.inputNumber(_:) -- 85.7% (12/14 lines) + L78 CalculatorService.setOperation(_:) -- 85.7% (12/14 lines) L195 CalculatorService.formatNumber(_:) -- 85.7% (18/21 lines) - L93 CalculatorService.calculate() -- 89.5% (34/38 lines) - L63 CalculatorService.inputDecimal() -- 92.9% (13/14 lines) -🟢 Full Coverage (28 functions) -- all at 100% +🟢 Full Coverage (15 functions) -- all at 100% Next steps: 1. View overall coverage: xcodebuildmcp coverage get-coverage-report --xcresult-path /TestResults.xcresult diff --git a/src/snapshot-tests/__fixtures__/cli/text/debugging/lldb-command--success.txt b/src/snapshot-tests/__fixtures__/cli/text/debugging/lldb-command--success.txt index 94055091f..c796dafd6 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/debugging/lldb-command--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/debugging/lldb-command--success.txt @@ -6,9 +6,4 @@ ✅ Command executed Output: - Current breakpoints: - 1: file = 'ContentView.swift', line = 42, exact_match = 0, locations = - Names: - dap - - 1.1: where = CalculatorApp.debug.dylib`closure #1 in closure #1 in closure #1 in ContentView.body.getter + at ContentView.swift:42:31, address = , resolved, hit count = 0 + No breakpoints currently set. diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build--error-compiler.txt index 57f7369c3..be5bba0b6 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/build--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build--error-compiler.txt @@ -3,14 +3,13 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Derived Data: Compiler Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33:42 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53:42 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build--error-prepared-tests-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build--error-prepared-tests-wrong-scheme.txt index 5ecb6b4e8..1eb285701 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/build--error-prepared-tests-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build--error-prepared-tests-wrong-scheme.txt @@ -4,7 +4,7 @@ Scheme: NONEXISTENT Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace Platform: iOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Derived Data: Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build--error-wrong-scheme.txt index b872bf0f5..1eb285701 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/build--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build--error-wrong-scheme.txt @@ -3,9 +3,8 @@ Scheme: NONEXISTENT Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Derived Data: Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build--success-prepared-tests-generic.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build--success-prepared-tests-generic.txt index cb318c66e..07075b7ca 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/build--success-prepared-tests-generic.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build--success-prepared-tests-generic.txt @@ -4,7 +4,7 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace Platform: iOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Derived Data: ✅ Build succeeded. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build--success.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build--success.txt index 17613ffd6..2175cbc9b 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/build--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build--success.txt @@ -3,13 +3,12 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Derived Data: ✅ Build succeeded. (⏱️ ) └ Files: └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log Next steps: -1. Get built device app path: xcodebuildmcp device get-app-path --scheme CalculatorApp +1. Get built device app path: xcodebuildmcp device get-app-path --scheme CalculatorApp --derived-data-path diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--error-compiler.txt index 044ddf736..b6699d2e9 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--error-compiler.txt @@ -3,15 +3,14 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS - Device: () - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Device: + Derived Data: Compiler Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33:42 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53:42 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--error-wrong-scheme.txt index 66ad3c609..ded0e6345 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--error-wrong-scheme.txt @@ -3,10 +3,9 @@ Scheme: NONEXISTENT Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS - Device: () - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Device: + Derived Data: Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--success.txt index 64e22805e..1bd7daa7d 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/build-and-run--success.txt @@ -3,10 +3,9 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS - Device: () - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Device: + Derived Data: ℹ️ Resolving app path ✅ Resolving app path @@ -19,7 +18,7 @@ ├ Bundle ID: io.sentry.calculatorapp ├ Process ID: └ Files: - ├ App Path: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app + ├ App Path: /Build/Products/Debug-iphoneos/CalculatorApp.app └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_device__pid.log Next steps: diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/get-app-path--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/device/get-app-path--error-wrong-scheme.txt index 6817789b7..0b2761ee1 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/get-app-path--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/get-app-path--error-wrong-scheme.txt @@ -3,7 +3,6 @@ Scheme: NONEXISTENT Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/cli/text/device/get-app-path--success.txt index a6e0241c6..38ecc5634 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/get-app-path--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/get-app-path--success.txt @@ -3,14 +3,13 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS ✅ Success └ Files: - └ App Path: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app + └ App Path: /Build/Products/Debug-iphoneos/CalculatorApp.app Next steps: -1. Get bundle ID: xcodebuildmcp device get-app-bundle-id --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app -2. Install app on device: xcodebuildmcp device install --device-id DEVICE_UDID --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app +1. Get bundle ID: xcodebuildmcp device get-app-bundle-id --app-path /Build/Products/Debug-iphoneos/CalculatorApp.app +2. Install app on device: xcodebuildmcp device install --device-id DEVICE_UDID --app-path /Build/Products/Debug-iphoneos/CalculatorApp.app 3. Launch app on device: xcodebuildmcp device launch --device-id DEVICE_UDID --bundle-id BUNDLE_ID diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/install--success.txt b/src/snapshot-tests/__fixtures__/cli/text/device/install--success.txt index 425f11a28..766a644b4 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/install--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/install--success.txt @@ -1,7 +1,7 @@ 📦 Install App - Device: () - App: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app + Device: + App: /Build/Products/Debug-iphoneos/CalculatorApp.app ✅ App installed successfully. diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/launch--success.txt b/src/snapshot-tests/__fixtures__/cli/text/device/launch--success.txt index f1366bc66..6cef69335 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/launch--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/launch--success.txt @@ -1,7 +1,7 @@ 🚀 Launch App - Device: () + Device: Bundle ID: io.sentry.calculatorapp ✅ App launched successfully. diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/stop--success.txt b/src/snapshot-tests/__fixtures__/cli/text/device/stop--success.txt index 2fa577551..9703b2e57 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/stop--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/stop--success.txt @@ -1,7 +1,7 @@ 🛑 Stop App - Device: () + Device: PID: ✅ App stopped successfully diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/test--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/device/test--error-compiler.txt index ddbc13588..08b6c58c2 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/test--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/test--error-compiler.txt @@ -3,10 +3,9 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS - Device: () - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Device: + Derived Data: Selective Testing: CalculatorAppTests/CalculatorAppTests/testAddition @@ -16,9 +15,8 @@ Discovered 1 test(s): Compiler Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33:42 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53:42 ❌ Test failed. (⏱️ ) └ Files: - ├ Result Bundle: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_device__pid.xcresult └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_device__pid.log diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/text/device/test--failure.txt index cbd595b3f..443c5ab8e 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/test--failure.txt @@ -3,10 +3,9 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS - Device: () - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Device: + Derived Data: Discovered 57 test(s): CalculatorAppFeatureTests/CalculatorBasicTests/testClear @@ -43,14 +42,13 @@ Running tests (23 completed, 2 failures, 0 skipped) CalculatorAppTests ✗ testCalculatorServiceFailure: - XCTAssertEqual failed: ("0") is not equal to ("999") - This test should fail - display should be 0, not 999 - example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:52 - + /example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:52 IntentionalFailureTests ✗ test: - XCTAssertTrue failed - This test should fail to verify error reporting - example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:286 - + /example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:286 ❌ tests failed, passed, skipped (⏱️ ) └ Files: ├ Result Bundle: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_device__pid.xcresult - └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_device__pid.log + ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_device__pid.log + └ Test Products: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_device__pid.xctestproducts diff --git a/src/snapshot-tests/__fixtures__/cli/text/device/test--success.txt b/src/snapshot-tests/__fixtures__/cli/text/device/test--success.txt index ae68d6925..7b949b3cc 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/device/test--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/device/test--success.txt @@ -3,10 +3,9 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS - Device: () - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Device: + Derived Data: Selective Testing: CalculatorAppTests/CalculatorAppTests/testAddition @@ -17,4 +16,5 @@ Running tests (1 completed, 0 failures, 0 skipped) ✅ 1 test passed, 0 failed, 0 skipped (⏱️ ) └ Files: ├ Result Bundle: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/result-bundles/test_device__pid.xcresult - └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_device__pid.log + ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_device__pid.log + └ Test Products: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/test-products/test_device__pid.xctestproducts diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-compiler.txt index 8c97dd1d8..704b60362 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-compiler.txt @@ -3,14 +3,13 @@ Scheme: MCPTest Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData Compiler Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - example_projects/macOS/MCPTest/MCPTestApp.swift:20:42 + /example_projects/macOS/MCPTest/MCPTestApp.swift:20:42 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-prepared-tests-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-prepared-tests-wrong-scheme.txt index 4238aea69..adcd3c00a 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-prepared-tests-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-prepared-tests-wrong-scheme.txt @@ -4,7 +4,7 @@ Scheme: NONEXISTENT Project: example_projects/macOS/MCPTest.xcodeproj Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-wrong-scheme.txt index 0d7ef994c..adcd3c00a 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build--error-wrong-scheme.txt @@ -3,9 +3,8 @@ Scheme: NONEXISTENT Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build--success-prepared-tests.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build--success-prepared-tests.txt index 666cd8bf9..64a5f1d08 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/build--success-prepared-tests.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build--success-prepared-tests.txt @@ -4,7 +4,7 @@ Scheme: MCPTest Project: example_projects/macOS/MCPTest.xcodeproj Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData ✅ Build succeeded. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build--success.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build--success.txt index 3d74d0c2c..d835522ed 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/build--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build--success.txt @@ -3,9 +3,8 @@ Scheme: MCPTest Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData ✅ Build succeeded. (⏱️ ) ├ Bundle ID: io.sentry.MCPTest.macOS @@ -13,4 +12,4 @@ └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log Next steps: -1. Get built macOS app path: xcodebuildmcp macos get-app-path --scheme MCPTest +1. Get built macOS app path: xcodebuildmcp macos get-app-path --scheme MCPTest --derived-data-path /DerivedData diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--error-compiler.txt index 6c40977a8..2b11f7acb 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--error-compiler.txt @@ -3,14 +3,13 @@ Scheme: MCPTest Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData Compiler Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - example_projects/macOS/MCPTest/MCPTestApp.swift:20:42 + /example_projects/macOS/MCPTest/MCPTestApp.swift:20:42 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--error-wrong-scheme.txt index 711ff7b31..7450d4a9d 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--error-wrong-scheme.txt @@ -3,9 +3,8 @@ Scheme: NONEXISTENT Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--success.txt index dc91eca6e..13996a4ab 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/build-and-run--success.txt @@ -3,9 +3,8 @@ Scheme: MCPTest Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData ℹ️ Resolving app path ✅ Resolving app path @@ -17,7 +16,7 @@ ├ Bundle ID: io.sentry.MCPTest.macOS ├ Process ID: └ Files: - ├ App Path: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app + ├ App Path: /DerivedData/Build/Products/Debug/MCPTest.app └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_macos__pid.log Next steps: diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/get-app-path--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/get-app-path--error-wrong-scheme.txt index 0661e1121..5129bab95 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/get-app-path--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/get-app-path--error-wrong-scheme.txt @@ -3,7 +3,6 @@ Scheme: NONEXISTENT Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/get-app-path--success.txt index 8a94fdea4..41106695b 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/get-app-path--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/get-app-path--success.txt @@ -3,13 +3,12 @@ Scheme: MCPTest Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS ✅ Success └ Files: - └ App Path: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app + └ App Path: /DerivedData/Build/Products/Debug/MCPTest.app Next steps: -1. Get bundle ID: xcodebuildmcp macos get-macos-bundle-id --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app -2. Launch app: xcodebuildmcp macos launch --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app +1. Get bundle ID: xcodebuildmcp macos get-macos-bundle-id --app-path /DerivedData/Build/Products/Debug/MCPTest.app +2. Launch app: xcodebuildmcp macos launch --app-path /DerivedData/Build/Products/Debug/MCPTest.app diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/get-macos-bundle-id--error-missing-app.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/get-macos-bundle-id--error-missing-app.txt index a7d25d46a..f17496c53 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/get-macos-bundle-id--error-missing-app.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/get-macos-bundle-id--error-missing-app.txt @@ -1,10 +1,10 @@ 🔍 Get macOS Bundle ID - App: /nonexistent/path/Fake.app + App: /missing.app Errors (1): - ✗ File not found: '/nonexistent/path/Fake.app'. Please check the path and try again. + ✗ File not found: '/missing.app'. Please check the path and try again. ❌ Failed to get macOS bundle ID. diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/launch--success.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/launch--success.txt index 2362ab71b..517f10528 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/launch--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/launch--success.txt @@ -1,7 +1,7 @@ 🚀 Launch macOS App - App: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app + App: /DerivedData/Build/Products/Debug/MCPTest.app ✅ App launched successfully ├ Bundle ID: io.sentry.MCPTest.macOS diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/stop--success.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/stop--success.txt index dffe5e4ef..5bcead8c8 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/stop--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/stop--success.txt @@ -1,6 +1,6 @@ 🛑 Stop macOS App - App: MCPTest + App: PID ✅ App stopped successfully diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-compiler.txt index f44082a27..1abcfaf24 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-compiler.txt @@ -3,9 +3,8 @@ Scheme: MCPTest Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData Selective Testing: MCPTestTests/MCPTestTests/appNameIsCorrect() MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect @@ -17,7 +16,7 @@ Discovered 2 test(s): Compiler Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - example_projects/macOS/MCPTest/MCPTestApp.swift:20:42 + /example_projects/macOS/MCPTest/MCPTestApp.swift:20:42 ❌ Test failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-wrong-scheme.txt index e3b67a6f0..0eacd90a1 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/test--error-wrong-scheme.txt @@ -3,9 +3,8 @@ Scheme: NONEXISTENT Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/test--failure.txt index 64320a171..1abbe25f0 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/test--failure.txt @@ -3,9 +3,8 @@ Scheme: MCPTest Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData Discovered 4 test(s): MCPTestTests/MCPTestTests/appNameIsCorrect diff --git a/src/snapshot-tests/__fixtures__/cli/text/macos/test--success.txt b/src/snapshot-tests/__fixtures__/cli/text/macos/test--success.txt index c151ec21c..13c24efba 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/macos/test--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/macos/test--success.txt @@ -3,9 +3,8 @@ Scheme: MCPTest Project: example_projects/macOS/MCPTest.xcodeproj - Configuration: Debug Platform: macOS - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest- + Derived Data: /DerivedData Selective Testing: MCPTestTests/MCPTestTests/appNameIsCorrect() MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-compiler.txt index bc83057bf..0b1919cf7 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-compiler.txt @@ -3,15 +3,14 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: Compiler Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33:42 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53:42 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-prepared-tests-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-prepared-tests-wrong-scheme.txt index f9d10500d..10c52a6fb 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-prepared-tests-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-prepared-tests-wrong-scheme.txt @@ -4,8 +4,8 @@ Scheme: NONEXISTENT Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-wrong-scheme.txt index dae2a4233..10c52a6fb 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--error-wrong-scheme.txt @@ -3,10 +3,9 @@ Scheme: NONEXISTENT Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success-prepared-tests.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success-prepared-tests.txt index 49db1d402..dbbeffd80 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success-prepared-tests.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success-prepared-tests.txt @@ -4,8 +4,8 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: ✅ Build succeeded. (⏱️ ) └ Files: @@ -14,4 +14,4 @@ └ XCTest Run: /CalculatorApp Tests.xctestproducts/Tests/0/CalculatorApp.xctestrun Next steps: -1. Run prepared tests: xcodebuildmcp simulator test --test-products-path '/CalculatorApp Tests.xctestproducts' --simulator-name 'iPhone 17' +1. Run prepared tests: xcodebuildmcp simulator test --test-products-path '/CalculatorApp Tests.xctestproducts' --simulator-id diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success.txt index 7395ba47e..5189bc531 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build--success.txt @@ -3,14 +3,13 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: ✅ Build succeeded. (⏱️ ) └ Files: └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log Next steps: -1. Get built app path in simulator derived data: xcodebuildmcp simulator get-app-path --simulator-name 'iPhone 17' --scheme CalculatorApp --platform 'iOS Simulator' +1. Get built app path in simulator derived data: xcodebuildmcp simulator get-app-path --simulator-id --scheme CalculatorApp --platform 'iOS Simulator' --derived-data-path diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--error-compiler.txt index 0d23f830c..73361f216 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--error-compiler.txt @@ -3,15 +3,14 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: Compiler Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33:42 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53:42 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--error-wrong-scheme.txt index 21075fecf..9a1c6aa78 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--error-wrong-scheme.txt @@ -3,10 +3,9 @@ Scheme: NONEXISTENT Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--success.txt index 97674114a..e70a09b4f 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/build-and-run--success.txt @@ -3,10 +3,9 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: ℹ️ Resolving app path ✅ Resolving app path @@ -21,7 +20,7 @@ ├ Bundle ID: io.sentry.calculatorapp ├ Process ID: └ Files: - ├ App Path: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app + ├ App Path: /Build/Products/Debug-iphonesimulator/CalculatorApp.app ├ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_sim__pid.log ├ Runtime Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/io.sentry.calculatorapp__pid.log └ OSLog: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/io.sentry.calculatorapp_oslog__pid.log diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/get-app-path--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/get-app-path--error-wrong-scheme.txt index 4ccab0533..070beb687 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/get-app-path--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/get-app-path--error-wrong-scheme.txt @@ -3,9 +3,8 @@ Scheme: NONEXISTENT Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 + Simulator: Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/get-app-path--success.txt index 7b3473270..74b3c832d 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/get-app-path--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/get-app-path--success.txt @@ -3,16 +3,15 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 + Simulator: ✅ Get app path successful (⏱️ ) └ Files: - └ App Path: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app + └ App Path: /Build/Products/Debug-iphonesimulator/CalculatorApp.app Next steps: -1. Get bundle ID: xcodebuildmcp simulator get-app-bundle-id --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app +1. Get bundle ID: xcodebuildmcp simulator get-app-bundle-id --app-path /Build/Products/Debug-iphonesimulator/CalculatorApp.app 2. Boot simulator: xcodebuildmcp simulator boot --simulator-id SIMULATOR_UUID -3. Install app: xcodebuildmcp simulator install --simulator-id SIMULATOR_UUID --app-path ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app +3. Install app: xcodebuildmcp simulator install --simulator-id SIMULATOR_UUID --app-path /Build/Products/Debug-iphonesimulator/CalculatorApp.app 4. Launch app: xcodebuildmcp simulator launch-app --simulator-id SIMULATOR_UUID --bundle-id BUNDLE_ID diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/install--success.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/install--success.txt index 12875e64a..62d72e37d 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/install--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/install--success.txt @@ -2,7 +2,7 @@ 📦 Install App Simulator: - App Path: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app + App Path: /Build/Products/Debug-iphonesimulator/CalculatorApp.app ✅ App installed successfully diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--error-compiler.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--error-compiler.txt index bad0431de..2ba2412cc 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--error-compiler.txt @@ -3,10 +3,9 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: Selective Testing: CalculatorAppTests/CalculatorAppTests/testAddition @@ -16,7 +15,7 @@ Discovered 1 test(s): Compiler Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33:42 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53:42 ❌ Test failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--error-wrong-scheme.txt index 85568ca7f..5a994022d 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--error-wrong-scheme.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--error-wrong-scheme.txt @@ -3,10 +3,9 @@ Scheme: NONEXISTENT Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--failure.txt index 83d5b4e3b..a9028046a 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--failure.txt @@ -3,10 +3,9 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: Discovered 57 test(s): CalculatorAppFeatureTests/CalculatorBasicTests/testClear @@ -28,12 +27,12 @@ This should fail - display should be 0, not 999 CalculatorAppTests ✗ testCalculatorServiceFailure: - XCTAssertEqual failed: ("0") is not equal to ("999") - This test should fail - display should be 0, not 999 - example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:52 + /example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:52 IntentionalFailureTests ✗ test: - XCTAssertTrue failed - This test should fail to verify error reporting - example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:286 + /example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:286 ❌ tests failed, passed, skipped (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--success.txt b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--success.txt index e39ed1898..c649bc26b 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/simulator/test--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/simulator/test--success.txt @@ -3,10 +3,9 @@ Scheme: CalculatorApp Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace - Configuration: Debug Platform: iOS Simulator - Simulator: iPhone 17 - Derived Data: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp- + Simulator: + Derived Data: Selective Testing: CalculatorAppTests/CalculatorAppTests/testAddition diff --git a/src/snapshot-tests/__fixtures__/cli/text/swift-package/build--error-bad-path.txt b/src/snapshot-tests/__fixtures__/cli/text/swift-package/build--error-bad-path.txt index bbf70264d..47144859a 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/swift-package/build--error-bad-path.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/swift-package/build--error-bad-path.txt @@ -1,11 +1,11 @@ 📦 Swift Package Build - Package: /example_projects/NONEXISTENT + Package: /spm/NONEXISTENT Errors (1): - ✗ chdir error: No such file or directory (2): /example_projects/NONEXISTENT + ✗ chdir error: No such file or directory (2): /spm/NONEXISTENT ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/swift-package/build--success.txt b/src/snapshot-tests/__fixtures__/cli/text/swift-package/build--success.txt index 5a4cd434b..0395cc4c3 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/swift-package/build--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/swift-package/build--success.txt @@ -1,7 +1,7 @@ 📦 Swift Package Build - Package: /example_projects/spm + Package: /spm ✅ Build succeeded. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/swift-package/clean--error-bad-path.txt b/src/snapshot-tests/__fixtures__/cli/text/swift-package/clean--error-bad-path.txt index cb4abdeb9..eb91857c9 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/swift-package/clean--error-bad-path.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/swift-package/clean--error-bad-path.txt @@ -1,10 +1,10 @@ 🧹 Swift Package Clean - Package: /example_projects/NONEXISTENT + Package: /spm/NONEXISTENT Errors (1): - ✗ chdir error: No such file or directory (2): /example_projects/NONEXISTENT + ✗ chdir error: No such file or directory (2): /spm/NONEXISTENT ❌ Swift package clean failed. diff --git a/src/snapshot-tests/__fixtures__/cli/text/swift-package/clean--success.txt b/src/snapshot-tests/__fixtures__/cli/text/swift-package/clean--success.txt index a0e316a04..72204ef41 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/swift-package/clean--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/swift-package/clean--success.txt @@ -1,6 +1,6 @@ 🧹 Swift Package Clean - Package: /example_projects/spm + Package: /spm ✅ Swift package cleaned successfully diff --git a/src/snapshot-tests/__fixtures__/cli/text/swift-package/list--success.txt b/src/snapshot-tests/__fixtures__/cli/text/swift-package/list--success.txt index 17a38338f..d8f1cd25a 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/swift-package/list--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/swift-package/list--success.txt @@ -5,4 +5,4 @@ Running Processes (1): 🟢 spm PID: | Uptime: - Package: /example_projects/spm + Package: /spm diff --git a/src/snapshot-tests/__fixtures__/cli/text/swift-package/run--error-bad-executable.txt b/src/snapshot-tests/__fixtures__/cli/text/swift-package/run--error-bad-executable.txt index 049283385..2b1ea83d5 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/swift-package/run--error-bad-executable.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/swift-package/run--error-bad-executable.txt @@ -1,7 +1,7 @@ 🚀 Swift Package Run - Package: /example_projects/spm + Package: /spm Executable: nonexistent-executable Errors (1): diff --git a/src/snapshot-tests/__fixtures__/cli/text/swift-package/run--success.txt b/src/snapshot-tests/__fixtures__/cli/text/swift-package/run--success.txt index 05d8aae6b..1b320aa7c 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/swift-package/run--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/swift-package/run--success.txt @@ -1,14 +1,14 @@ 🚀 Swift Package Run - Package: /example_projects/spm + Package: /spm Executable: spm ✅ Build succeeded. (⏱️ ) ✅ Build & Run complete ├ Process ID: └ Files: - ├ App Path: example_projects/spm/.build/arm64-apple-macosx/debug/spm + ├ App Path: /spm/.build//debug/spm └ Build Logs: ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_spm__pid.log Output diff --git a/src/snapshot-tests/__fixtures__/cli/text/swift-package/test--error-bad-path.txt b/src/snapshot-tests/__fixtures__/cli/text/swift-package/test--error-bad-path.txt index 750e0865b..2913a1bca 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/swift-package/test--error-bad-path.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/swift-package/test--error-bad-path.txt @@ -8,7 +8,7 @@ Errors (1): - ✗ chdir error: No such file or directory (2): /example_projects/NONEXISTENT + ✗ chdir error: No such file or directory (2): /spm/NONEXISTENT ❌ Test failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/cli/text/swift-package/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/text/swift-package/test--failure.txt index 0516de499..8b20afd20 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/swift-package/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/swift-package/test--failure.txt @@ -16,7 +16,7 @@ Running tests (7 completed, 2 failures, 0 skipped) CalculatorAppTests ✗ testCalculatorServiceFailure: - XCTAssertEqual failed: ("0") is not equal to ("999") - This test should fail - display should be 0, not 999 - example_projects/spm/Tests/TestLibTests/SimpleTests.swift:49 + /spm/Tests/TestLibTests/SimpleTests.swift:49 (Unknown Suite) ✗ test: diff --git a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/snapshot-ui--success.txt b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/snapshot-ui--success.txt index 095f30a53..8fc16540b 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/snapshot-ui--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/snapshot-ui--success.txt @@ -29,7 +29,7 @@ Tips - Refs are snapshot-specific; after snapshot_ui or wait_for_ui, use refs from the latest output. - Use wait_for_ui for text/assertions or changing UI. -✅ Runtime UI snapshot captured with 28 elements, 19 likely targets, and 0 scroll areas. +✅ Runtime UI snapshot captured with elements, likely targets, and scroll areas. Next steps: 1. Refresh after layout changes: xcodebuildmcp ui-automation snapshot-ui --simulator-id diff --git a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/swipe--error-not-actionable.txt b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/swipe--error-not-actionable.txt index f227e42b4..0ac0a086d 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/swipe--error-not-actionable.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/swipe--error-not-actionable.txt @@ -5,8 +5,8 @@ Recovery Code: TARGET_NOT_ACTIONABLE - Message: Element ref 'e3' does not support 'swipeWithin'. - Element: e3 + Message: Element ref '' does not support 'swipeWithin'. + Element: Hint: Choose an elementRef that lists the required action, or refresh with snapshot_ui. -❌ Element ref 'e3' does not support 'swipeWithin'. +❌ Element ref '' does not support 'swipeWithin'. diff --git a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/swipe--success.txt b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/swipe--success.txt index 4a0a437d1..a6fd918b7 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/swipe--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/swipe--success.txt @@ -6,5 +6,4 @@ ✅ Swipe up within elementRef simulated successfully. Next steps: -1. Batch same-screen taps: xcodebuildmcp ui-automation batch --json '{"simulatorId":"","steps":[{"action":"tap","elementRef":""},{"action":"tap","elementRef":""}]}' -2. Tap an elementRef: xcodebuildmcp ui-automation tap --simulator-id --element-ref +1. diff --git a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/type-text--error-not-actionable.txt b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/type-text--error-not-actionable.txt index 27afd2f76..4a16a4a63 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/type-text--error-not-actionable.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/type-text--error-not-actionable.txt @@ -5,8 +5,8 @@ Recovery Code: TARGET_NOT_ACTIONABLE - Message: Element ref 'e3' does not support 'typeText'. - Element: e3 + Message: Element ref '' does not support 'typeText'. + Element: Hint: Choose an elementRef that lists the required action, or refresh with snapshot_ui. -❌ Element ref 'e3' does not support 'typeText'. +❌ Element ref '' does not support 'typeText'. diff --git a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/wait-for-ui--success.txt b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/wait-for-ui--success.txt index 043c407c5..624a65fac 100644 --- a/src/snapshot-tests/__fixtures__/cli/text/ui-automation/wait-for-ui--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/text/ui-automation/wait-for-ui--success.txt @@ -32,7 +32,7 @@ Tips - Refs are snapshot-specific; after snapshot_ui or wait_for_ui, use refs from the latest output. - Use wait_for_ui for text/assertions or changing UI. -✅ Wait completed; runtime UI snapshot refreshed with 28 elements, 19 likely targets, and 0 scroll areas. +✅ Wait completed; runtime UI snapshot refreshed with elements, likely targets, and scroll areas. Next steps: 1. Capture a fresh runtime UI snapshot: xcodebuildmcp ui-automation snapshot-ui --simulator-id diff --git a/src/snapshot-tests/__fixtures__/mcp/json/ui-automation/swipe--success.json b/src/snapshot-tests/__fixtures__/mcp/json/ui-automation/swipe--success.json index 3ed971670..9b6d1fec7 100644 --- a/src/snapshot-tests/__fixtures__/mcp/json/ui-automation/swipe--success.json +++ b/src/snapshot-tests/__fixtures__/mcp/json/ui-automation/swipe--success.json @@ -13,11 +13,11 @@ "direction": "up", "from": { "x": 201, - "y": 743 + "y": 468 }, "to": { "x": 201, - "y": 131 + "y": 406 } }, "artifacts": { @@ -28,45 +28,31 @@ "rs": "1", "screenHash": "", "seq": 1, - "count": 144, - "targets": [ - "|tap|button|Home Screen & App Library||com.apple.settings.homeScreen", - "|typeText|text-field||Search|", - "|tap|button|Camera||com.apple.settings.camera", - "|tap|button|Search||com.apple.settings.search", - "|tap|button|StandBy||com.apple.settings.standBy", - "|tap|button|Screen Time||com.apple.settings.screenTime", - "|tap|button|Passcode||com.apple.settings.passcodeAndBiometrics", - "|tap|button|Privacy & Security||com.apple.settings.privacyAndSecurity", - "|tap|button|Game Center||com.apple.settings.gameCenter", - "|tap|button|iCloud||com.apple.settings.iCloud", - "|tap|button|Apps||com.apple.settings.apps", - "|tap|button|Developer||com.apple.settings.developer", - "|tap|button|Dictate||Dictate", - "|tap|button|Dictate||Dictate" - ], + "count": 99999, + "targets": [], "scroll": [ - "|swipe|application|Settings||" + "|swipe|scroll-view|||snapshot-scroll-surface" ], "text": [ - "|text|text|Apple Intelligence & Siri||", - "|text|text|Camera||", - "|text|text|Home Screen & App Library||", - "|text|text|Search||", - "|text|text|StandBy||", - "|text|text|Screen Time||", - "|text|text|Passcode||", - "|text|text|Privacy & Security||", - "|text|text|Game Center||", - "|text|text|iCloud||", - "|text|text|Apps||", - "|text|text|Developer||" + "|text|text|Snapshot row 0||", + "|text|text|Snapshot row 1||", + "|text|text|Snapshot row 2||", + "|text|text|Snapshot row 3||", + "|text|text|Snapshot row 4||", + "|text|text|Snapshot row 5||", + "|text|text|Snapshot row 6||", + "|text|text|Snapshot row 7||", + "|text|text|Snapshot row 8||", + "|text|text|Snapshot row 9||", + "|text|text|Snapshot row 10||", + "|text|text|Snapshot row 11||", + "|text|text|Snapshot row 12||", + "|text|text|Snapshot row 13||" ], "udid": "" } }, "nextSteps": [ - "Batch same-screen taps: batch({ simulatorId: \"\", steps: [{\"action\":\"tap\",\"elementRef\":\"\"},{\"action\":\"tap\",\"elementRef\":\"\"}] })", - "Tap an elementRef: tap({ simulatorId: \"\", elementRef: \"\" })" + "" ] } diff --git a/src/snapshot-tests/__fixtures__/mcp/text/coverage/get-coverage-report--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/coverage/get-coverage-report--success.txt index bae11ffd9..b4ae271a8 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/coverage/get-coverage-report--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/coverage/get-coverage-report--success.txt @@ -1,10 +1,10 @@ 📊 Coverage Report -ℹ️ Overall: 94.9% (371/391 lines) +ℹ️ Overall: 4.6% (18/391 lines) Targets - CalculatorAppTests.xctest: 94.9% (371/391 lines) + CalculatorAppTests.xctest: 4.6% (18/391 lines) Next steps: 1. View file-level coverage: get_file_coverage({ xcresultPath: "/TestResults.xcresult" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/coverage/get-file-coverage--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/coverage/get-file-coverage--success.txt index 2d539e0a3..5742a6109 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/coverage/get-file-coverage--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/coverage/get-file-coverage--success.txt @@ -3,24 +3,37 @@ File: example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorService.swift -ℹ️ Coverage: 83.1% (157/189 lines) +ℹ️ Coverage: 55.0% (104/189 lines) -🔴 Not Covered (7 functions, 22 lines) +🔴 Not Covered (19 functions, 70 lines) L159 CalculatorService.deleteLastDigit() -- 0/16 lines + L63 CalculatorService.inputDecimal() -- 0/14 lines + L148 CalculatorService.clear() -- 0/10 lines + L134 CalculatorService.toggleSign() -- 0/6 lines + L141 CalculatorService.percentage() -- 0/6 lines + L178 CalculatorService.setError(_:) -- 0/5 lines L58 implicit closure #2 in CalculatorService.inputNumber(_:) -- 0/1 lines + L68 implicit closure #1 in CalculatorService.inputDecimal() -- 0/1 lines + L94 implicit closure #1 in CalculatorService.calculate() -- 0/1 lines L98 implicit closure #3 in CalculatorService.calculate() -- 0/1 lines L99 implicit closure #4 in CalculatorService.calculate() -- 0/1 lines L162 implicit closure #1 in CalculatorService.deleteLastDigit() -- 0/1 lines L172 implicit closure #2 in CalculatorService.deleteLastDigit() -- 0/1 lines + L209 implicit closure #3 in CalculatorService.formatNumber(_:) -- 0/1 lines L214 implicit closure #4 in CalculatorService.formatNumber(_:) -- 0/1 lines + L221 CalculatorService.currentValue.getter -- 0/1 lines + L222 CalculatorService.previousValue.getter -- 0/1 lines + L223 CalculatorService.currentOperation.getter -- 0/1 lines + L224 CalculatorService.willResetDisplay.getter -- 0/1 lines -🟡 Partial Coverage (4 functions) +🟡 Partial Coverage (5 functions) L184 CalculatorService.updateExpressionDisplay() -- 80.0% (8/10 lines) + L93 CalculatorService.calculate() -- 84.2% (32/38 lines) + L47 CalculatorService.inputNumber(_:) -- 85.7% (12/14 lines) + L78 CalculatorService.setOperation(_:) -- 85.7% (12/14 lines) L195 CalculatorService.formatNumber(_:) -- 85.7% (18/21 lines) - L93 CalculatorService.calculate() -- 89.5% (34/38 lines) - L63 CalculatorService.inputDecimal() -- 92.9% (13/14 lines) -🟢 Full Coverage (28 functions) -- all at 100% +🟢 Full Coverage (15 functions) -- all at 100% Next steps: 1. View overall coverage: get_coverage_report({ xcresultPath: "/TestResults.xcresult" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/debugging/lldb-command--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/debugging/lldb-command--success.txt index 8894bef41..88183d729 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/debugging/lldb-command--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/debugging/lldb-command--success.txt @@ -4,9 +4,4 @@ ✅ Command executed Output: - Current breakpoints: - 1: file = 'ContentView.swift', line = 42, exact_match = 0, locations = - Names: - dap - - 1.1: where = CalculatorApp.debug.dylib`closure #1 in closure #1 in closure #1 in ContentView.body.getter + at ContentView.swift:42:31, address = , resolved, hit count = 0 + No breakpoints currently set. diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/build--error-compiler.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/build--error-compiler.txt index c720ef98e..c31a09d49 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/device/build--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/build--error-compiler.txt @@ -4,7 +4,7 @@ Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/build--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/build--success.txt index 9f8794f70..50f8c540a 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/device/build--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/build--success.txt @@ -6,4 +6,4 @@ └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_device__pid.log — Build Logs Next steps: -1. Get built device app path: get_device_app_path({ scheme: "CalculatorApp" }) +1. Get built device app path: get_device_app_path({ scheme: "CalculatorApp", derivedDataPath: "" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/build-and-run--error-compiler.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/build-and-run--error-compiler.txt index dff811af7..218365c3e 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/device/build-and-run--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/build-and-run--error-compiler.txt @@ -4,7 +4,7 @@ Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/build-and-run--success.txt index 10d63318d..cd050c55e 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/device/build-and-run--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/build-and-run--success.txt @@ -12,9 +12,8 @@ ├ Bundle ID: io.sentry.calculatorapp ├ Process ID: └ Files: - └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ - ├── DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app — App Path - └── logs/build_run_device__pid.log — Build Logs + ├── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_device__pid.log — Build Logs + └── /Build/Products/Debug-iphoneos/CalculatorApp.app — App Path Next steps: 1. Stop app on device: stop_app_device({ deviceId: "", processId: }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/get-app-path--success.txt index 9fc79b8fd..bb82a7dcf 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/device/get-app-path--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/get-app-path--success.txt @@ -3,9 +3,9 @@ ✅ Success └ Files: - └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app — App Path + └── /Build/Products/Debug-iphoneos/CalculatorApp.app — App Path Next steps: -1. Get bundle ID: get_app_bundle_id({ appPath: "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app" }) -2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app" }) +1. Get bundle ID: get_app_bundle_id({ appPath: "/Build/Products/Debug-iphoneos/CalculatorApp.app" }) +2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "/Build/Products/Debug-iphoneos/CalculatorApp.app" }) 3. Launch app on device: launch_app_device({ deviceId: "DEVICE_UDID", bundleId: "BUNDLE_ID" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/test--error-compiler.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/test--error-compiler.txt index 2bb489471..ff6a7bdf4 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/device/test--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/test--error-compiler.txt @@ -7,10 +7,8 @@ Discovered 1 test(s): Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53 ❌ Test failed. (⏱️ ) └ Files: - └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ - ├── logs/test_device__pid.log — Build Logs - └── result-bundles/test_device__pid.xcresult — Result Bundle + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/test_device__pid.log — Build Logs diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/test--failure.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/test--failure.txt index d4fe8e5d6..0ab31d224 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/device/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/test--failure.txt @@ -22,4 +22,5 @@ Test Failures (2): └ Files: └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ ├── logs/test_device__pid.log — Build Logs - └── result-bundles/test_device__pid.xcresult — Result Bundle + ├── result-bundles/test_device__pid.xcresult — Result Bundle + └── test-products/test_device__pid.xctestproducts — Test Products diff --git a/src/snapshot-tests/__fixtures__/mcp/text/device/test--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/device/test--success.txt index aeba621eb..69c78ee8d 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/device/test--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/device/test--success.txt @@ -8,4 +8,5 @@ Discovered 1 test(s): └ Files: └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ ├── logs/test_device__pid.log — Build Logs - └── result-bundles/test_device__pid.xcresult — Result Bundle + ├── result-bundles/test_device__pid.xcresult — Result Bundle + └── test-products/test_device__pid.xctestproducts — Test Products diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/build--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/build--success.txt index 6d3ee03ab..5aba95064 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/macos/build--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/build--success.txt @@ -7,4 +7,4 @@ └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_macos__pid.log — Build Logs Next steps: -1. Get built macOS app path: get_mac_app_path({ scheme: "MCPTest" }) +1. Get built macOS app path: get_mac_app_path({ scheme: "MCPTest", derivedDataPath: "/DerivedData" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/build-and-run--success.txt index f45718388..f695b3d92 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/macos/build-and-run--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/build-and-run--success.txt @@ -11,9 +11,8 @@ ├ Bundle ID: io.sentry.MCPTest.macOS ├ Process ID: └ Files: - └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ - ├── DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app — App Path - └── logs/build_run_macos__pid.log — Build Logs + ├── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_macos__pid.log — Build Logs + └── /DerivedData/Build/Products/Debug/MCPTest.app — App Path Next steps: 1. Interact with the launched app in the foreground diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/get-app-path--success.txt index 48d88a45c..cd6e80ab1 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/macos/get-app-path--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/get-app-path--success.txt @@ -3,8 +3,8 @@ ✅ Success └ Files: - └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app — App Path + └── /DerivedData/Build/Products/Debug/MCPTest.app — App Path Next steps: -1. Get bundle ID: get_mac_bundle_id({ appPath: "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app" }) -2. Launch app: launch_mac_app({ appPath: "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/MCPTest-/Build/Products/Debug/MCPTest.app" }) +1. Get bundle ID: get_mac_bundle_id({ appPath: "/DerivedData/Build/Products/Debug/MCPTest.app" }) +2. Launch app: launch_mac_app({ appPath: "/DerivedData/Build/Products/Debug/MCPTest.app" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/macos/get-macos-bundle-id--error-missing-app.txt b/src/snapshot-tests/__fixtures__/mcp/text/macos/get-macos-bundle-id--error-missing-app.txt index fac62d2f5..16dde937c 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/macos/get-macos-bundle-id--error-missing-app.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/macos/get-macos-bundle-id--error-missing-app.txt @@ -3,6 +3,6 @@ Errors (1): - ✗ File not found: '/nonexistent/path/Fake.app'. Please check the path and try again. + ✗ File not found: '/missing.app'. Please check the path and try again. ❌ Failed to get macOS bundle ID. diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--error-compiler.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--error-compiler.txt index e94f9f3a0..4e43a90c6 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--error-compiler.txt @@ -4,7 +4,7 @@ Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success-prepared-tests.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success-prepared-tests.txt index 550e86618..2c2708841 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success-prepared-tests.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success-prepared-tests.txt @@ -8,4 +8,4 @@ └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log — Build Logs Next steps: -1. Run prepared tests: test_sim({ testProductsPath: "/CalculatorApp Tests.xctestproducts", simulatorName: "iPhone 17" }) +1. Run prepared tests: test_sim({ testProductsPath: "/CalculatorApp Tests.xctestproducts", simulatorId: "" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success.txt index cb5804c08..02cab56e8 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build--success.txt @@ -6,4 +6,4 @@ └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_sim__pid.log — Build Logs Next steps: -1. Get built app path in simulator derived data: get_sim_app_path({ simulatorName: "iPhone 17", scheme: "CalculatorApp", platform: "iOS Simulator" }) +1. Get built app path in simulator derived data: get_sim_app_path({ simulatorId: "", scheme: "CalculatorApp", platform: "iOS Simulator", derivedDataPath: "" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build-and-run--error-compiler.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build-and-run--error-compiler.txt index 3dcc58bda..7fad89001 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build-and-run--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build-and-run--error-compiler.txt @@ -4,7 +4,7 @@ Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53 ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build-and-run--success.txt index 354429bd5..015abda20 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/simulator/build-and-run--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/build-and-run--success.txt @@ -14,12 +14,11 @@ ├ Bundle ID: io.sentry.calculatorapp ├ Process ID: └ Files: - └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/ - ├── DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app — App Path - └── logs/ - ├── build_run_sim__pid.log — Build Logs - ├── io.sentry.calculatorapp__pid.log — Runtime Logs - └── io.sentry.calculatorapp_oslog__pid.log — OSLog + ├── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/ + │ ├── build_run_sim__pid.log — Build Logs + │ ├── io.sentry.calculatorapp__pid.log — Runtime Logs + │ └── io.sentry.calculatorapp_oslog__pid.log — OSLog + └── /Build/Products/Debug-iphonesimulator/CalculatorApp.app — App Path Next steps: 1. Stop app in simulator: stop_app_sim({ simulatorId: "", bundleId: "io.sentry.calculatorapp" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/get-app-path--success.txt index 7efb25cf8..525822f4f 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/simulator/get-app-path--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/get-app-path--success.txt @@ -3,10 +3,10 @@ ✅ Get app path successful (⏱️ ) └ Files: - └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app — App Path + └── /Build/Products/Debug-iphonesimulator/CalculatorApp.app — App Path Next steps: -1. Get bundle ID: get_app_bundle_id({ appPath: "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app" }) +1. Get bundle ID: get_app_bundle_id({ appPath: "/Build/Products/Debug-iphonesimulator/CalculatorApp.app" }) 2. Boot simulator: boot_sim({ simulatorId: "SIMULATOR_UUID" }) -3. Install app: install_app_sim({ simulatorId: "SIMULATOR_UUID", appPath: "~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app" }) +3. Install app: install_app_sim({ simulatorId: "SIMULATOR_UUID", appPath: "/Build/Products/Debug-iphonesimulator/CalculatorApp.app" }) 4. Launch app: launch_app_sim({ simulatorId: "SIMULATOR_UUID", bundleId: "BUNDLE_ID" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--error-compiler.txt b/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--error-compiler.txt index a9d7a15e7..ea10a5158 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--error-compiler.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/simulator/test--error-compiler.txt @@ -7,7 +7,7 @@ Discovered 1 test(s): Errors (1): ✗ cannot convert value of type 'String' to specified type 'Int' - /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:33 + /example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift:53 ❌ Test failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/build--error-bad-path.txt b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/build--error-bad-path.txt index 466e7b1a2..a050d4705 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/build--error-bad-path.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/build--error-bad-path.txt @@ -3,7 +3,7 @@ Errors (1): - ✗ chdir error: No such file or directory (2): /example_projects/NONEXISTENT + ✗ chdir error: No such file or directory (2): /spm/NONEXISTENT ❌ Build failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/clean--error-bad-path.txt b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/clean--error-bad-path.txt index 0363983ec..ebe3a0c50 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/clean--error-bad-path.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/clean--error-bad-path.txt @@ -3,6 +3,6 @@ Errors (1): - ✗ chdir error: No such file or directory (2): /example_projects/NONEXISTENT + ✗ chdir error: No such file or directory (2): /spm/NONEXISTENT ❌ Swift package clean failed. diff --git a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/list--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/list--success.txt index 17a38338f..d8f1cd25a 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/list--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/list--success.txt @@ -5,4 +5,4 @@ Running Processes (1): 🟢 spm PID: | Uptime: - Package: /example_projects/spm + Package: /spm diff --git a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/run--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/run--success.txt index 5b38814ec..abfb0ae43 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/run--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/run--success.txt @@ -5,8 +5,8 @@ ✅ Build & Run complete ├ Process ID: └ Files: - ├── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_spm__pid.log — Build Logs - └── example_projects/spm/.build/arm64-apple-macosx/debug/spm — App Path + ├── /spm/.build//debug/spm — App Path + └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/build_run_spm__pid.log — Build Logs Output Hello, world! diff --git a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/test--error-bad-path.txt b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/test--error-bad-path.txt index 2d2fe3081..e941ccfb7 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/test--error-bad-path.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/test--error-bad-path.txt @@ -3,7 +3,7 @@ Errors (1): - ✗ chdir error: No such file or directory (2): /example_projects/NONEXISTENT + ✗ chdir error: No such file or directory (2): /spm/NONEXISTENT ❌ Test failed. (⏱️ ) └ Files: diff --git a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/test--failure.txt b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/test--failure.txt index 821bb760b..6c0f814c9 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/swift-package/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/swift-package/test--failure.txt @@ -3,13 +3,13 @@ Test Failures (2): - ✗ CalculatorAppTests / testCalculatorServiceFailure: XCTAssertEqual failed: ("0") is not equal to ("999") - This test should fail - display should be 0, not 999 - /example_projects/spm/Tests/TestLibTests/SimpleTests.swift:49 - ✗ (Unknown Suite) / test: Expectation failed: Bool(false) Test failed SimpleTests.swift:57 + ✗ CalculatorAppTests / testCalculatorServiceFailure: XCTAssertEqual failed: ("0") is not equal to ("999") - This test should fail - display should be 0, not 999 + /spm/Tests/TestLibTests/SimpleTests.swift:49 + ❌ tests failed, passed, skipped (⏱️ ) └ Files: └── ~/Library/Developer/XcodeBuildMCP/workspaces/XcodeBuildMCP-/logs/swift_package_test__pid.log — Build Logs diff --git a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/snapshot-ui--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/snapshot-ui--success.txt index 2ee723a91..2a4af955d 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/snapshot-ui--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/snapshot-ui--success.txt @@ -27,7 +27,7 @@ Tips - Refs are snapshot-specific; after snapshot_ui or wait_for_ui, use refs from the latest output. - Use wait_for_ui for text/assertions or changing UI. -✅ Runtime UI snapshot captured with 28 elements, 19 likely targets, and 0 scroll areas. +✅ Runtime UI snapshot captured with elements, likely targets, and scroll areas. Next steps: 1. Refresh after layout changes: snapshot_ui({ simulatorId: "" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/swipe--error-not-actionable.txt b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/swipe--error-not-actionable.txt index 51d69da9e..16fa6dbfa 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/swipe--error-not-actionable.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/swipe--error-not-actionable.txt @@ -3,8 +3,8 @@ Recovery Code: TARGET_NOT_ACTIONABLE - Message: Element ref 'e3' does not support 'swipeWithin'. - Element: e3 + Message: Element ref '' does not support 'swipeWithin'. + Element: Hint: Choose an elementRef that lists the required action, or refresh with snapshot_ui. -❌ Element ref 'e3' does not support 'swipeWithin'. +❌ Element ref '' does not support 'swipeWithin'. diff --git a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/swipe--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/swipe--success.txt index 868cf4400..c66def209 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/swipe--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/swipe--success.txt @@ -4,5 +4,4 @@ ✅ Swipe up within elementRef simulated successfully. Next steps: -1. Batch same-screen taps: batch({ simulatorId: "", steps: [{"action":"tap","elementRef":""},{"action":"tap","elementRef":""}] }) -2. Tap an elementRef: tap({ simulatorId: "", elementRef: "" }) +1. diff --git a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/type-text--error-not-actionable.txt b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/type-text--error-not-actionable.txt index 93cf24775..0f1d6359c 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/type-text--error-not-actionable.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/type-text--error-not-actionable.txt @@ -3,8 +3,8 @@ Recovery Code: TARGET_NOT_ACTIONABLE - Message: Element ref 'e3' does not support 'typeText'. - Element: e3 + Message: Element ref '' does not support 'typeText'. + Element: Hint: Choose an elementRef that lists the required action, or refresh with snapshot_ui. -❌ Element ref 'e3' does not support 'typeText'. +❌ Element ref '' does not support 'typeText'. diff --git a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/wait-for-ui--success.txt b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/wait-for-ui--success.txt index 63baa2d7f..d751f5b53 100644 --- a/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/wait-for-ui--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/text/ui-automation/wait-for-ui--success.txt @@ -30,7 +30,7 @@ Tips - Refs are snapshot-specific; after snapshot_ui or wait_for_ui, use refs from the latest output. - Use wait_for_ui for text/assertions or changing UI. -✅ Wait completed; runtime UI snapshot refreshed with 28 elements, 19 likely targets, and 0 scroll areas. +✅ Wait completed; runtime UI snapshot refreshed with elements, likely targets, and scroll areas. Next steps: 1. Capture a fresh runtime UI snapshot: snapshot_ui({ simulatorId: "" }) diff --git a/src/snapshot-tests/__tests__/fixture-io.test.ts b/src/snapshot-tests/__tests__/fixture-io.test.ts index cce46a848..ca1983c01 100644 --- a/src/snapshot-tests/__tests__/fixture-io.test.ts +++ b/src/snapshot-tests/__tests__/fixture-io.test.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; -import { expectMatchesFixture, fixturePathFor } from '../fixture-io.ts'; +import { expectMatchesFixture, expectResultMatchesFixture, fixturePathFor } from '../fixture-io.ts'; const workflow = '__fixture_diff_test__'; const scenario = 'block-diff'; @@ -81,3 +81,113 @@ describe('fixture diff formatting', () => { }).toThrowError(/\+\s+3 same\n\+\s+4 same/); }); }); + +describe('snapshot result outcome validation', () => { + it('rejects an unexpected error before update mode can overwrite a success fixture', () => { + fs.mkdirSync(fixtureDir, { recursive: true }); + fs.writeFileSync(fixturePath, 'known success', 'utf8'); + const previousUpdateMode = process.env.UPDATE_SNAPSHOTS; + process.env.UPDATE_SNAPSHOTS = '1'; + + try { + expect(() => { + expectResultMatchesFixture( + { + text: 'normalized error', + rawText: 'raw setup failure', + isError: true, + outcome: 'domain-error', + }, + 'success', + { runtime: 'cli/text', workflow, scenario }, + ); + }).toThrowError(/expected success, received domain-error[\s\S]*raw setup failure/); + expect(fs.readFileSync(fixturePath, 'utf8')).toBe('known success'); + } finally { + if (previousUpdateMode === undefined) { + delete process.env.UPDATE_SNAPSHOTS; + } else { + process.env.UPDATE_SNAPSHOTS = previousUpdateMode; + } + } + }); + + it('writes a fixture after the expected outcome is confirmed', () => { + const previousUpdateMode = process.env.UPDATE_SNAPSHOTS; + process.env.UPDATE_SNAPSHOTS = '1'; + + try { + expectResultMatchesFixture( + { + text: 'expected error', + rawText: 'raw error', + isError: true, + outcome: 'domain-error', + }, + 'error', + { runtime: 'cli/text', workflow, scenario }, + ); + expect(fs.readFileSync(fixturePath, 'utf8')).toBe('expected error'); + } finally { + if (previousUpdateMode === undefined) { + delete process.env.UPDATE_SNAPSHOTS; + } else { + process.env.UPDATE_SNAPSHOTS = previousUpdateMode; + } + } + }); + + it('allows deterministic validation errors to update error fixtures', () => { + const previousUpdateMode = process.env.UPDATE_SNAPSHOTS; + process.env.UPDATE_SNAPSHOTS = '1'; + + try { + expectResultMatchesFixture( + { + text: 'expected validation error', + rawText: 'MCP input validation error', + isError: true, + outcome: 'validation-error', + }, + 'error', + { runtime: 'cli/text', workflow, scenario }, + ); + expect(fs.readFileSync(fixturePath, 'utf8')).toBe('expected validation error'); + } finally { + if (previousUpdateMode === undefined) { + delete process.env.UPDATE_SNAPSHOTS; + } else { + process.env.UPDATE_SNAPSHOTS = previousUpdateMode; + } + } + }); + + it('rejects infrastructure errors before update mode can overwrite an error fixture', () => { + fs.mkdirSync(fixtureDir, { recursive: true }); + fs.writeFileSync(fixturePath, 'known domain error', 'utf8'); + const previousUpdateMode = process.env.UPDATE_SNAPSHOTS; + process.env.UPDATE_SNAPSHOTS = '1'; + + try { + expect(() => { + expectResultMatchesFixture( + { + text: 'normalized infrastructure error', + rawText: 'MCP transport failed', + isError: true, + outcome: 'infrastructure-error', + }, + 'error', + { runtime: 'mcp/text', workflow, scenario }, + ); + }).toThrowError(/expected error, received infrastructure-error[\s\S]*MCP transport failed/); + expect(fs.readFileSync(fixturePath, 'utf8')).toBe('known domain error'); + } finally { + if (previousUpdateMode === undefined) { + delete process.env.UPDATE_SNAPSHOTS; + } else { + process.env.UPDATE_SNAPSHOTS = previousUpdateMode; + } + } + }); +}); diff --git a/src/snapshot-tests/__tests__/json-harness-error-state.test.ts b/src/snapshot-tests/__tests__/json-harness-error-state.test.ts index 573b66347..7cb44d39a 100644 --- a/src/snapshot-tests/__tests__/json-harness-error-state.test.ts +++ b/src/snapshot-tests/__tests__/json-harness-error-state.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { assertCliSnapshotProcessResult, resolveCliJsonSnapshotErrorState } from '../harness.ts'; -import { resolveMcpSnapshotErrorState } from '../mcp-harness.ts'; +import { + assertCliSnapshotProcessResult, + resolveCliJsonSnapshotErrorState, + resolveCliJsonSnapshotOutcome, +} from '../harness.ts'; +import { resolveMcpSnapshotOutcome } from '../mcp-harness.ts'; import type { StructuredOutputEnvelope } from '../../types/structured-output.ts'; const successEnvelope: StructuredOutputEnvelope = { @@ -57,6 +61,8 @@ describe('JSON snapshot harness error state', () => { it('uses CLI process status and envelope.didError when they agree', () => { expect(resolveCliJsonSnapshotErrorState(0, successEnvelope, 'tool')).toBe(false); expect(resolveCliJsonSnapshotErrorState(1, errorEnvelope, 'tool')).toBe(true); + expect(resolveCliJsonSnapshotOutcome(0, successEnvelope, 'tool')).toBe('success'); + expect(resolveCliJsonSnapshotOutcome(1, errorEnvelope, 'tool')).toBe('domain-error'); }); it('rejects null CLI process status', () => { @@ -75,18 +81,33 @@ describe('JSON snapshot harness error state', () => { }); it('uses MCP transport isError and structuredContent.didError when they agree', () => { - expect(resolveMcpSnapshotErrorState(false, false, 'tool')).toBe(false); - expect(resolveMcpSnapshotErrorState(true, true, 'tool')).toBe(true); + expect(resolveMcpSnapshotOutcome(false, false, 'tool')).toBe('success'); + expect(resolveMcpSnapshotOutcome(true, true, 'tool')).toBe('domain-error'); + }); + + it('classifies an MCP error without a domain envelope as an infrastructure failure', () => { + expect(resolveMcpSnapshotOutcome(true, undefined, 'tool')).toBe('infrastructure-error'); + }); + + it('classifies MCP argument validation separately from infrastructure failures', () => { + expect( + resolveMcpSnapshotOutcome( + true, + undefined, + 'tool', + 'MCP error -32602: Input validation error: Invalid arguments for tool', + ), + ).toBe('validation-error'); }); it('rejects MCP transport isError and structuredContent.didError disagreement', () => { - expect(() => resolveMcpSnapshotErrorState(true, false, 'tool')).toThrow( + expect(() => resolveMcpSnapshotOutcome(true, false, 'tool')).toThrow( 'MCP result.isError (true) disagrees with structuredContent.didError (false)', ); - expect(() => resolveMcpSnapshotErrorState(false, true, 'tool')).toThrow( + expect(() => resolveMcpSnapshotOutcome(false, true, 'tool')).toThrow( 'MCP result.isError (false) disagrees with structuredContent.didError (true)', ); - expect(() => resolveMcpSnapshotErrorState(undefined, true, 'tool')).toThrow( + expect(() => resolveMcpSnapshotOutcome(undefined, true, 'tool')).toThrow( 'MCP result.isError (undefined) disagrees with structuredContent.didError (true)', ); }); diff --git a/src/snapshot-tests/__tests__/json-normalize.test.ts b/src/snapshot-tests/__tests__/json-normalize.test.ts index 5b78a1dd1..f638f6518 100644 --- a/src/snapshot-tests/__tests__/json-normalize.test.ts +++ b/src/snapshot-tests/__tests__/json-normalize.test.ts @@ -172,15 +172,15 @@ describe('normalizeStructuredEnvelope', () => { rs: '1', screenHash: 'screen-hash', seq: 7, - count: 2, + count: 99999, targets: ['e48|tap|button|Camera||com.apple.settings.camera'], scroll: ['e1|swipe|application|Calculator||Calculator'], text: ['e42|text|text|12:34||'], }, }, nextSteps: [ - 'Tap: xcodebuildmcp ui-automation tap --element-ref e48', - 'MCP: swipe({ withinElementRef: "e1" })', + 'Take screenshot for verification: screenshot({ simulatorId: "SIMULATOR-1" })', + 'Scroll visible content: swipe({ withinElementRef: "e1" })', ], }; @@ -199,16 +199,13 @@ describe('normalizeStructuredEnvelope', () => { rs: '1', screenHash: '', seq: 1, - count: 2, + count: 99999, targets: ['|tap|button|Camera||com.apple.settings.camera'], scroll: ['|swipe|application|Calculator||Calculator'], text: ['|text|text|