diff --git a/docs/dev/investigation-simulator-test-hang-2026-04-11.md b/docs/dev/investigation-simulator-test-hang-2026-04-11.md new file mode 100644 index 00000000..ada175d1 --- /dev/null +++ b/docs/dev/investigation-simulator-test-hang-2026-04-11.md @@ -0,0 +1,170 @@ +# Investigation: Simulator test hang and EMFILE failures + +## Summary +XcodeBuildMCP definitively leaks detached simulator OSLog stream processes from the simulator launch-with-logging path. Real-world verification showed that production code created orphaned `simctl spawn log stream ...` processes under PID 1, the normal `simulator stop` command did not clean them up, and repeated launches accumulated additional survivors. After clearing the leaked processes, the previously unhealthy simulator recovered: `xcrun simctl get_app_container ...` completed successfully in `259 ms`, and a real `node build/cli.js simulator test ...` run progressed through `test-without-building`, executed tests, and returned control to the terminal instead of hanging at `Process spawn via launchd failed`. That makes the leak the confirmed cause of the broken simulator state behind the original symptom. + +## Symptoms +- `build-for-testing` succeeds. +- `test-without-building` reports `Process spawn via launchd failed` and `Too many open files`. +- `NSPOSIXErrorDomain Code 24` appears in Xcode output. +- The failing `xcodebuild` process can remain alive after printing the error. +- On the machine, dozens of orphaned `simctl spawn ... log stream ...` processes existed for the same simulator and bundle. + +## Investigation Log + +### Phase 1 - Initial machine assessment +**Hypothesis:** The simulator/Xcode environment had accumulated leaked processes or descriptors from earlier E2E/manual work. +**Findings:** The shell limit was high, but launchd soft maxfiles was low, and the machine had many orphaned simulator log-stream processes plus multiple stuck `xcodebuild` instances. +**Evidence:** +- `ulimit -n` returned `1048575`. +- `launchctl limit maxfiles` returned soft `256`, hard `unlimited`. +- `ps` showed **81** live `simctl spawn 01DA97D9-3856-46C5-A75E-DDD48100B2DB log stream --level=debug --predicate subsystem == "io.sentry.calculatorapp"` processes under PID 1. +- `ps -p 46498,48422 -o pid,ppid,stat,etime,command` showed two stuck `xcodebuild ... test-without-building` processes still alive. +- `lsof -p 46498 | wc -l` and `lsof -p 48422 | wc -l` each showed about 200 open fds. +**Conclusion:** Confirmed degraded local environment. Needed to separate product leak from broader simulator/Xcode damage. + +### Phase 2 - Code path identification +**Hypothesis:** Recent simulator launch work in XcodeBuildMCP explicitly creates detached OSLog stream processes and fails to manage their lifecycle. +**Findings:** `launchSimulatorAppWithLogging()` starts an OSLog stream via `startOsLogStream()`. That helper spawns detached `xcrun simctl spawn ... log stream` children and immediately `unref()`s them. `stop_app_sim` only terminates the app and does not stop the OSLog stream. Session tracking only accounts for `activeLogSessions` from `log_capture.ts`, not these detached children. +**Evidence:** +- `src/utils/simulator-steps.ts:205` calls `startOsLogStream(...)`. +- `src/utils/simulator-steps.ts:251` defines `startOsLogStream`. +- `src/utils/simulator-steps.ts:278` sets `detached: true`. +- `src/utils/simulator-steps.ts:281` calls `child.unref()`. +- `src/mcp/tools/simulator/stop_app_sim.ts:58` only runs `['xcrun', 'simctl', 'terminate', simulatorId, params.bundleId]`. +- `src/utils/log_capture.ts:60` stores tracked sessions in `activeLogSessions`. +- `src/utils/log-capture/index.ts:10-11` lists only `activeLogSessions` ids. +- `src/utils/session-status.ts:52` reports simulator active session ids from `listActiveSimulatorLogSessionIds()`. +**Conclusion:** Confirmed product design bug: detached OSLog children are created outside the tracked log-session lifecycle. + +### Phase 3 - Real-world proof that production code creates orphaned OSLog children +**Hypothesis:** The production simulator launch helper is capable of creating the exact orphaned `simctl spawn ... log stream ...` processes observed on the machine. +**Findings:** After clearing all existing matching orphan processes, running the production `launchSimulatorAppWithLogging()` helper created a new detached `simctl spawn ... log stream ...` process under PID 1 for the exact simulator and bundle under investigation. +**Evidence:** +- Before the controlled run, matching process count was `0`. +- Production helper invocation used built code: + ```sh + node --input-type=module - <<'NODE' + import { launchSimulatorAppWithLogging } from './build/utils/simulator-steps.js'; + const fakeExecutor = async () => ({ success: true, output: 'io.sentry.calculatorapp: 123', process: { pid: 123 }, exitCode: 0 }); + const result = await launchSimulatorAppWithLogging( + '01DA97D9-3856-46C5-A75E-DDD48100B2DB', + 'io.sentry.calculatorapp', + fakeExecutor, + ); + console.log(JSON.stringify(result, null, 2)); + NODE + ``` +- That helper returned success and produced an OSLog file: + `/Users/cameroncooke/Library/Developer/XcodeBuildMCP/logs/io.sentry.calculatorapp_oslog_2026-04-11T08-46-40-929Z_pid62912.log` +- Immediately afterward, process table showed: + `62966 1 00:11 /Library/Developer/PrivateFrameworks/CoreSimulator.framework/Versions/A/Resources/bin/simctl spawn 01DA97D9-3856-46C5-A75E-DDD48100B2DB log stream --level=debug --predicate subsystem == "io.sentry.calculatorapp"` +- The OSLog file contained runtime output from the launched app, including: + `Calculator app launched` +**Conclusion:** Definitively confirmed. The observed orphan command line is created by XcodeBuildMCP production code. + +### Phase 4 - Real-world proof that normal stop does not clean up the leaked child +**Hypothesis:** The normal stop tool leaves the detached OSLog stream running. +**Findings:** Running the normal stop command successfully terminated the app, but the detached log-stream process remained alive under PID 1. +**Evidence:** +- Stop command used: + ```sh + node build/cli.js simulator stop --simulator-id 01DA97D9-3856-46C5-A75E-DDD48100B2DB --bundle-id io.sentry.calculatorapp --output text + ``` +- CLI output reported `App stopped successfully`. +- After stop, process table still showed: + `62966 1 00:20 /Library/Developer/PrivateFrameworks/CoreSimulator.framework/Versions/A/Resources/bin/simctl spawn 01DA97D9-3856-46C5-A75E-DDD48100B2DB log stream --level=debug --predicate subsystem == "io.sentry.calculatorapp"` +- Matching process count remained `1`. +**Conclusion:** Definitively confirmed. `simulator stop` does not stop the detached OSLog stream child created by launch-with-logging. + +### Phase 5 - Real-world proof of accumulation +**Hypothesis:** Repeated launches can accumulate additional detached OSLog stream survivors. +**Findings:** A second invocation of the same production helper created another survivor. Two distinct PIDs were alive concurrently under PID 1. +**Evidence:** +- Second production helper invocation returned success and wrote a second OSLog file: + `/Users/cameroncooke/Library/Developer/XcodeBuildMCP/logs/io.sentry.calculatorapp_oslog_2026-04-11T08-47-12-997Z_pid63093.log` +- Process table then showed both: + - `62966 1 00:49 /Library/Developer/PrivateFrameworks/CoreSimulator.framework/Versions/A/Resources/bin/simctl spawn ...` + - `63142 1 00:17 /Library/Developer/PrivateFrameworks/CoreSimulator.framework/Versions/A/Resources/bin/simctl spawn ...` +- Matching process count was `2`. +- The first OSLog file later contained output from both launches, while the second file also contained output from the second launch, showing overlapping capture behavior. +**Conclusion:** Definitively confirmed. The leak accumulates across repeated launches. + +### Phase 6 - Short-timeout false positive and post-cleanup recovery +**Hypothesis:** The earlier `get_app_container` timeout may have been a measurement artifact, and the real question is whether the simulator recovers once the leaked processes are removed. +**Findings:** The earlier 5-second probe was too aggressive and not a reliable signal. After cleanup, rerunning the simulator metadata command with a realistic timeout completed successfully in `259 ms`. A real `simulator test` run then progressed through `build-for-testing`, entered `test-without-building`, executed tests, and returned control to the terminal. The failure mode changed from `Process spawn via launchd failed / Code 24 / blinking cursor` to ordinary test execution with the fixture project's expected intentional failures. +**Evidence:** +- Cleanup removed all matching orphan streams: count changed from `81` to `0`. +- Post-cleanup verification command: + ```sh + python3 - <<'PY' + import subprocess, time + cmd = ['xcrun','simctl','get_app_container','01DA97D9-3856-46C5-A75E-DDD48100B2DB','io.sentry.calculatorapp','app'] + start = time.time() + out = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + print('STATUS:completed') + print('RC:', out.returncode) + print('ELAPSED_MS:', int((time.time()-start)*1000)) + print('STDOUT:', out.stdout.strip()) + PY + ``` + returned `STATUS:completed`, `RC: 0`, and `ELAPSED_MS: 259` with a valid app-container path. +- Real test command: + ```sh + node build/cli.js simulator test --workspace-path /Volumes/Developer/XcodeBuildMCP/example_projects/iOS_Calculator/CalculatorApp.xcworkspace --scheme CalculatorApp --simulator-id 01DA97D9-3856-46C5-A75E-DDD48100B2DB --output raw + ``` + completed and returned exit code `1` only because the fixture suite contains intentional failures. Final output included: + - `IDETestOperationsObserverDebug: 16.342 elapsed -- Testing started completed.` + - `** TEST EXECUTE FAILED **` + - result bundle path under `/Users/cameroncooke/Library/Developer/XcodeBuildMCP/DerivedData/Logs/Test/...` +- Critically, the rerun did **not** reproduce `Process spawn via launchd failed`, `NSPOSIXErrorDomain Code 24`, or the terminal hang. +**Conclusion:** Confirmed. Clearing the leaked OSLog stream processes restored simulator health sufficiently for the original test path to run normally. The leak is the verified cause of the broken simulator state behind the original symptom. + +### Phase 7 - Eliminated hypotheses +**Hypothesis:** `test_sim` directly creates the leaked `simctl spawn ... log stream ...` children. +**Findings:** `test_sim` does not call `launchSimulatorAppWithLogging()` or `startOsLogStream()`. +**Evidence:** +- `src/mcp/tools/simulator/test_sim.ts` routes into `src/utils/test-common.ts` and xcodebuild orchestration, not simulator launch-with-logging. +- The actual spawn site is `src/utils/simulator-steps.ts:251-281`. +**Conclusion:** Eliminated. `test_sim` is not the direct source of the observed orphan stream processes. + +## Root Cause +There is a definitive product-side leak in XcodeBuildMCP’s simulator launch-with-logging path. + +`launchSimulatorAppWithLogging()` in `src/utils/simulator-steps.ts` starts OSLog capture by calling `startOsLogStream()` (`src/utils/simulator-steps.ts:205`). `startOsLogStream()` then launches: + +```ts +xcrun simctl spawn log stream --level=debug --predicate subsystem == "" +``` + +using `detached: true` (`src/utils/simulator-steps.ts:278`) and immediately `child.unref()` (`src/utils/simulator-steps.ts:281`). The process handle is not stored in any registry, and the normal stop path only runs `simctl terminate` on the app (`src/mcp/tools/simulator/stop_app_sim.ts:58`). Therefore these OSLog stream children are not tied to app lifecycle, not visible in tracked simulator log-session state, and not cleaned up by the normal stop tool. + +This was verified with real production code and real process-table inspection: +- the helper created the exact orphaned `simctl spawn ... log stream ...` process shape seen in the field, +- the process lived under PID 1 after the parent exited, +- `simulator stop` did not remove it, +- and repeated launches accumulated multiple survivors. + +This was verified end-to-end with real recovery evidence: after removing the leaked stream processes, simulator metadata calls succeeded again and the original `simulator test` command stopped failing in the launch path. In other words, `Code 24` was Xcode/CoreSimulator reporting the downstream effect — an unhealthy simulator launch environment caused by the leaked detached helpers — rather than a separate root cause inside the test suite itself. + +## Recommendations +1. Track OSLog stream children from `src/utils/simulator-steps.ts` in an explicit registry. + - File: `src/utils/simulator-steps.ts` + - Record PID/process handle, simulator id, bundle id, and log path. +2. Stop tracked OSLog stream children when the app is stopped. + - File: `src/mcp/tools/simulator/stop_app_sim.ts` + - Extend stop flow to terminate the matching OSLog stream(s), not just the app. +3. Integrate detached simulator OSLog stream cleanup into shutdown/session lifecycle. + - Files: `src/server/mcp-shutdown.ts`, `src/server/mcp-lifecycle.ts`, `src/utils/session-status.ts` + - Ensure status reflects these children and shutdown cleans them. +4. Add regression tests for lifecycle, not just launch success. + - Files: `src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts`, `src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts`, `src/utils/__tests__/simulator-steps-pid.test.ts` + - Assert launch creates tracked OSLog capture and stop/shutdown removes it. +5. Add a doctor/cleanup path for existing leaked simulator OSLog streams. + - Detect orphaned `simctl spawn log stream ...` helpers and terminate them before they poison later runs. + +## Preventive Measures +- Never start detached helper processes without a matching ownership and teardown model. +- Surface all long-lived simulator-side helpers in session status. +- Add an integration test that repeatedly launches/stops an app and asserts no monotonic growth in matching `simctl`/`xcodebuild` processes. +- Add a cleanup command or doctor check that detects and reports orphaned simulator OSLog streams. diff --git a/example_projects/iOS_Calculator/.mcp.json b/example_projects/iOS_Calculator/.mcp.json new file mode 100644 index 00000000..2725c131 --- /dev/null +++ b/example_projects/iOS_Calculator/.mcp.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "XcodeBuildMCP": { + "type": "stdio", + "command": "node", + "args": [ + "../../build/cli.js", + "mcp" + ], + "env": {} + } + } +} \ No newline at end of file diff --git a/src/mcp/resources/__tests__/session-status.test.ts b/src/mcp/resources/__tests__/session-status.test.ts index fb483531..848bfb88 100644 --- a/src/mcp/resources/__tests__/session-status.test.ts +++ b/src/mcp/resources/__tests__/session-status.test.ts @@ -1,16 +1,45 @@ +import { mkdtempSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import * as path from 'node:path'; +import { EventEmitter } from 'node:events'; +import type { ChildProcess } from 'node:child_process'; import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { clearDaemonActivityRegistry } from '../../../daemon/activity-registry.ts'; import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts'; import { activeLogSessions } from '../../../utils/log_capture.ts'; import { activeDeviceLogSessions } from '../../../utils/log-capture/device-log-sessions.ts'; +import { + clearAllSimulatorLaunchOsLogSessionsForTests, + registerSimulatorLaunchOsLogSession, + setSimulatorLaunchOsLogRegistryDirOverrideForTests, +} from '../../../utils/log-capture/simulator-launch-oslog-sessions.ts'; +import { setSimulatorLaunchOsLogRecordActiveOverrideForTests } from '../../../utils/log-capture/simulator-launch-oslog-registry.ts'; +import { setRuntimeInstanceForTests } from '../../../utils/runtime-instance.ts'; import { clearAllProcesses } from '../../tools/swift-package/active-processes.ts'; import { sessionStatusResourceLogic } from '../session-status.ts'; +let registryDir: string; + +function createTrackedChild(pid = 777): ChildProcess { + const emitter = new EventEmitter(); + const child = emitter as ChildProcess; + Object.defineProperty(child, 'pid', { value: pid, configurable: true }); + Object.defineProperty(child, 'exitCode', { value: null, writable: true, configurable: true }); + child.kill = (() => true) as ChildProcess['kill']; + return child; +} + describe('session-status resource', () => { beforeEach(async () => { + registryDir = mkdtempSync(path.join(tmpdir(), 'xcodebuildmcp-session-status-')); + setSimulatorLaunchOsLogRegistryDirOverrideForTests(registryDir); + setRuntimeInstanceForTests({ instanceId: 'session-status-test', pid: process.pid }); + setSimulatorLaunchOsLogRecordActiveOverrideForTests(async () => true); activeLogSessions.clear(); activeDeviceLogSessions.clear(); clearAllProcesses(); + await clearAllSimulatorLaunchOsLogSessionsForTests(); clearDaemonActivityRegistry(); await getDefaultDebuggerManager().disposeAll(); }); @@ -19,8 +48,13 @@ describe('session-status resource', () => { activeLogSessions.clear(); activeDeviceLogSessions.clear(); clearAllProcesses(); + await clearAllSimulatorLaunchOsLogSessionsForTests(); clearDaemonActivityRegistry(); await getDefaultDebuggerManager().disposeAll(); + setSimulatorLaunchOsLogRecordActiveOverrideForTests(null); + setRuntimeInstanceForTests(null); + setSimulatorLaunchOsLogRegistryDirOverrideForTests(null); + await rm(registryDir, { recursive: true, force: true }); }); describe('Handler Functionality', () => { @@ -31,6 +65,7 @@ describe('session-status resource', () => { const parsed = JSON.parse(result.contents[0].text); expect(parsed.logging.simulator.activeSessionIds).toEqual([]); + expect(parsed.logging.simulator.activeLaunchOsLogSessions).toEqual([]); expect(parsed.logging.device.activeSessionIds).toEqual([]); expect(parsed.debug.currentSessionId).toBe(null); expect(parsed.debug.sessionIds).toEqual([]); @@ -43,5 +78,27 @@ describe('session-status resource', () => { expect(parsed.process.rssBytes).toBeTypeOf('number'); expect(parsed.process.heapUsedBytes).toBeTypeOf('number'); }); + + it('should include tracked launch OSLog sessions', async () => { + await registerSimulatorLaunchOsLogSession({ + process: createTrackedChild(888), + simulatorUuid: 'sim-1', + bundleId: 'io.sentry.app', + logFilePath: '/tmp/app.log', + }); + + const result = await sessionStatusResourceLogic(); + const parsed = JSON.parse(result.contents[0].text); + + expect(parsed.logging.simulator.activeLaunchOsLogSessions).toEqual([ + expect.objectContaining({ + simulatorUuid: 'sim-1', + bundleId: 'io.sentry.app', + pid: 888, + logFilePath: '/tmp/app.log', + ownedByCurrentProcess: true, + }), + ]); + }); }); }); diff --git a/src/mcp/resources/session-status.ts b/src/mcp/resources/session-status.ts index ceaf9b2d..b9a694ac 100644 --- a/src/mcp/resources/session-status.ts +++ b/src/mcp/resources/session-status.ts @@ -5,12 +5,13 @@ */ import { log } from '../../utils/logging/index.ts'; +import { toErrorMessage } from '../../utils/errors.ts'; import { getSessionRuntimeStatusSnapshot } from '../../utils/session-status.ts'; export async function sessionStatusResourceLogic(): Promise<{ contents: Array<{ text: string }> }> { try { log('info', 'Processing session status resource request'); - const status = getSessionRuntimeStatusSnapshot(); + const status = await getSessionRuntimeStatusSnapshot(); return { contents: [ @@ -20,7 +21,7 @@ export async function sessionStatusResourceLogic(): Promise<{ contents: Array<{ ], }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = toErrorMessage(error); log('error', `Error in session status resource handler: ${errorMessage}`); return { diff --git a/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts b/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts index d2ce8911..dc201a8e 100644 --- a/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as z from 'zod'; import { createMockExecutor, @@ -6,12 +6,84 @@ import { } from '../../../../test-utils/mock-executors.ts'; import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; +import { + clearAllSimulatorLaunchOsLogSessionsForTests, + registerSimulatorLaunchOsLogSession, + setSimulatorLaunchOsLogRegistryDirOverrideForTests, +} from '../../../../utils/log-capture/simulator-launch-oslog-sessions.ts'; +import { setSimulatorLaunchOsLogRecordActiveOverrideForTests } from '../../../../utils/log-capture/simulator-launch-oslog-registry.ts'; import { schema, handler, stop_app_simLogic } from '../stop_app_sim.ts'; import { allText, runLogic } from '../../../../test-utils/test-helpers.ts'; +import { EventEmitter } from 'node:events'; +import { mkdtempSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import * as path from 'node:path'; +import type { ChildProcess } from 'node:child_process'; +import { setRuntimeInstanceForTests } from '../../../../utils/runtime-instance.ts'; + +function createTrackedChild(options?: { + pid?: number; + killImplementation?: (signal?: NodeJS.Signals | number) => boolean; +}): ChildProcess { + const emitter = new EventEmitter(); + const child = emitter as ChildProcess; + let exitCode: number | null = null; + const pid = options?.pid ?? nextPid++; + + Object.defineProperty(child, 'pid', { value: pid, configurable: true }); + Object.defineProperty(child, 'exitCode', { + configurable: true, + get: () => exitCode, + set: (value: number | null) => { + exitCode = value; + }, + }); + + trackedChildren.set(pid, child); + + child.kill = vi.fn((signal?: NodeJS.Signals | number) => { + if (options?.killImplementation) { + return options.killImplementation(signal); + } + exitCode = 0; + queueMicrotask(() => { + emitter.emit('exit', 0, signal); + emitter.emit('close', 0, signal); + }); + return true; + }) as ChildProcess['kill']; + + return child; +} + +let registryDir: string; +let nextPid = 1234; +const trackedChildren = new Map(); describe('stop_app_sim tool', () => { - beforeEach(() => { + beforeEach(async () => { + nextPid = 1234; + trackedChildren.clear(); + registryDir = mkdtempSync(path.join(tmpdir(), 'xcodebuildmcp-stop-app-sim-')); + setSimulatorLaunchOsLogRegistryDirOverrideForTests(registryDir); + setRuntimeInstanceForTests({ instanceId: 'stop-app-sim-test', pid: process.pid }); + setSimulatorLaunchOsLogRecordActiveOverrideForTests(async (record) => { + const child = trackedChildren.get(record.helperPid); + return child ? child.exitCode == null : true; + }); + sessionStore.clear(); + await clearAllSimulatorLaunchOsLogSessionsForTests(); + }); + + afterEach(async () => { sessionStore.clear(); + await clearAllSimulatorLaunchOsLogSessionsForTests(); + setSimulatorLaunchOsLogRecordActiveOverrideForTests(null); + setRuntimeInstanceForTests(null); + setSimulatorLaunchOsLogRegistryDirOverrideForTests(null); + trackedChildren.clear(); + await rm(registryDir, { recursive: true, force: true }); }); describe('Export Field Validation (Literal)', () => { @@ -89,6 +161,32 @@ describe('stop_app_sim tool', () => { expect(text).toContain('test-uuid'); }); + it('stops tracked OSLog sessions alongside the app', async () => { + const mockExecutor = createMockExecutor({ success: true, output: '' }); + const child = createTrackedChild(); + await registerSimulatorLaunchOsLogSession({ + process: child, + simulatorUuid: 'test-uuid', + bundleId: 'io.sentry.App', + logFilePath: '/tmp/app.log', + }); + + const result = await runLogic(() => + stop_app_simLogic( + { + simulatorId: 'test-uuid', + bundleId: 'io.sentry.App', + }, + mockExecutor, + ), + ); + + const text = allText(result); + expect(child.kill).toHaveBeenCalledWith('SIGTERM'); + expect(text).toContain('stopped successfully'); + expect(text).not.toContain('Tracked OSLog sessions cleaned up'); + }); + it('should display friendly name when simulatorName is provided alongside resolved simulatorId', async () => { const mockExecutor = createMockExecutor({ success: true, output: '' }); @@ -117,6 +215,13 @@ describe('stop_app_sim tool', () => { error: 'Simulator not found', }); + await registerSimulatorLaunchOsLogSession({ + process: createTrackedChild(), + simulatorUuid: 'invalid-uuid', + bundleId: 'io.sentry.App', + logFilePath: '/tmp/app.log', + }); + const result = await runLogic(() => stop_app_simLogic( { @@ -133,6 +238,35 @@ describe('stop_app_sim tool', () => { expect(result.isError).toBe(true); }); + it('should report cleanup failures even when terminate succeeds', async () => { + const mockExecutor = createMockExecutor({ success: true, output: '' }); + await registerSimulatorLaunchOsLogSession({ + process: createTrackedChild({ + killImplementation: () => { + throw new Error('cleanup boom'); + }, + }), + simulatorUuid: 'test-uuid', + bundleId: 'io.sentry.App', + logFilePath: '/tmp/app.log', + }); + + const result = await runLogic(() => + stop_app_simLogic( + { + simulatorId: 'test-uuid', + bundleId: 'io.sentry.App', + }, + mockExecutor, + ), + ); + + const text = allText(result); + expect(text).toContain('OSLog cleanup failed'); + expect(text).toContain('cleanup boom'); + expect(result.isError).toBe(true); + }); + it('should handle unexpected exceptions', async () => { const throwingExecutor = async () => { throw new Error('Unexpected error'); diff --git a/src/mcp/tools/simulator/stop_app_sim.ts b/src/mcp/tools/simulator/stop_app_sim.ts index ff8efcb0..3e616b07 100644 --- a/src/mcp/tools/simulator/stop_app_sim.ts +++ b/src/mcp/tools/simulator/stop_app_sim.ts @@ -9,6 +9,7 @@ import { } from '../../../utils/typed-tool-factory.ts'; import { withErrorHandling } from '../../../utils/tool-error-handling.ts'; import { header, statusLine } from '../../../utils/tool-event-builders.ts'; +import { stopSimulatorLaunchOsLogSessionsForApp } from '../../../utils/log-capture/index.ts'; const baseSchemaObject = z.object({ simulatorId: z @@ -57,10 +58,25 @@ export async function stop_app_simLogic( async () => { const command = ['xcrun', 'simctl', 'terminate', simulatorId, params.bundleId]; const result = await executor(command, 'Stop App in Simulator', false); + const cleanupResult = await stopSimulatorLaunchOsLogSessionsForApp( + simulatorId, + params.bundleId, + 1000, + ); + + if (!result.success || cleanupResult.errorCount > 0) { + const details: string[] = []; + if (!result.success) { + details.push(result.error ?? 'Unknown simulator terminate error'); + } + if (cleanupResult.errorCount > 0) { + details.push(`OSLog cleanup failed: ${cleanupResult.errors.join('; ')}`); + } - if (!result.success) { ctx.emit(headerEvent); - ctx.emit(statusLine('error', `Stop app in simulator operation failed: ${result.error}`)); + ctx.emit( + statusLine('error', `Stop app in simulator operation failed: ${details.join(' | ')}`), + ); return; } diff --git a/src/rendering/__tests__/text-render-parity.test.ts b/src/rendering/__tests__/text-render-parity.test.ts new file mode 100644 index 00000000..38274a6f --- /dev/null +++ b/src/rendering/__tests__/text-render-parity.test.ts @@ -0,0 +1,171 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { PipelineEvent } from '../../types/pipeline-events.ts'; +import { renderEvents } from '../render.ts'; +import { createCliTextRenderer } from '../../utils/renderers/cli-text-renderer.ts'; + +function captureCliText(events: readonly PipelineEvent[]): string { + const stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); + const renderer = createCliTextRenderer({ interactive: false }); + + for (const event of events) { + renderer.onEvent(event); + } + renderer.finalize(); + + return stdoutWrite.mock.calls.flat().join(''); +} + +describe('text render parity', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('matches non-interactive cli text for discovery and summary output', () => { + const events: PipelineEvent[] = [ + { + type: 'header', + timestamp: '2026-04-10T22:50:00.000Z', + operation: 'Test', + params: [ + { label: 'Scheme', value: 'CalculatorApp' }, + { label: 'Configuration', value: 'Debug' }, + { label: 'Platform', value: 'iOS Simulator' }, + ], + }, + { + type: 'test-discovery', + timestamp: '2026-04-10T22:50:01.000Z', + operation: 'TEST', + total: 1, + tests: ['CalculatorAppTests/CalculatorAppTests/testAddition'], + truncated: false, + }, + { + type: 'summary', + timestamp: '2026-04-10T22:50:02.000Z', + operation: 'TEST', + status: 'SUCCEEDED', + totalTests: 1, + passedTests: 1, + skippedTests: 0, + durationMs: 1500, + }, + ]; + + expect(renderEvents(events, 'text')).toBe(captureCliText(events)); + }); + + it('matches non-interactive cli text for failure diagnostics and summary spacing', () => { + const events: PipelineEvent[] = [ + { + type: 'header', + timestamp: '2026-04-10T22:50:00.000Z', + operation: 'Test', + params: [ + { label: 'Scheme', value: 'MCPTest' }, + { label: 'Configuration', value: 'Debug' }, + { label: 'Platform', value: 'macOS' }, + ], + }, + { + type: 'test-discovery', + timestamp: '2026-04-10T22:50:01.000Z', + operation: 'TEST', + total: 2, + tests: [ + 'MCPTestTests/MCPTestTests/appNameIsCorrect', + 'MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect', + ], + truncated: false, + }, + { + type: 'test-failure', + timestamp: '2026-04-10T22:50:02.000Z', + operation: 'TEST', + suite: 'MCPTestsXCTests', + test: 'testDeliberateFailure()', + message: 'XCTAssertTrue failed', + location: 'MCPTestsXCTests.swift:11', + }, + { + type: 'summary', + timestamp: '2026-04-10T22:50:03.000Z', + operation: 'TEST', + status: 'FAILED', + totalTests: 2, + passedTests: 1, + failedTests: 1, + skippedTests: 0, + durationMs: 2200, + }, + ]; + + expect(renderEvents(events, 'text')).toBe(captureCliText(events)); + }); + + it('renders next steps in MCP tool-call syntax for MCP runtime text transcripts', () => { + const events: PipelineEvent[] = [ + { + type: 'summary', + timestamp: '2026-04-10T22:50:05.000Z', + operation: 'BUILD', + status: 'SUCCEEDED', + durationMs: 7100, + }, + { + type: 'next-steps', + timestamp: '2026-04-10T22:50:06.000Z', + runtime: 'mcp', + steps: [ + { + label: 'Get built macOS app path', + tool: 'get_mac_app_path', + cliTool: 'get-app-path', + workflow: 'macos', + params: { + scheme: 'MCPTest', + }, + }, + ], + }, + ]; + + const output = renderEvents(events, 'text'); + expect(output).toBe(captureCliText(events)); + expect(output).toContain('get_mac_app_path({ scheme: "MCPTest" })'); + expect(output).not.toContain('xcodebuildmcp macos get-app-path'); + }); + + it('renders next steps in CLI syntax for CLI runtime text transcripts', () => { + const events: PipelineEvent[] = [ + { + type: 'summary', + timestamp: '2026-04-10T22:50:05.000Z', + operation: 'BUILD', + status: 'SUCCEEDED', + durationMs: 7100, + }, + { + type: 'next-steps', + timestamp: '2026-04-10T22:50:06.000Z', + runtime: 'cli', + steps: [ + { + label: 'Get built macOS app path', + tool: 'get_mac_app_path', + cliTool: 'get-app-path', + workflow: 'macos', + params: { + scheme: 'MCPTest', + }, + }, + ], + }, + ]; + + const output = renderEvents(events, 'text'); + expect(output).toBe(captureCliText(events)); + expect(output).toContain('xcodebuildmcp macos get-app-path --scheme "MCPTest"'); + expect(output).not.toContain('get_mac_app_path({'); + }); +}); diff --git a/src/rendering/render.ts b/src/rendering/render.ts index 67dd448b..669bca7b 100644 --- a/src/rendering/render.ts +++ b/src/rendering/render.ts @@ -1,27 +1,9 @@ -import type { - CompilerErrorEvent, - CompilerWarningEvent, - PipelineEvent, - TestFailureEvent, -} from '../types/pipeline-events.ts'; +import type { PipelineEvent } from '../types/pipeline-events.ts'; import { sessionStore } from '../utils/session-store.ts'; -import { deriveDiagnosticBaseDir } from '../utils/renderers/index.ts'; import { - formatBuildStageEvent, - formatDetailTreeEvent, - formatFileRefEvent, - formatGroupedCompilerErrors, - formatGroupedTestFailures, - formatGroupedWarnings, - formatHeaderEvent, - formatNextStepsEvent, - formatSectionEvent, - formatStatusLineEvent, - formatSummaryEvent, - formatTableEvent, - formatTestDiscoveryEvent, -} from '../utils/renderers/event-formatting.ts'; -import { createCliTextRenderer } from '../utils/renderers/cli-text-renderer.ts'; + createCliTextRenderer, + renderCliTextTranscript, +} from '../utils/renderers/cli-text-renderer.ts'; import type { RenderSession, RenderStrategy, ImageAttachment } from './types.ts'; function isErrorEvent(event: PipelineEvent): boolean { @@ -31,127 +13,21 @@ function isErrorEvent(event: PipelineEvent): boolean { ); } -function createTextRenderSession(): RenderSession { +interface RenderSessionHooks { + onEmit?: (event: PipelineEvent) => void; + finalize: (events: readonly PipelineEvent[]) => string; +} + +function createBaseRenderSession(hooks: RenderSessionHooks): RenderSession { const events: PipelineEvent[] = []; const attachments: ImageAttachment[] = []; - const contentParts: string[] = []; - const suppressWarnings = sessionStore.get('suppressWarnings'); - const groupedCompilerErrors: CompilerErrorEvent[] = []; - const groupedWarnings: CompilerWarningEvent[] = []; - const groupedTestFailures: TestFailureEvent[] = []; - - let diagnosticBaseDir: string | null = null; let hasError = false; - const pushText = (text: string): void => { - contentParts.push(text); - }; - - const pushSection = (text: string): void => { - pushText(`\n${text}`); - }; - return { emit(event: PipelineEvent): void { events.push(event); if (isErrorEvent(event)) hasError = true; - - switch (event.type) { - case 'header': { - diagnosticBaseDir = deriveDiagnosticBaseDir(event); - pushSection(formatHeaderEvent(event)); - break; - } - - case 'build-stage': { - pushSection(formatBuildStageEvent(event)); - break; - } - - case 'status-line': { - pushSection(formatStatusLineEvent(event)); - break; - } - - case 'section': { - pushText(`\n\n${formatSectionEvent(event)}`); - break; - } - - case 'detail-tree': { - pushSection(formatDetailTreeEvent(event)); - break; - } - - case 'table': { - pushSection(formatTableEvent(event)); - break; - } - - case 'file-ref': { - pushSection(formatFileRefEvent(event)); - break; - } - - case 'compiler-warning': { - if (!suppressWarnings) { - groupedWarnings.push(event); - } - break; - } - - case 'compiler-error': { - groupedCompilerErrors.push(event); - break; - } - - case 'test-discovery': { - pushText(formatTestDiscoveryEvent(event)); - break; - } - - case 'test-progress': { - break; - } - - case 'test-failure': { - groupedTestFailures.push(event); - break; - } - - case 'summary': { - const diagOpts = { baseDir: diagnosticBaseDir ?? undefined }; - const diagnosticSections: string[] = []; - - if (groupedTestFailures.length > 0) { - diagnosticSections.push(formatGroupedTestFailures(groupedTestFailures, diagOpts)); - groupedTestFailures.length = 0; - } - - if (groupedWarnings.length > 0) { - diagnosticSections.push(formatGroupedWarnings(groupedWarnings, diagOpts)); - groupedWarnings.length = 0; - } - - if (event.status === 'FAILED' && groupedCompilerErrors.length > 0) { - diagnosticSections.push(formatGroupedCompilerErrors(groupedCompilerErrors, diagOpts)); - groupedCompilerErrors.length = 0; - } - - if (diagnosticSections.length > 0) { - pushSection(diagnosticSections.join('\n\n')); - } - - pushSection(formatSummaryEvent(event)); - break; - } - - case 'next-steps': { - const effectiveRuntime = event.runtime === 'cli' ? 'cli' : 'mcp'; - pushText(`\n\n${formatNextStepsEvent(event, effectiveRuntime)}`); - break; - } - } + hooks.onEmit?.(event); }, attach(image: ImageAttachment): void { @@ -171,80 +47,39 @@ function createTextRenderSession(): RenderSession { }, finalize(): string { - diagnosticBaseDir = null; - return contentParts.join(''); + return hooks.finalize(events); }, }; } -function createCliTextRenderSession(options: { interactive: boolean }): RenderSession { - const events: PipelineEvent[] = []; - const attachments: ImageAttachment[] = []; - const renderer = createCliTextRenderer(options); - let hasError = false; - - return { - emit(event: PipelineEvent): void { - events.push(event); - if (isErrorEvent(event)) hasError = true; - renderer.onEvent(event); - }, - - attach(image: ImageAttachment): void { - attachments.push(image); - }, - - getEvents(): readonly PipelineEvent[] { - return events; - }, +function createTextRenderSession(): RenderSession { + const suppressWarnings = sessionStore.get('suppressWarnings'); - getAttachments(): readonly ImageAttachment[] { - return attachments; - }, + return createBaseRenderSession({ + finalize: (events) => + renderCliTextTranscript(events, { + suppressWarnings: suppressWarnings ?? false, + }), + }); +} - isError(): boolean { - return hasError; - }, +function createCliTextRenderSession(options: { interactive: boolean }): RenderSession { + const renderer = createCliTextRenderer(options); - finalize(): string { + return createBaseRenderSession({ + onEmit: (event) => renderer.onEvent(event), + finalize: () => { renderer.finalize(); return ''; }, - }; + }); } function createCliJsonRenderSession(): RenderSession { - const events: PipelineEvent[] = []; - const attachments: ImageAttachment[] = []; - let hasError = false; - - return { - emit(event: PipelineEvent): void { - events.push(event); - if (isErrorEvent(event)) hasError = true; - process.stdout.write(JSON.stringify(event) + '\n'); - }, - - attach(image: ImageAttachment): void { - attachments.push(image); - }, - - getEvents(): readonly PipelineEvent[] { - return events; - }, - - getAttachments(): readonly ImageAttachment[] { - return attachments; - }, - - isError(): boolean { - return hasError; - }, - - finalize(): string { - return ''; - }, - }; + return createBaseRenderSession({ + onEmit: (event) => process.stdout.write(JSON.stringify(event) + '\n'), + finalize: () => '', + }); } export interface RenderSessionOptions { diff --git a/src/server/__tests__/mcp-lifecycle.test.ts b/src/server/__tests__/mcp-lifecycle.test.ts index 832b931e..97654afd 100644 --- a/src/server/__tests__/mcp-lifecycle.test.ts +++ b/src/server/__tests__/mcp-lifecycle.test.ts @@ -1,5 +1,9 @@ import { EventEmitter } from 'node:events'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { mkdtempSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import * as path from 'node:path'; +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; import { createMockExecutor } from '../../test-utils/mock-executors.ts'; import { buildMcpLifecycleSnapshot, @@ -8,6 +12,17 @@ import { isTransportDisconnectReason, } from '../mcp-lifecycle.ts'; import * as shutdownState from '../../utils/shutdown-state.ts'; +import { + clearAllSimulatorLaunchOsLogSessionsForTests, + registerSimulatorLaunchOsLogSession, + setSimulatorLaunchOsLogRegistryDirOverrideForTests, +} from '../../utils/log-capture/simulator-launch-oslog-sessions.ts'; +import { setSimulatorLaunchOsLogRecordActiveOverrideForTests } from '../../utils/log-capture/simulator-launch-oslog-registry.ts'; +import { setRuntimeInstanceForTests } from '../../utils/runtime-instance.ts'; +import { EventEmitter as NodeEventEmitter } from 'node:events'; +import type { ChildProcess } from 'node:child_process'; + +let registryDir: string; class TestStdin extends EventEmitter { override once(event: string, listener: (...args: unknown[]) => void): this { @@ -33,11 +48,33 @@ class TestProcess extends EventEmitter { } } +function createTrackedChild(pid = 777): ChildProcess { + const emitter = new NodeEventEmitter(); + const child = emitter as ChildProcess; + Object.defineProperty(child, 'pid', { value: pid, configurable: true }); + Object.defineProperty(child, 'exitCode', { value: null, writable: true, configurable: true }); + child.kill = vi.fn(() => true) as ChildProcess['kill']; + return child; +} + describe('mcp lifecycle coordinator', () => { - beforeEach(() => { + beforeEach(async () => { + registryDir = mkdtempSync(path.join(tmpdir(), 'xcodebuildmcp-mcp-lifecycle-')); + setSimulatorLaunchOsLogRegistryDirOverrideForTests(registryDir); + setRuntimeInstanceForTests({ instanceId: 'mcp-lifecycle-test', pid: process.pid }); + setSimulatorLaunchOsLogRecordActiveOverrideForTests(async () => true); + await clearAllSimulatorLaunchOsLogSessionsForTests(); vi.restoreAllMocks(); }); + afterEach(async () => { + await clearAllSimulatorLaunchOsLogSessionsForTests(); + setSimulatorLaunchOsLogRecordActiveOverrideForTests(null); + setRuntimeInstanceForTests(null); + setSimulatorLaunchOsLogRegistryDirOverrideForTests(null); + await rm(registryDir, { recursive: true, force: true }); + }); + it('deduplicates shutdown requests from stdin end and close', async () => { const processRef = new TestProcess(); const onShutdown = vi.fn().mockResolvedValue(undefined); @@ -139,6 +176,22 @@ describe('mcp lifecycle coordinator', () => { }); describe('mcp lifecycle snapshot', () => { + beforeEach(async () => { + registryDir = mkdtempSync(path.join(tmpdir(), 'xcodebuildmcp-mcp-lifecycle-')); + setSimulatorLaunchOsLogRegistryDirOverrideForTests(registryDir); + setRuntimeInstanceForTests({ instanceId: 'mcp-lifecycle-test', pid: process.pid }); + setSimulatorLaunchOsLogRecordActiveOverrideForTests(async () => true); + await clearAllSimulatorLaunchOsLogSessionsForTests(); + }); + + afterEach(async () => { + await clearAllSimulatorLaunchOsLogSessionsForTests(); + setSimulatorLaunchOsLogRecordActiveOverrideForTests(null); + setRuntimeInstanceForTests(null); + setSimulatorLaunchOsLogRegistryDirOverrideForTests(null); + await rm(registryDir, { recursive: true, force: true }); + }); + it('classifies peer-count and memory anomalies', () => { expect( classifyMcpLifecycleAnomalies({ @@ -177,9 +230,30 @@ describe('mcp lifecycle snapshot', () => { expect(snapshot.matchingMcpPeerSummary).toEqual([{ pid: 999, ageSeconds: 180, rssKb: 1024 }]); expect(snapshot.ppid).toBe(process.ppid); expect(snapshot.orphaned).toBe(process.ppid === 1); + expect(snapshot.simulatorLaunchOsLogSessionCount).toBe(0); + expect(snapshot.ownedSimulatorLaunchOsLogSessionCount).toBe(0); expect(snapshot.anomalies).toEqual(['peer-age-high']); }); + it('reports tracked simulator launch OSLog session counts', async () => { + await registerSimulatorLaunchOsLogSession({ + process: createTrackedChild(888), + simulatorUuid: 'sim-1', + bundleId: 'io.sentry.app', + logFilePath: '/tmp/app.log', + }); + + const snapshot = await buildMcpLifecycleSnapshot({ + phase: 'running', + shutdownReason: null, + startedAtMs: Date.now() - 1000, + commandExecutor: createMockExecutor({ output: '' }), + }); + + expect(snapshot.simulatorLaunchOsLogSessionCount).toBe(1); + expect(snapshot.ownedSimulatorLaunchOsLogSessionCount).toBe(1); + }); + it('classifies transport disconnect reasons', () => { expect(isTransportDisconnectReason('stdin-end')).toBe(true); expect(isTransportDisconnectReason('stdin-close')).toBe(true); diff --git a/src/server/__tests__/mcp-shutdown.test.ts b/src/server/__tests__/mcp-shutdown.test.ts index 24146acf..5236b604 100644 --- a/src/server/__tests__/mcp-shutdown.test.ts +++ b/src/server/__tests__/mcp-shutdown.test.ts @@ -10,6 +10,11 @@ const mocks = vi.hoisted(() => ({ errorCount: 0, errors: [], })), + stopOwnedSimulatorLaunchOsLogSessions: vi.fn(async () => ({ + stoppedSessionCount: 0, + errorCount: 0, + errors: [], + })), stopAllVideoCaptureSessions: vi.fn(async () => ({ stoppedSessionCount: 0, errorCount: 0, @@ -40,6 +45,9 @@ vi.mock('../../utils/log_capture.ts', () => ({ vi.mock('../../utils/log-capture/device-log-sessions.ts', () => ({ stopAllDeviceLogCaptures: mocks.stopAllDeviceLogCaptures, })); +vi.mock('../../utils/log-capture/simulator-launch-oslog-sessions.ts', () => ({ + stopOwnedSimulatorLaunchOsLogSessions: mocks.stopOwnedSimulatorLaunchOsLogSessions, +})); vi.mock('../../utils/video_capture.ts', () => ({ stopAllVideoCaptureSessions: mocks.stopAllVideoCaptureSessions, })); @@ -83,6 +91,8 @@ describe('runMcpShutdown', () => { activeOperationByCategory: {}, debuggerSessionCount: 0, simulatorLogSessionCount: 0, + simulatorLaunchOsLogSessionCount: 0, + ownedSimulatorLaunchOsLogSessionCount: 0, deviceLogSessionCount: 0, videoCaptureSessionCount: 0, swiftPackageProcessCount: 0, @@ -102,6 +112,7 @@ describe('runMcpShutdown', () => { expect(mocks.disposeAll).toHaveBeenCalledTimes(1); expect(mocks.stopAllLogCaptures).toHaveBeenCalledTimes(1); expect(mocks.stopAllDeviceLogCaptures).toHaveBeenCalledTimes(1); + expect(mocks.stopOwnedSimulatorLaunchOsLogSessions).toHaveBeenCalledTimes(1); expect(mocks.stopAllVideoCaptureSessions).toHaveBeenCalledTimes(1); expect(mocks.stopAllTrackedProcesses).toHaveBeenCalledTimes(1); }); @@ -129,6 +140,8 @@ describe('runMcpShutdown', () => { activeOperationByCategory: {}, debuggerSessionCount: 0, simulatorLogSessionCount: 1, + simulatorLaunchOsLogSessionCount: 0, + ownedSimulatorLaunchOsLogSessionCount: 0, deviceLogSessionCount: 0, videoCaptureSessionCount: 0, swiftPackageProcessCount: 0, @@ -166,6 +179,8 @@ describe('runMcpShutdown', () => { activeOperationByCategory: {}, debuggerSessionCount: 0, simulatorLogSessionCount: 2, + simulatorLaunchOsLogSessionCount: 0, + ownedSimulatorLaunchOsLogSessionCount: 0, deviceLogSessionCount: 0, videoCaptureSessionCount: 0, swiftPackageProcessCount: 0, @@ -202,6 +217,8 @@ describe('runMcpShutdown', () => { activeOperationByCategory: {}, debuggerSessionCount: 1, simulatorLogSessionCount: 0, + simulatorLaunchOsLogSessionCount: 0, + ownedSimulatorLaunchOsLogSessionCount: 0, deviceLogSessionCount: 0, videoCaptureSessionCount: 0, swiftPackageProcessCount: 0, diff --git a/src/server/mcp-lifecycle.ts b/src/server/mcp-lifecycle.ts index 52bf156c..d6af79d4 100644 --- a/src/server/mcp-lifecycle.ts +++ b/src/server/mcp-lifecycle.ts @@ -3,6 +3,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { getDefaultDebuggerManager } from '../utils/debugger/index.ts'; import { activeLogSessions } from '../utils/log_capture.ts'; import { activeDeviceLogSessions } from '../utils/log-capture/device-log-sessions.ts'; +import { listActiveSimulatorLaunchOsLogSessions } from '../utils/log-capture/simulator-launch-oslog-sessions.ts'; import { activeProcesses } from '../mcp/tools/swift-package/active-processes.ts'; import { getDaemonActivitySnapshot } from '../daemon/activity-registry.ts'; import { listActiveVideoCaptureSessionIds } from '../utils/video_capture.ts'; @@ -61,6 +62,8 @@ export interface McpLifecycleSnapshot { activeOperationByCategory: Record; debuggerSessionCount: number; simulatorLogSessionCount: number; + simulatorLaunchOsLogSessionCount: number; + ownedSimulatorLaunchOsLogSessionCount: number; deviceLogSessionCount: number; videoCaptureSessionCount: number; swiftPackageProcessCount: number; @@ -157,23 +160,23 @@ export function classifyMcpLifecycleAnomalies( 'uptimeMs' | 'rssBytes' | 'matchingMcpProcessCount' | 'matchingMcpPeerSummary' >, ): McpLifecycleAnomaly[] { - const anomalies = new Set(); + const anomalies: McpLifecycleAnomaly[] = []; const peerCount = Math.max(0, (snapshot.matchingMcpProcessCount ?? 0) - 1); if (peerCount >= PEER_COUNT_HIGH_THRESHOLD) { - anomalies.add('peer-count-high'); + anomalies.push('peer-count-high'); } if (snapshot.matchingMcpPeerSummary.some((peer) => peer.ageSeconds >= PEER_AGE_HIGH_SECONDS)) { - anomalies.add('peer-age-high'); + anomalies.push('peer-age-high'); } if (snapshot.rssBytes >= HIGH_RSS_BYTES) { - anomalies.add('high-rss'); + anomalies.push('high-rss'); } if (snapshot.uptimeMs >= LONG_LIVED_UPTIME_MS && snapshot.rssBytes >= LONG_LIVED_HIGH_RSS_BYTES) { - anomalies.add('long-lived-high-rss'); + anomalies.push('long-lived-high-rss'); } - return [...anomalies].sort(); + return anomalies.sort(); } function isLikelyMcpProcessCommand(command: string): boolean { @@ -247,12 +250,7 @@ async function sampleMcpPeerProcesses( const peers = matched .filter((entry) => entry.pid !== currentPid) .map(({ pid, ageSeconds, rssKb }) => ({ pid, ageSeconds, rssKb })) - .sort((left, right) => { - if (right.ageSeconds !== left.ageSeconds) { - return right.ageSeconds - left.ageSeconds; - } - return right.rssKb - left.rssKb; - }) + .sort((left, right) => right.ageSeconds - left.ageSeconds || right.rssKb - left.rssKb) .slice(0, 5); return { @@ -287,6 +285,7 @@ export async function buildMcpLifecycleSnapshot(options: { options.commandExecutor ?? getDefaultCommandExecutor(), process.pid, ); + const simulatorLaunchOsLogSessions = await listActiveSimulatorLaunchOsLogSessions(); const snapshotWithoutAnomalies = { pid: process.pid, @@ -303,6 +302,10 @@ export async function buildMcpLifecycleSnapshot(options: { activeOperationByCategory: activitySnapshot.byCategory, debuggerSessionCount: getDefaultDebuggerManager().listSessions().length, simulatorLogSessionCount: activeLogSessions.size, + simulatorLaunchOsLogSessionCount: simulatorLaunchOsLogSessions.length, + ownedSimulatorLaunchOsLogSessionCount: simulatorLaunchOsLogSessions.filter( + (session) => session.ownedByCurrentProcess, + ).length, deviceLogSessionCount: activeDeviceLogSessions.size, videoCaptureSessionCount: listActiveVideoCaptureSessionIds().length, swiftPackageProcessCount: activeProcesses.size, diff --git a/src/server/mcp-shutdown.ts b/src/server/mcp-shutdown.ts index 9c923610..de876afb 100644 --- a/src/server/mcp-shutdown.ts +++ b/src/server/mcp-shutdown.ts @@ -4,6 +4,7 @@ import { stopXcodeStateWatcher } from '../utils/xcode-state-watcher.ts'; import { shutdownXcodeToolsBridge } from '../integrations/xcode-tools-bridge/index.ts'; import { stopAllLogCaptures } from '../utils/log_capture.ts'; import { stopAllDeviceLogCaptures } from '../utils/log-capture/device-log-sessions.ts'; +import { stopOwnedSimulatorLaunchOsLogSessions } from '../utils/log-capture/simulator-launch-oslog-sessions.ts'; import { stopAllVideoCaptureSessions } from '../utils/video_capture.ts'; import { stopAllTrackedProcesses } from '../mcp/tools/swift-package/active-processes.ts'; import { @@ -12,6 +13,7 @@ import { type FlushSentryOutcome, } from '../utils/sentry.ts'; import { sealSentryCapture } from '../utils/shutdown-state.ts'; +import { toErrorMessage } from '../utils/errors.ts'; import type { McpLifecycleSnapshot, McpShutdownReason } from './mcp-lifecycle.ts'; import { isTransportDisconnectReason } from './mcp-lifecycle.ts'; @@ -51,10 +53,6 @@ export interface McpShutdownResult { steps: ShutdownStepResult[]; } -function stringifyError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - async function runStep( name: string, timeoutMs: number, @@ -74,7 +72,7 @@ async function runStep( .catch( (error): RunStepRaceOutcome => ({ kind: 'error', - error: stringifyError(error), + error: toErrorMessage(error), }), ); const outcome = await Promise.race([operationOutcome, timeoutPromise]); @@ -146,12 +144,15 @@ export async function runMcpShutdown(input: { const steps: ShutdownStepResult[] = []; const pushStep = (name: string, outcome: ShutdownStepOutcome): void => { - steps.push({ + const step: ShutdownStepResult = { name, status: outcome.status, durationMs: outcome.durationMs, - ...(outcome.error ? { error: outcome.error } : {}), - }); + }; + if (outcome.error) { + step.error = outcome.error; + } + steps.push(step); }; const serverCloseTimeout = transportDisconnected @@ -196,6 +197,11 @@ export async function runMcpShutdown(input: { timeoutMs: bulkStepTimeoutMs(input.snapshot.simulatorLogSessionCount), operation: () => stopAllLogCaptures(STEP_TIMEOUT_MS), }, + { + name: 'simulator-launch-oslogs.stop-owned', + timeoutMs: bulkStepTimeoutMs(input.snapshot.ownedSimulatorLaunchOsLogSessionCount), + operation: () => stopOwnedSimulatorLaunchOsLogSessions(STEP_TIMEOUT_MS), + }, { name: 'device-logs.stop-all', timeoutMs: bulkStepTimeoutMs(input.snapshot.deviceLogSessionCount), @@ -218,7 +224,7 @@ export async function runMcpShutdown(input: { pushStep(cleanupStep.name, outcome); } - const triggerError = input.error === undefined ? undefined : stringifyError(input.error); + const triggerError = input.error === undefined ? undefined : toErrorMessage(input.error); const cleanupFailureCount = steps.filter( (step) => step.status === 'failed' || step.status === 'timed_out', ).length; diff --git a/src/snapshot-tests/__fixtures__/coverage/get-coverage-report--error-invalid-bundle.txt b/src/snapshot-tests/__fixtures__/cli/coverage/get-coverage-report--error-invalid-bundle.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/coverage/get-coverage-report--error-invalid-bundle.txt rename to src/snapshot-tests/__fixtures__/cli/coverage/get-coverage-report--error-invalid-bundle.txt diff --git a/src/snapshot-tests/__fixtures__/coverage/get-coverage-report--success.txt b/src/snapshot-tests/__fixtures__/cli/coverage/get-coverage-report--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/coverage/get-coverage-report--success.txt rename to src/snapshot-tests/__fixtures__/cli/coverage/get-coverage-report--success.txt diff --git a/src/snapshot-tests/__fixtures__/coverage/get-file-coverage--error-invalid-bundle.txt b/src/snapshot-tests/__fixtures__/cli/coverage/get-file-coverage--error-invalid-bundle.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/coverage/get-file-coverage--error-invalid-bundle.txt rename to src/snapshot-tests/__fixtures__/cli/coverage/get-file-coverage--error-invalid-bundle.txt diff --git a/src/snapshot-tests/__fixtures__/coverage/get-file-coverage--success.txt b/src/snapshot-tests/__fixtures__/cli/coverage/get-file-coverage--success.txt similarity index 77% rename from src/snapshot-tests/__fixtures__/coverage/get-file-coverage--success.txt rename to src/snapshot-tests/__fixtures__/cli/coverage/get-file-coverage--success.txt index 18f185fc..d880ab4c 100644 --- a/src/snapshot-tests/__fixtures__/coverage/get-file-coverage--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/coverage/get-file-coverage--success.txt @@ -6,10 +6,11 @@ File: example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorService.swift -ℹ️ Coverage: 83.1% (157/189 lines) +ℹ️ Coverage: 77.8% (147/189 lines) -🔴 Not Covered (7 functions, 22 lines) +🔴 Not Covered (8 functions, 27 lines) L159 CalculatorService.deleteLastDigit() -- 0/16 lines + L178 CalculatorService.setError(_:) -- 0/5 lines L58 implicit closure #2 in CalculatorService.inputNumber(_:) -- 0/1 lines L98 implicit closure #3 in CalculatorService.calculate() -- 0/1 lines L99 implicit closure #4 in CalculatorService.calculate() -- 0/1 lines @@ -18,12 +19,12 @@ File: example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorApp L214 implicit closure #4 in CalculatorService.formatNumber(_:) -- 0/1 lines 🟡 Partial Coverage (4 functions) + L63 CalculatorService.inputDecimal() -- 71.4% (10/14 lines) L184 CalculatorService.updateExpressionDisplay() -- 80.0% (8/10 lines) + L93 CalculatorService.calculate() -- 84.2% (32/38 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 (27 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__/debugging/add-breakpoint--error-no-session.txt b/src/snapshot-tests/__fixtures__/cli/debugging/add-breakpoint--error-no-session.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/add-breakpoint--error-no-session.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/add-breakpoint--error-no-session.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/add-breakpoint--success.txt b/src/snapshot-tests/__fixtures__/cli/debugging/add-breakpoint--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/add-breakpoint--success.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/add-breakpoint--success.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/attach--error-no-process.txt b/src/snapshot-tests/__fixtures__/cli/debugging/attach--error-no-process.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/attach--error-no-process.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/attach--error-no-process.txt diff --git a/src/snapshot-tests/__fixtures__/cli/debugging/attach--success-continue.txt b/src/snapshot-tests/__fixtures__/cli/debugging/attach--success-continue.txt new file mode 100644 index 00000000..c3d9682f --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/debugging/attach--success-continue.txt @@ -0,0 +1,12 @@ + +🐛 Attach Debugger + +✅ Attached DAP debugger to simulator process () + ├ Debug session ID: + ├ Status: This session is now the current debug session. + └ Execution: Execution is running. App is responsive to UI interaction. + +Next steps: +1. Add a breakpoint: xcodebuildmcp debugging add-breakpoint --debug-session-id "" --file "..." --line "123" +2. Continue execution: xcodebuildmcp debugging continue --debug-session-id "" +3. Show call stack: xcodebuildmcp debugging stack --debug-session-id "" diff --git a/src/snapshot-tests/__fixtures__/cli/debugging/attach--success.txt b/src/snapshot-tests/__fixtures__/cli/debugging/attach--success.txt new file mode 100644 index 00000000..18563ca0 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/debugging/attach--success.txt @@ -0,0 +1,12 @@ + +🐛 Attach Debugger + +✅ Attached DAP debugger to simulator process () + ├ Debug session ID: + ├ Status: This session is now the current debug session. + └ Execution: Execution is paused. Use debug_continue to resume before UI automation. + +Next steps: +1. Add a breakpoint: xcodebuildmcp debugging add-breakpoint --debug-session-id "" --file "..." --line "123" +2. Continue execution: xcodebuildmcp debugging continue --debug-session-id "" +3. Show call stack: xcodebuildmcp debugging stack --debug-session-id "" diff --git a/src/snapshot-tests/__fixtures__/debugging/continue--error-no-session.txt b/src/snapshot-tests/__fixtures__/cli/debugging/continue--error-no-session.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/continue--error-no-session.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/continue--error-no-session.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/continue--success.txt b/src/snapshot-tests/__fixtures__/cli/debugging/continue--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/continue--success.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/continue--success.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/detach--error-no-session.txt b/src/snapshot-tests/__fixtures__/cli/debugging/detach--error-no-session.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/detach--error-no-session.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/detach--error-no-session.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/detach--success.txt b/src/snapshot-tests/__fixtures__/cli/debugging/detach--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/detach--success.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/detach--success.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/lldb-command--error-no-session.txt b/src/snapshot-tests/__fixtures__/cli/debugging/lldb-command--error-no-session.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/lldb-command--error-no-session.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/lldb-command--error-no-session.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/lldb-command--success.txt b/src/snapshot-tests/__fixtures__/cli/debugging/lldb-command--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/lldb-command--success.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/lldb-command--success.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/remove-breakpoint--error-no-session.txt b/src/snapshot-tests/__fixtures__/cli/debugging/remove-breakpoint--error-no-session.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/remove-breakpoint--error-no-session.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/remove-breakpoint--error-no-session.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/remove-breakpoint--success.txt b/src/snapshot-tests/__fixtures__/cli/debugging/remove-breakpoint--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/remove-breakpoint--success.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/remove-breakpoint--success.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/stack--error-no-session.txt b/src/snapshot-tests/__fixtures__/cli/debugging/stack--error-no-session.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/stack--error-no-session.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/stack--error-no-session.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/stack--success.txt b/src/snapshot-tests/__fixtures__/cli/debugging/stack--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/stack--success.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/stack--success.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/variables--error-no-session.txt b/src/snapshot-tests/__fixtures__/cli/debugging/variables--error-no-session.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/variables--error-no-session.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/variables--error-no-session.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/variables--success.txt b/src/snapshot-tests/__fixtures__/cli/debugging/variables--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/variables--success.txt rename to src/snapshot-tests/__fixtures__/cli/debugging/variables--success.txt diff --git a/src/snapshot-tests/__fixtures__/device/build--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/device/build--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/build--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/device/build--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/device/build--success.txt b/src/snapshot-tests/__fixtures__/cli/device/build--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/build--success.txt rename to src/snapshot-tests/__fixtures__/cli/device/build--success.txt diff --git a/src/snapshot-tests/__fixtures__/cli/device/build-and-run--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/device/build-and-run--error-wrong-scheme.txt new file mode 100644 index 00000000..37a0c499 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/device/build-and-run--error-wrong-scheme.txt @@ -0,0 +1,16 @@ + +🚀 Build & Run + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + Device: () + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_run_device__pid.log diff --git a/src/snapshot-tests/__fixtures__/device/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/cli/device/build-and-run--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/build-and-run--success.txt rename to src/snapshot-tests/__fixtures__/cli/device/build-and-run--success.txt diff --git a/src/snapshot-tests/__fixtures__/device/get-app-path--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/device/get-app-path--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/get-app-path--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/device/get-app-path--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/device/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/cli/device/get-app-path--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/get-app-path--success.txt rename to src/snapshot-tests/__fixtures__/cli/device/get-app-path--success.txt diff --git a/src/snapshot-tests/__fixtures__/device/install--error-invalid-app.txt b/src/snapshot-tests/__fixtures__/cli/device/install--error-invalid-app.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/install--error-invalid-app.txt rename to src/snapshot-tests/__fixtures__/cli/device/install--error-invalid-app.txt diff --git a/src/snapshot-tests/__fixtures__/device/install--success.txt b/src/snapshot-tests/__fixtures__/cli/device/install--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/install--success.txt rename to src/snapshot-tests/__fixtures__/cli/device/install--success.txt diff --git a/src/snapshot-tests/__fixtures__/device/launch--error-invalid-bundle.txt b/src/snapshot-tests/__fixtures__/cli/device/launch--error-invalid-bundle.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/launch--error-invalid-bundle.txt rename to src/snapshot-tests/__fixtures__/cli/device/launch--error-invalid-bundle.txt diff --git a/src/snapshot-tests/__fixtures__/device/launch--success.txt b/src/snapshot-tests/__fixtures__/cli/device/launch--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/launch--success.txt rename to src/snapshot-tests/__fixtures__/cli/device/launch--success.txt diff --git a/src/snapshot-tests/__fixtures__/device/list--success.txt b/src/snapshot-tests/__fixtures__/cli/device/list--success.txt similarity index 88% rename from src/snapshot-tests/__fixtures__/device/list--success.txt rename to src/snapshot-tests/__fixtures__/cli/device/list--success.txt index 3c710681..b6e03de4 100644 --- a/src/snapshot-tests/__fixtures__/device/list--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/device/list--success.txt @@ -29,6 +29,6 @@ Hints Before running build/run/test/UI automation tools, set the desired device identifier in session defaults. Next steps: -1. Build for device: xcodebuildmcp device build +1. Build for device: xcodebuildmcp device build --scheme "YOUR_SCHEME" --device-id "UUID_FROM_ABOVE" 2. Run tests on device: xcodebuildmcp device test 3. Get app path: xcodebuildmcp device get-app-path diff --git a/src/snapshot-tests/__fixtures__/device/stop--error-no-app.txt b/src/snapshot-tests/__fixtures__/cli/device/stop--error-no-app.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/stop--error-no-app.txt rename to src/snapshot-tests/__fixtures__/cli/device/stop--error-no-app.txt diff --git a/src/snapshot-tests/__fixtures__/device/stop--success.txt b/src/snapshot-tests/__fixtures__/cli/device/stop--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/device/stop--success.txt rename to src/snapshot-tests/__fixtures__/cli/device/stop--success.txt diff --git a/src/snapshot-tests/__fixtures__/device/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/device/test--failure.txt similarity index 56% rename from src/snapshot-tests/__fixtures__/device/test--failure.txt rename to src/snapshot-tests/__fixtures__/cli/device/test--failure.txt index ff1e90f3..b3d3e4cc 100644 --- a/src/snapshot-tests/__fixtures__/device/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/cli/device/test--failure.txt @@ -7,6 +7,15 @@ Device: () Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData +Discovered 52 test(s): + CalculatorAppFeatureTests/CalculatorBasicTests/testClear + CalculatorAppFeatureTests/CalculatorBasicTests/testInitialState + CalculatorAppFeatureTests/CalculatorBasicTests/testIntentionalFailure + CalculatorAppFeatureTests/CalculatorIntegrationTests/testChainCalculations + CalculatorAppFeatureTests/CalculatorIntegrationTests/testComplexCalculation + CalculatorAppFeatureTests/CalculatorIntegrationTests/testExpressionDisplay + (...and 46 more) + CalculatorAppTests ✗ testCalculatorServiceFailure: - XCTAssertEqual failed: ("0") is not equal to ("999") - This test should fail - display should be 0, not 999 @@ -17,5 +26,5 @@ IntentionalFailureTests - XCTAssertTrue failed - This test should fail to verify error reporting example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:286 -❌ 2 tests failed, 21 passed, 0 skipped (⏱️ ) +❌ tests failed, passed, skipped (⏱️ ) └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_device__pid.log diff --git a/src/snapshot-tests/__fixtures__/device/test--success.txt b/src/snapshot-tests/__fixtures__/cli/device/test--success.txt similarity index 67% rename from src/snapshot-tests/__fixtures__/device/test--success.txt rename to src/snapshot-tests/__fixtures__/cli/device/test--success.txt index 6e3662c9..fdd54e05 100644 --- a/src/snapshot-tests/__fixtures__/device/test--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/device/test--success.txt @@ -6,6 +6,11 @@ Platform: iOS Device: () Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + Selective Testing: + CalculatorAppTests/CalculatorAppTests/testAddition + +Discovered 1 test(s): + CalculatorAppTests/CalculatorAppTests/testAddition ✅ 1 test passed, 0 skipped (⏱️ ) └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_device__pid.log diff --git a/src/snapshot-tests/__fixtures__/macos/build--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/macos/build--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/build--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/macos/build--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/macos/build--success.txt b/src/snapshot-tests/__fixtures__/cli/macos/build--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/build--success.txt rename to src/snapshot-tests/__fixtures__/cli/macos/build--success.txt diff --git a/src/snapshot-tests/__fixtures__/macos/build-and-run--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/macos/build-and-run--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/build-and-run--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/macos/build-and-run--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/macos/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/cli/macos/build-and-run--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/build-and-run--success.txt rename to src/snapshot-tests/__fixtures__/cli/macos/build-and-run--success.txt diff --git a/src/snapshot-tests/__fixtures__/macos/get-app-path--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/macos/get-app-path--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/get-app-path--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/macos/get-app-path--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/macos/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/cli/macos/get-app-path--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/get-app-path--success.txt rename to src/snapshot-tests/__fixtures__/cli/macos/get-app-path--success.txt diff --git a/src/snapshot-tests/__fixtures__/macos/get-macos-bundle-id--error-missing-app.txt b/src/snapshot-tests/__fixtures__/cli/macos/get-macos-bundle-id--error-missing-app.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/get-macos-bundle-id--error-missing-app.txt rename to src/snapshot-tests/__fixtures__/cli/macos/get-macos-bundle-id--error-missing-app.txt diff --git a/src/snapshot-tests/__fixtures__/macos/get-macos-bundle-id--success.txt b/src/snapshot-tests/__fixtures__/cli/macos/get-macos-bundle-id--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/get-macos-bundle-id--success.txt rename to src/snapshot-tests/__fixtures__/cli/macos/get-macos-bundle-id--success.txt diff --git a/src/snapshot-tests/__fixtures__/macos/launch--error-invalid-app.txt b/src/snapshot-tests/__fixtures__/cli/macos/launch--error-invalid-app.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/launch--error-invalid-app.txt rename to src/snapshot-tests/__fixtures__/cli/macos/launch--error-invalid-app.txt diff --git a/src/snapshot-tests/__fixtures__/macos/launch--success.txt b/src/snapshot-tests/__fixtures__/cli/macos/launch--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/launch--success.txt rename to src/snapshot-tests/__fixtures__/cli/macos/launch--success.txt diff --git a/src/snapshot-tests/__fixtures__/macos/stop--error-no-app.txt b/src/snapshot-tests/__fixtures__/cli/macos/stop--error-no-app.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/stop--error-no-app.txt rename to src/snapshot-tests/__fixtures__/cli/macos/stop--error-no-app.txt diff --git a/src/snapshot-tests/__fixtures__/macos/stop--success.txt b/src/snapshot-tests/__fixtures__/cli/macos/stop--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/stop--success.txt rename to src/snapshot-tests/__fixtures__/cli/macos/stop--success.txt diff --git a/src/snapshot-tests/__fixtures__/macos/test--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/macos/test--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/macos/test--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/macos/test--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/macos/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/macos/test--failure.txt similarity index 64% rename from src/snapshot-tests/__fixtures__/macos/test--failure.txt rename to src/snapshot-tests/__fixtures__/cli/macos/test--failure.txt index c6fa1a4b..7f876426 100644 --- a/src/snapshot-tests/__fixtures__/macos/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/cli/macos/test--failure.txt @@ -6,6 +6,12 @@ Platform: macOS Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData +Discovered 4 test(s): + MCPTestTests/MCPTestTests/appNameIsCorrect + MCPTestTests/MCPTestTests/deliberateFailure + MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect + MCPTestTests/MCPTestsXCTests/testDeliberateFailure + MCPTestsXCTests ✗ testDeliberateFailure(): - XCTAssertTrue failed - This test is designed to fail for snapshot testing @@ -16,5 +22,5 @@ MCPTestTests - Expectation failed: 1 == 2: This test is designed to fail for snapshot testing MCPTestTests.swift:11 -❌ 2 tests failed, 2 passed, 0 skipped (⏱️ ) +❌ tests failed, passed, skipped (⏱️ ) └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/macos/test--success.txt b/src/snapshot-tests/__fixtures__/cli/macos/test--success.txt similarity index 53% rename from src/snapshot-tests/__fixtures__/macos/test--success.txt rename to src/snapshot-tests/__fixtures__/cli/macos/test--success.txt index 50a4db50..384ca1bf 100644 --- a/src/snapshot-tests/__fixtures__/macos/test--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/macos/test--success.txt @@ -5,6 +5,13 @@ Configuration: Debug Platform: macOS Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + Selective Testing: + MCPTestTests/MCPTestTests/appNameIsCorrect() + MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect + +Discovered 2 test(s): + MCPTestTests/MCPTestTests/appNameIsCorrect + MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect ✅ 2 tests passed, 0 skipped (⏱️ ) └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/project-discovery/discover-projs--error-invalid-root.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/discover-projs--error-invalid-root.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/discover-projs--error-invalid-root.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/discover-projs--error-invalid-root.txt diff --git a/src/snapshot-tests/__fixtures__/project-discovery/discover-projs--success.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/discover-projs--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/discover-projs--success.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/discover-projs--success.txt diff --git a/src/snapshot-tests/__fixtures__/project-discovery/get-app-bundle-id--error-missing-app.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/get-app-bundle-id--error-missing-app.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/get-app-bundle-id--error-missing-app.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/get-app-bundle-id--error-missing-app.txt diff --git a/src/snapshot-tests/__fixtures__/project-discovery/get-app-bundle-id--success.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/get-app-bundle-id--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/get-app-bundle-id--success.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/get-app-bundle-id--success.txt diff --git a/src/snapshot-tests/__fixtures__/project-discovery/get-macos-bundle-id--error-missing-app.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/get-macos-bundle-id--error-missing-app.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/get-macos-bundle-id--error-missing-app.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/get-macos-bundle-id--error-missing-app.txt diff --git a/src/snapshot-tests/__fixtures__/project-discovery/get-macos-bundle-id--success.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/get-macos-bundle-id--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/get-macos-bundle-id--success.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/get-macos-bundle-id--success.txt diff --git a/src/snapshot-tests/__fixtures__/project-discovery/list-schemes--error-invalid-workspace.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/list-schemes--error-invalid-workspace.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/list-schemes--error-invalid-workspace.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/list-schemes--error-invalid-workspace.txt diff --git a/src/snapshot-tests/__fixtures__/project-discovery/list-schemes--success.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/list-schemes--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/list-schemes--success.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/list-schemes--success.txt diff --git a/src/snapshot-tests/__fixtures__/project-discovery/show-build-settings--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/show-build-settings--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/show-build-settings--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/show-build-settings--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/project-discovery/show-build-settings--success.txt b/src/snapshot-tests/__fixtures__/cli/project-discovery/show-build-settings--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-discovery/show-build-settings--success.txt rename to src/snapshot-tests/__fixtures__/cli/project-discovery/show-build-settings--success.txt diff --git a/src/snapshot-tests/__fixtures__/project-scaffolding/scaffold-ios--error-existing.txt b/src/snapshot-tests/__fixtures__/cli/project-scaffolding/scaffold-ios--error-existing.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-scaffolding/scaffold-ios--error-existing.txt rename to src/snapshot-tests/__fixtures__/cli/project-scaffolding/scaffold-ios--error-existing.txt diff --git a/src/snapshot-tests/__fixtures__/project-scaffolding/scaffold-ios--success.txt b/src/snapshot-tests/__fixtures__/cli/project-scaffolding/scaffold-ios--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-scaffolding/scaffold-ios--success.txt rename to src/snapshot-tests/__fixtures__/cli/project-scaffolding/scaffold-ios--success.txt diff --git a/src/snapshot-tests/__fixtures__/project-scaffolding/scaffold-macos--error-existing.txt b/src/snapshot-tests/__fixtures__/cli/project-scaffolding/scaffold-macos--error-existing.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-scaffolding/scaffold-macos--error-existing.txt rename to src/snapshot-tests/__fixtures__/cli/project-scaffolding/scaffold-macos--error-existing.txt diff --git a/src/snapshot-tests/__fixtures__/project-scaffolding/scaffold-macos--success.txt b/src/snapshot-tests/__fixtures__/cli/project-scaffolding/scaffold-macos--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/project-scaffolding/scaffold-macos--success.txt rename to src/snapshot-tests/__fixtures__/cli/project-scaffolding/scaffold-macos--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/boot--error-invalid-id.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/boot--error-invalid-id.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/boot--error-invalid-id.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/boot--error-invalid-id.txt diff --git a/src/snapshot-tests/__fixtures__/cli/simulator-management/boot--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/boot--success.txt new file mode 100644 index 00000000..8b053d01 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/simulator-management/boot--success.txt @@ -0,0 +1,11 @@ + +📱 Boot Simulator + + Simulator: + +✅ Simulator booted successfully + +Next steps: +1. Open the Simulator app (makes it visible): xcodebuildmcp simulator-management open +2. Install an app: xcodebuildmcp simulator install --simulator-id "" --app-path "PATH_TO_YOUR_APP" +3. Launch an app: xcodebuildmcp simulator launch-app --simulator-id "" --bundle-id "YOUR_APP_BUNDLE_ID" diff --git a/src/snapshot-tests/__fixtures__/simulator-management/erase--error-invalid-id.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/erase--error-invalid-id.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/erase--error-invalid-id.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/erase--error-invalid-id.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/erase--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/erase--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/erase--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/erase--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/list--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/list--success.txt similarity index 94% rename from src/snapshot-tests/__fixtures__/simulator/list--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/list--success.txt index 17bcb5e9..bef2aac7 100644 --- a/src/snapshot-tests/__fixtures__/simulator/list--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/simulator-management/list--success.txt @@ -5,10 +5,10 @@ iOS Simulators: iOS 26.4: - 📱 [✗] iPhone 17 Pro (Shutdown) + 📱 [✓] iPhone 17 Pro (Booted) UDID: - 📱 [✗] iPhone 17 Pro Max (Shutdown) + 📱 [✓] iPhone 17 Pro Max (Booted) UDID: 📱 [✗] iPhone 17e (Shutdown) diff --git a/src/snapshot-tests/__fixtures__/simulator-management/open--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/open--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/open--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/open--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/reset-location--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/reset-location--error-invalid-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/reset-location--error-invalid-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/reset-location--error-invalid-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/reset-location--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/reset-location--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/reset-location--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/reset-location--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/set-appearance--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/set-appearance--error-invalid-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/set-appearance--error-invalid-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/set-appearance--error-invalid-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/set-appearance--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/set-appearance--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/set-appearance--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/set-appearance--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/set-location--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/set-location--error-invalid-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/set-location--error-invalid-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/set-location--error-invalid-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/set-location--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/set-location--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/set-location--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/set-location--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/statusbar--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/statusbar--error-invalid-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/statusbar--error-invalid-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/statusbar--error-invalid-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/statusbar--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator-management/statusbar--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator-management/statusbar--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator-management/statusbar--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/build--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/simulator/build--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/build--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/build--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/build--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator/build--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/build--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/build--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/build-and-run--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/simulator/build-and-run--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/build-and-run--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/build-and-run--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator/build-and-run--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/build-and-run--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/build-and-run--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/get-app-path--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/simulator/get-app-path--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/get-app-path--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/get-app-path--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator/get-app-path--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/get-app-path--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/get-app-path--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/install--error-invalid-app.txt b/src/snapshot-tests/__fixtures__/cli/simulator/install--error-invalid-app.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/install--error-invalid-app.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/install--error-invalid-app.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/install--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator/install--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/install--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/install--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/launch-app--error-not-installed.txt b/src/snapshot-tests/__fixtures__/cli/simulator/launch-app--error-not-installed.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/launch-app--error-not-installed.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/launch-app--error-not-installed.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/launch-app--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator/launch-app--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/launch-app--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/launch-app--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator-management/list--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator/list--success.txt similarity index 71% rename from src/snapshot-tests/__fixtures__/simulator-management/list--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/list--success.txt index e5176898..bef2aac7 100644 --- a/src/snapshot-tests/__fixtures__/simulator-management/list--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/simulator/list--success.txt @@ -5,10 +5,10 @@ iOS Simulators: iOS 26.4: - 📱 [✗] iPhone 17 Pro (Shutdown) + 📱 [✓] iPhone 17 Pro (Booted) UDID: - 📱 [✗] iPhone 17 Pro Max (Shutdown) + 📱 [✓] iPhone 17 Pro Max (Booted) UDID: 📱 [✗] iPhone 17e (Shutdown) @@ -38,26 +38,7 @@ iOS Simulators: 📱 [✗] iPad (A16) (Shutdown) UDID: -watchOS Simulators: - - watchOS 26.4: - - ⌚️ [✗] Apple Watch Series 11 (46mm) (Shutdown) - UDID: - - ⌚️ [✗] Apple Watch Series 11 (42mm) (Shutdown) - UDID: - - ⌚️ [✗] Apple Watch Ultra 3 (49mm) (Shutdown) - UDID: - - ⌚️ [✗] Apple Watch SE 3 (44mm) (Shutdown) - UDID: - - ⌚️ [✗] Apple Watch SE 3 (40mm) (Shutdown) - UDID: - -✅ 16 simulators available (11 iOS, 5 watchOS). +✅ 11 simulators available (11 iOS). Hints Use the simulator ID/UDID from above when required by other tools. diff --git a/src/snapshot-tests/__fixtures__/simulator/screenshot--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/cli/simulator/screenshot--error-invalid-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/screenshot--error-invalid-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/screenshot--error-invalid-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/screenshot--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator/screenshot--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/screenshot--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/screenshot--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/stop--error-no-app.txt b/src/snapshot-tests/__fixtures__/cli/simulator/stop--error-no-app.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/stop--error-no-app.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/stop--error-no-app.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/stop--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator/stop--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/stop--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/stop--success.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/test--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/simulator/test--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/simulator/test--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/test--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/simulator/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/simulator/test--failure.txt similarity index 63% rename from src/snapshot-tests/__fixtures__/simulator/test--failure.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/test--failure.txt index a4508780..9db430ad 100644 --- a/src/snapshot-tests/__fixtures__/simulator/test--failure.txt +++ b/src/snapshot-tests/__fixtures__/cli/simulator/test--failure.txt @@ -7,6 +7,15 @@ Simulator: iPhone 17 Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData +Discovered 52 test(s): + CalculatorAppFeatureTests/CalculatorBasicTests/testClear + CalculatorAppFeatureTests/CalculatorBasicTests/testInitialState + CalculatorAppFeatureTests/CalculatorBasicTests/testIntentionalFailure + CalculatorAppFeatureTests/CalculatorIntegrationTests/testChainCalculations + CalculatorAppFeatureTests/CalculatorIntegrationTests/testComplexCalculation + CalculatorAppFeatureTests/CalculatorIntegrationTests/testExpressionDisplay + (...and 46 more) + CalculatorAppTests ✗ testCalculatorServiceFailure: - XCTAssertEqual failed: ("0") is not equal to ("999") - This test should fail - display should be 0, not 999 diff --git a/src/snapshot-tests/__fixtures__/simulator/test--success.txt b/src/snapshot-tests/__fixtures__/cli/simulator/test--success.txt similarity index 67% rename from src/snapshot-tests/__fixtures__/simulator/test--success.txt rename to src/snapshot-tests/__fixtures__/cli/simulator/test--success.txt index 32ec09c3..d0fb32a1 100644 --- a/src/snapshot-tests/__fixtures__/simulator/test--success.txt +++ b/src/snapshot-tests/__fixtures__/cli/simulator/test--success.txt @@ -6,6 +6,11 @@ Platform: iOS Simulator Simulator: iPhone 17 Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + Selective Testing: + CalculatorAppTests/CalculatorAppTests/testAddition + +Discovered 1 test(s): + CalculatorAppTests/CalculatorAppTests/testAddition ✅ 1 test passed, 0 skipped (⏱️ ) └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_sim__pid.log diff --git a/src/snapshot-tests/__fixtures__/swift-package/build--error-bad-path.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/build--error-bad-path.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/build--error-bad-path.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/build--error-bad-path.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/build--success.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/build--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/build--success.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/build--success.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/clean--error-bad-path.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/clean--error-bad-path.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/clean--error-bad-path.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/clean--error-bad-path.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/clean--success.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/clean--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/clean--success.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/clean--success.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/list--no-processes.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/list--no-processes.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/list--no-processes.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/list--no-processes.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/list--success.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/list--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/list--success.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/list--success.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/run--error-bad-executable.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/run--error-bad-executable.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/run--error-bad-executable.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/run--error-bad-executable.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/run--success.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/run--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/run--success.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/run--success.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/stop--error-no-process.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/stop--error-no-process.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/stop--error-no-process.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/stop--error-no-process.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/test--error-bad-path.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/test--error-bad-path.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/test--error-bad-path.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/test--error-bad-path.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/test--failure.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/test--failure.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/test--failure.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/test--failure.txt diff --git a/src/snapshot-tests/__fixtures__/swift-package/test--success.txt b/src/snapshot-tests/__fixtures__/cli/swift-package/test--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/swift-package/test--success.txt rename to src/snapshot-tests/__fixtures__/cli/swift-package/test--success.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/button--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/button--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/button--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/button--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/button--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/button--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/button--success.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/button--success.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/gesture--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/gesture--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/gesture--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/gesture--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/gesture--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/gesture--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/gesture--success.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/gesture--success.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/key-press--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/key-press--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/key-press--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/key-press--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/key-press--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/key-press--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/key-press--success.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/key-press--success.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/key-sequence--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/key-sequence--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/key-sequence--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/key-sequence--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/key-sequence--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/key-sequence--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/key-sequence--success.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/key-sequence--success.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/long-press--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/long-press--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/long-press--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/long-press--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/long-press--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/long-press--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/long-press--success.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/long-press--success.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/snapshot-ui--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/snapshot-ui--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/snapshot-ui--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/snapshot-ui--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/cli/ui-automation/snapshot-ui--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/snapshot-ui--success.txt new file mode 100644 index 00000000..4a5a0c65 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/cli/ui-automation/snapshot-ui--success.txt @@ -0,0 +1,336 @@ + +📷 Snapshot UI + + Simulator: + +✅ Accessibility hierarchy retrieved successfully. + +Accessibility Hierarchy + ```json + [ + { + "AXFrame" : "{{0, 0}, {402, 874}}", + "AXUniqueId" : null, + "frame" : { + "y" : 0, + "x" : 0, + "width" : 402, + "height" : 874 + }, + "role_description" : "application", + "AXLabel" : " ", + "content_required" : false, + "type" : "Application", + "title" : null, + "help" : null, + "custom_actions" : [ + + ], + "AXValue" : null, + "enabled" : true, + "role" : "AXApplication", + "children" : [ + { + "AXFrame" : "{{28.333333333333371, 88}, {68, 90.666666666666657}}", + "AXUniqueId" : "Fitness", + "frame" : { + "y" : 88, + "x" : 28.3, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Fitness", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{120.66666666666674, 88}, {68, 90.666666666666657}}", + "AXUniqueId" : "Watch", + "frame" : { + "y" : 88, + "x" : 120.7, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Watch", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{211.33333333333326, 88}, {72.333333333333314, 90.666666666666657}}", + "AXUniqueId" : "Contacts", + "frame" : { + "y" : 88, + "x" : 211.3, + "width" : 72.3, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Contacts", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{306, 88}, {68, 90.666666666666657}}", + "AXUniqueId" : "Files", + "frame" : { + "y" : 88, + "x" : 306, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Files", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{28.333333333333371, 188.33333333333334}, {68, 90.666666666666714}}", + "AXUniqueId" : "Preview", + "frame" : { + "y" : 188.3, + "x" : 28.3, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Preview", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{120.66666666666674, 188.33333333333334}, {68, 90.666666666666714}}", + "AXUniqueId" : "Utilities", + "frame" : { + "y" : 188.3, + "x" : 120.7, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Utilities folder", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "2 apps", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{207.66666666666663, 188.33333333333334}, {79.333333333333314, 90.666666666666714}}", + "AXUniqueId" : "Calculator", + "frame" : { + "y" : 188.3, + "x" : 207.7, + "width" : 79.3, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Calculator", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{162, 703.66666666666674}, {78, 30}}", + "AXUniqueId" : "spotlight-pill", + "frame" : { + "y" : 703.7, + "x" : 162, + "width" : 78, + "height" : 30 + }, + "role_description" : "slider", + "AXLabel" : "Search", + "content_required" : false, + "type" : "Slider", + "title" : null, + "help" : "Activate to open Spotlight.", + "custom_actions" : [ + + ], + "AXValue" : "Page 2 of 2", + "enabled" : true, + "role" : "AXSlider", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{123, 771.33333333333326}, {68, 68}}", + "AXUniqueId" : "Safari", + "frame" : { + "y" : 771.3, + "x" : 123, + "width" : 68, + "height" : 68 + }, + "role_description" : "button", + "AXLabel" : "Safari", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{211, 771.33333333333326}, {68, 68}}", + "AXUniqueId" : "Messages", + "frame" : { + "y" : 771.3, + "x" : 211, + "width" : 68, + "height" : 68 + }, + "role_description" : "button", + "AXLabel" : "Messages", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + } + ], + "subrole" : null, + "pid" : + } +] + ``` + +Tips + - Use frame coordinates for tap/swipe (center: x+width/2, y+height/2) + - If a debugger is attached, ensure the app is running (not stopped on breakpoints) + - Screenshots are for visual verification only + +Next steps: +1. Refresh after layout changes: xcodebuildmcp simulator snapshot-ui --simulator-id "" +2. Tap on element: xcodebuildmcp ui-automation tap --simulator-id "" --x "0" --y "0" +3. Take screenshot for verification: xcodebuildmcp simulator screenshot --simulator-id "" diff --git a/src/snapshot-tests/__fixtures__/ui-automation/swipe--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/swipe--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/swipe--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/swipe--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/swipe--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/swipe--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/swipe--success.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/swipe--success.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/tap--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/tap--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/tap--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/tap--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/tap--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/tap--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/tap--success.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/tap--success.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/touch--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/touch--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/touch--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/touch--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/touch--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/touch--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/touch--success.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/touch--success.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/type-text--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/type-text--error-no-simulator.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/type-text--error-no-simulator.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/type-text--error-no-simulator.txt diff --git a/src/snapshot-tests/__fixtures__/ui-automation/type-text--success.txt b/src/snapshot-tests/__fixtures__/cli/ui-automation/type-text--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/ui-automation/type-text--success.txt rename to src/snapshot-tests/__fixtures__/cli/ui-automation/type-text--success.txt diff --git a/src/snapshot-tests/__fixtures__/utilities/clean--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/cli/utilities/clean--error-wrong-scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/utilities/clean--error-wrong-scheme.txt rename to src/snapshot-tests/__fixtures__/cli/utilities/clean--error-wrong-scheme.txt diff --git a/src/snapshot-tests/__fixtures__/utilities/clean--success.txt b/src/snapshot-tests/__fixtures__/cli/utilities/clean--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/utilities/clean--success.txt rename to src/snapshot-tests/__fixtures__/cli/utilities/clean--success.txt diff --git a/src/snapshot-tests/__fixtures__/mcp-integration/build-sim--missing-params.txt b/src/snapshot-tests/__fixtures__/mcp-integration/build-sim--missing-params.txt deleted file mode 100644 index cbee9535..00000000 --- a/src/snapshot-tests/__fixtures__/mcp-integration/build-sim--missing-params.txt +++ /dev/null @@ -1,3 +0,0 @@ - -❌ Missing required session defaults: scheme is required -Set with: session-set-defaults { "scheme": "..." } diff --git a/src/snapshot-tests/__fixtures__/mcp/coverage/get-coverage-report--error-invalid-bundle.txt b/src/snapshot-tests/__fixtures__/mcp/coverage/get-coverage-report--error-invalid-bundle.txt new file mode 100644 index 00000000..f1b738e6 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/coverage/get-coverage-report--error-invalid-bundle.txt @@ -0,0 +1,6 @@ + +📊 Coverage Report + + xcresult: /invalid.xcresult + +❌ Failed to get coverage report: Error: Error Domain=XCCovErrorDomain Code=0 "Failed to load result bundle" UserInfo={NSLocalizedDescription=Failed to load result bundle, NSUnderlyingError= {Error Domain=XCResultStorage.ResultBundleFactory.Error Code=0 "Failed to create a new result bundle reader, underlying error: Info.plist at /invalid.xcresult/Info.plist does not exist, the result bundle might be corrupted or the provided path is not a result bundle"}} diff --git a/src/snapshot-tests/__fixtures__/mcp/coverage/get-coverage-report--success.txt b/src/snapshot-tests/__fixtures__/mcp/coverage/get-coverage-report--success.txt new file mode 100644 index 00000000..d33a2699 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/coverage/get-coverage-report--success.txt @@ -0,0 +1,13 @@ + +📊 Coverage Report + + xcresult: /TestResults.xcresult + Target Filter: CalculatorAppTests + +ℹ️ Overall: 94.9% (371/391 lines) + +Targets + CalculatorAppTests.xctest: 94.9% (371/391 lines) + +Next steps: +1. View file-level coverage: get_file_coverage({ xcresultPath: "/TestResults.xcresult" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/coverage/get-file-coverage--error-invalid-bundle.txt b/src/snapshot-tests/__fixtures__/mcp/coverage/get-file-coverage--error-invalid-bundle.txt new file mode 100644 index 00000000..2d5def30 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/coverage/get-file-coverage--error-invalid-bundle.txt @@ -0,0 +1,7 @@ + +📊 File Coverage + + xcresult: /invalid.xcresult + File: SomeFile.swift + +❌ Failed to get file coverage: Error: Error Domain=XCCovErrorDomain Code=0 "Failed to load result bundle" UserInfo={NSLocalizedDescription=Failed to load result bundle, NSUnderlyingError= {Error Domain=XCResultStorage.ResultBundleFactory.Error Code=0 "Failed to create a new result bundle reader, underlying error: Info.plist at /invalid.xcresult/Info.plist does not exist, the result bundle might be corrupted or the provided path is not a result bundle"}} diff --git a/src/snapshot-tests/__fixtures__/mcp/coverage/get-file-coverage--success.txt b/src/snapshot-tests/__fixtures__/mcp/coverage/get-file-coverage--success.txt new file mode 100644 index 00000000..064fd9eb --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/coverage/get-file-coverage--success.txt @@ -0,0 +1,30 @@ + +📊 File Coverage + + xcresult: /TestResults.xcresult + File: CalculatorService.swift + +File: example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorService.swift + +ℹ️ Coverage: 77.8% (147/189 lines) + +🔴 Not Covered (8 functions, 27 lines) + L159 CalculatorService.deleteLastDigit() -- 0/16 lines + L178 CalculatorService.setError(_:) -- 0/5 lines + L58 implicit closure #2 in CalculatorService.inputNumber(_:) -- 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 + L214 implicit closure #4 in CalculatorService.formatNumber(_:) -- 0/1 lines + +🟡 Partial Coverage (4 functions) + L63 CalculatorService.inputDecimal() -- 71.4% (10/14 lines) + L184 CalculatorService.updateExpressionDisplay() -- 80.0% (8/10 lines) + L93 CalculatorService.calculate() -- 84.2% (32/38 lines) + L195 CalculatorService.formatNumber(_:) -- 85.7% (18/21 lines) + +🟢 Full Coverage (27 functions) -- all at 100% + +Next steps: +1. View overall coverage: get_coverage_report({ xcresultPath: "/TestResults.xcresult" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/add-breakpoint--error-no-session.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/add-breakpoint--error-no-session.txt new file mode 100644 index 00000000..1495a4c5 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/add-breakpoint--error-no-session.txt @@ -0,0 +1,4 @@ + +🐛 Add Breakpoint + +❌ Failed to add breakpoint: No active debug session. Provide debugSessionId or attach first. diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/add-breakpoint--success.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/add-breakpoint--success.txt new file mode 100644 index 00000000..771bfb62 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/add-breakpoint--success.txt @@ -0,0 +1,7 @@ + +🐛 Add Breakpoint + +✅ Breakpoint 1 set + +Output: + Set breakpoint 1 at ContentView.swift:42 diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/attach--error-no-process.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/attach--error-no-process.txt new file mode 100644 index 00000000..06fc6b03 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/attach--error-no-process.txt @@ -0,0 +1,4 @@ + +🐛 Attach Debugger + +❌ Failed to resolve simulator PID: Invalid device: diff --git a/src/snapshot-tests/__fixtures__/debugging/attach--success-continue.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/attach--success-continue.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/attach--success-continue.txt rename to src/snapshot-tests/__fixtures__/mcp/debugging/attach--success-continue.txt diff --git a/src/snapshot-tests/__fixtures__/debugging/attach--success.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/attach--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/debugging/attach--success.txt rename to src/snapshot-tests/__fixtures__/mcp/debugging/attach--success.txt diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/continue--error-no-session.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/continue--error-no-session.txt new file mode 100644 index 00000000..b3d0975a --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/continue--error-no-session.txt @@ -0,0 +1,4 @@ + +🐛 Continue + +❌ Failed to resume debugger: No active debug session. Provide debugSessionId or attach first. diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/continue--success.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/continue--success.txt new file mode 100644 index 00000000..bb44582d --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/continue--success.txt @@ -0,0 +1,4 @@ + +🐛 Continue + +✅ Resumed debugger session diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/detach--error-no-session.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/detach--error-no-session.txt new file mode 100644 index 00000000..2b4192bf --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/detach--error-no-session.txt @@ -0,0 +1,4 @@ + +🐛 Detach + +❌ Failed to detach debugger: No active debug session. Provide debugSessionId or attach first. diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/detach--success.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/detach--success.txt new file mode 100644 index 00000000..89a010f5 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/detach--success.txt @@ -0,0 +1,4 @@ + +🐛 Detach + +✅ Detached debugger session diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/lldb-command--error-no-session.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/lldb-command--error-no-session.txt new file mode 100644 index 00000000..74ba88dc --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/lldb-command--error-no-session.txt @@ -0,0 +1,6 @@ + +🐛 LLDB Command + + Command: bt + +❌ Failed to run LLDB command: No active debug session. Provide debugSessionId or attach first. diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/lldb-command--success.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/lldb-command--success.txt new file mode 100644 index 00000000..8a360697 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/lldb-command--success.txt @@ -0,0 +1,12 @@ + +🐛 LLDB Command + + Command: breakpoint list + +✅ Command executed + +Output: + Current breakpoints: + 1: file = 'ContentView.swift', line = 42, exact_match = 0, locations = + Names: + dap diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/remove-breakpoint--error-no-session.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/remove-breakpoint--error-no-session.txt new file mode 100644 index 00000000..83bd0130 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/remove-breakpoint--error-no-session.txt @@ -0,0 +1,4 @@ + +🐛 Remove Breakpoint + +❌ Failed to remove breakpoint: No active debug session. Provide debugSessionId or attach first. diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/remove-breakpoint--success.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/remove-breakpoint--success.txt new file mode 100644 index 00000000..4714ab85 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/remove-breakpoint--success.txt @@ -0,0 +1,7 @@ + +🐛 Remove Breakpoint + +✅ Breakpoint 1 removed + +Output: + Removed breakpoint 1. diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/stack--error-no-session.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/stack--error-no-session.txt new file mode 100644 index 00000000..72172a93 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/stack--error-no-session.txt @@ -0,0 +1,4 @@ + +🐛 Stack Trace + +❌ Failed to get stack: No active debug session. Provide debugSessionId or attach first. diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/stack--success.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/stack--success.txt new file mode 100644 index 00000000..37cc8867 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/stack--success.txt @@ -0,0 +1,11 @@ + +🐛 Stack Trace + +✅ Stack trace retrieved + +Frames: + Thread (Thread 1) + + frame #: static CalculatorApp.$main() at /Library/Developer/CoreSimulator/Devices//data/Containers/Bundle/Application//CalculatorApp.app/CalculatorApp.debug.dylib`static CalculatorApp.CalculatorApp.$main() -> (): + frame #: main at /Library/Developer/CoreSimulator/Devices//data/Containers/Bundle/Application//CalculatorApp.app/CalculatorApp.debug.dylib`main: + diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/variables--error-no-session.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/variables--error-no-session.txt new file mode 100644 index 00000000..e76aa900 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/variables--error-no-session.txt @@ -0,0 +1,4 @@ + +🐛 Variables + +❌ Failed to get variables: No active debug session. Provide debugSessionId or attach first. diff --git a/src/snapshot-tests/__fixtures__/mcp/debugging/variables--success.txt b/src/snapshot-tests/__fixtures__/mcp/debugging/variables--success.txt new file mode 100644 index 00000000..bf96d0fd --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/debugging/variables--success.txt @@ -0,0 +1,18 @@ + +🐛 Variables + +✅ Variables retrieved + +Values: + Locals: + (no variables) + + Globals: + (no variables) + + Registers: + General Purpose Registers () = + Floating Point Registers () = + Exception State Registers () = + Scalable Vector Extension Registers () = + Scalable Matrix Extension Registers () = diff --git a/src/snapshot-tests/__fixtures__/mcp/device/build--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/device/build--error-wrong-scheme.txt new file mode 100644 index 00000000..20da1b1c --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/build--error-wrong-scheme.txt @@ -0,0 +1,15 @@ + +🔨 Build + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_device__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/device/build--success.txt b/src/snapshot-tests/__fixtures__/mcp/device/build--success.txt new file mode 100644 index 00000000..c4481aa3 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/build--success.txt @@ -0,0 +1,14 @@ + +🔨 Build + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +✅ Build succeeded. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_device__pid.log + +Next steps: +1. Get built device app path: get_device_app_path({ scheme: "CalculatorApp" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/device/build-and-run--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/device/build-and-run--error-wrong-scheme.txt new file mode 100644 index 00000000..37a0c499 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/build-and-run--error-wrong-scheme.txt @@ -0,0 +1,16 @@ + +🚀 Build & Run + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + Device: () + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_run_device__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/device/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/mcp/device/build-and-run--success.txt new file mode 100644 index 00000000..68f7f3ad --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/build-and-run--success.txt @@ -0,0 +1,25 @@ + +🚀 Build & Run + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + Device: () + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +ℹ️ Resolving app path +✅ Resolving app path +ℹ️ Installing app +✅ Installing app +ℹ️ Launching app + +✅ Build succeeded. (⏱️ ) +✅ Build & Run complete + ├ App Path: /Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug-iphoneos/CalculatorApp.app + ├ Bundle ID: io.sentry.calculatorapp + ├ Process ID: + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_run_device__pid.log + +Next steps: +1. Stop app on device: stop_app_device({ deviceId: "", processId: }) diff --git a/src/snapshot-tests/__fixtures__/mcp/device/get-app-path--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/device/get-app-path--error-wrong-scheme.txt new file mode 100644 index 00000000..6817789b --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/get-app-path--error-wrong-scheme.txt @@ -0,0 +1,13 @@ + +🔍 Get App Path + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + +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. + +❌ Query failed. diff --git a/src/snapshot-tests/__fixtures__/mcp/device/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/mcp/device/get-app-path--success.txt new file mode 100644 index 00000000..84ff3230 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/get-app-path--success.txt @@ -0,0 +1,15 @@ + +🔍 Get App Path + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + +✅ Success + └ App Path: /Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug-iphoneos/CalculatorApp.app + +Next steps: +1. Get bundle ID: get_app_bundle_id({ appPath: "/Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug-iphoneos/CalculatorApp.app" }) +2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "/Library/Developer/XcodeBuildMCP/DerivedData/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/device/install--error-invalid-app.txt b/src/snapshot-tests/__fixtures__/mcp/device/install--error-invalid-app.txt new file mode 100644 index 00000000..53e5b6d8 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/install--error-invalid-app.txt @@ -0,0 +1,10 @@ + +📦 Install App + + Device: + App: /tmp/nonexistent.app + +❌ Failed to install app: Failed to load provisioning paramter list due to error: Error Domain=com.apple.dt.CoreDeviceError Code=1002 "No provider was found." UserInfo={NSLocalizedDescription=No provider was found.}. +`devicectl manage create` may support a reduced set of arguments. +ERROR: The specified device was not found. (Name: ) (com.apple.dt.CoreDeviceError error 1000 (0x3E8)) + DeviceName = diff --git a/src/snapshot-tests/__fixtures__/mcp/device/install--success.txt b/src/snapshot-tests/__fixtures__/mcp/device/install--success.txt new file mode 100644 index 00000000..c500efa9 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/install--success.txt @@ -0,0 +1,7 @@ + +📦 Install App + + Device: () + App: /Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug-iphoneos/CalculatorApp.app + +✅ App installed successfully. diff --git a/src/snapshot-tests/__fixtures__/mcp/device/launch--error-invalid-bundle.txt b/src/snapshot-tests/__fixtures__/mcp/device/launch--error-invalid-bundle.txt new file mode 100644 index 00000000..9ffa4ce5 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/launch--error-invalid-bundle.txt @@ -0,0 +1,10 @@ + +🚀 Launch App + + Device: + Bundle ID: com.nonexistent.app + +❌ Failed to launch app: Failed to load provisioning paramter list due to error: Error Domain=com.apple.dt.CoreDeviceError Code=1002 "No provider was found." UserInfo={NSLocalizedDescription=No provider was found.}. +`devicectl manage create` may support a reduced set of arguments. +ERROR: The specified device was not found. (Name: ) (com.apple.dt.CoreDeviceError error 1000 (0x3E8)) + DeviceName = diff --git a/src/snapshot-tests/__fixtures__/mcp/device/launch--success.txt b/src/snapshot-tests/__fixtures__/mcp/device/launch--success.txt new file mode 100644 index 00000000..a4887124 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/launch--success.txt @@ -0,0 +1,11 @@ + +🚀 Launch App + + Device: () + Bundle ID: io.sentry.calculatorapp + +✅ App launched successfully. + └ Process ID: + +Next steps: +1. Stop the app: stop_app_device({ deviceId: "", processId: }) diff --git a/src/snapshot-tests/__fixtures__/mcp/device/list--success.txt b/src/snapshot-tests/__fixtures__/mcp/device/list--success.txt new file mode 100644 index 00000000..609c38dd --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/list--success.txt @@ -0,0 +1,34 @@ + +📱 List Devices + +iOS Devices: + + 📱 [✓] Cameron’s iPhone 16 Pro Max + OS: 26.3.1 (a) + UDID: + + 📱 [✗] iPhone + OS: 26.1 + UDID: + +watchOS Devices: + + ⌚️ [✗] Cameron’s Apple Watch + OS: 10.6.1 + UDID: + + ⌚️ [✓] Cameron’s Apple Watch + OS: 26.3 + UDID: + +✅ 4 physical devices discovered (2 iOS, 2 watchOS). + +Hints + Use the device ID/UDID from above when required by other tools. + Save a default device with session-set-defaults { deviceId: 'DEVICE_UDID' }. + Before running build/run/test/UI automation tools, set the desired device identifier in session defaults. + +Next steps: +1. Build for device: build_device({ scheme: "YOUR_SCHEME", deviceId: "UUID_FROM_ABOVE" }) +2. Run tests on device: test_device() +3. Get app path: get_device_app_path() diff --git a/src/snapshot-tests/__fixtures__/mcp/device/stop--error-no-app.txt b/src/snapshot-tests/__fixtures__/mcp/device/stop--error-no-app.txt new file mode 100644 index 00000000..76a7f793 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/stop--error-no-app.txt @@ -0,0 +1,10 @@ + +🛑 Stop App + + Device: + PID: + +❌ Failed to stop app: Failed to load provisioning paramter list due to error: Error Domain=com.apple.dt.CoreDeviceError Code=1002 "No provider was found." UserInfo={NSLocalizedDescription=No provider was found.}. +`devicectl manage create` may support a reduced set of arguments. +ERROR: The specified device was not found. (Name: ) (com.apple.dt.CoreDeviceError error 1000 (0x3E8)) + DeviceName = diff --git a/src/snapshot-tests/__fixtures__/mcp/device/stop--success.txt b/src/snapshot-tests/__fixtures__/mcp/device/stop--success.txt new file mode 100644 index 00000000..2fa57755 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/stop--success.txt @@ -0,0 +1,7 @@ + +🛑 Stop App + + Device: () + PID: + +✅ App stopped successfully diff --git a/src/snapshot-tests/__fixtures__/mcp/device/test--failure.txt b/src/snapshot-tests/__fixtures__/mcp/device/test--failure.txt new file mode 100644 index 00000000..b3d3e4cc --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/test--failure.txt @@ -0,0 +1,30 @@ + +🧪 Test + + Scheme: CalculatorApp + Configuration: Debug + Platform: iOS + Device: () + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +Discovered 52 test(s): + CalculatorAppFeatureTests/CalculatorBasicTests/testClear + CalculatorAppFeatureTests/CalculatorBasicTests/testInitialState + CalculatorAppFeatureTests/CalculatorBasicTests/testIntentionalFailure + CalculatorAppFeatureTests/CalculatorIntegrationTests/testChainCalculations + CalculatorAppFeatureTests/CalculatorIntegrationTests/testComplexCalculation + CalculatorAppFeatureTests/CalculatorIntegrationTests/testExpressionDisplay + (...and 46 more) + +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 + +IntentionalFailureTests + ✗ test: + - XCTAssertTrue failed - This test should fail to verify error reporting + example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:286 + +❌ tests failed, passed, skipped (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_device__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/device/test--success.txt b/src/snapshot-tests/__fixtures__/mcp/device/test--success.txt new file mode 100644 index 00000000..fdd54e05 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/device/test--success.txt @@ -0,0 +1,16 @@ + +🧪 Test + + Scheme: CalculatorApp + Configuration: Debug + Platform: iOS + Device: () + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + Selective Testing: + CalculatorAppTests/CalculatorAppTests/testAddition + +Discovered 1 test(s): + CalculatorAppTests/CalculatorAppTests/testAddition + +✅ 1 test passed, 0 skipped (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_device__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/build--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/macos/build--error-wrong-scheme.txt new file mode 100644 index 00000000..b07b3c73 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/build--error-wrong-scheme.txt @@ -0,0 +1,15 @@ + +🔨 Build + + Scheme: NONEXISTENT + Project: example_projects/macOS/MCPTest.xcodeproj + Configuration: Debug + Platform: macOS + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/build--success.txt b/src/snapshot-tests/__fixtures__/mcp/macos/build--success.txt new file mode 100644 index 00000000..51abc264 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/build--success.txt @@ -0,0 +1,15 @@ + +🔨 Build + + Scheme: MCPTest + Project: example_projects/macOS/MCPTest.xcodeproj + Configuration: Debug + Platform: macOS + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +✅ Build succeeded. (⏱️ ) + ├ Bundle ID: io.sentry.MCPTest.macOS + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_macos__pid.log + +Next steps: +1. Get built macOS app path: get_mac_app_path({ scheme: "MCPTest" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/build-and-run--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/macos/build-and-run--error-wrong-scheme.txt new file mode 100644 index 00000000..a9e244cf --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/build-and-run--error-wrong-scheme.txt @@ -0,0 +1,15 @@ + +🚀 Build & Run + + Scheme: NONEXISTENT + Project: example_projects/macOS/MCPTest.xcodeproj + Configuration: Debug + Platform: macOS + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_run_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/mcp/macos/build-and-run--success.txt new file mode 100644 index 00000000..a5f031a5 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/build-and-run--success.txt @@ -0,0 +1,23 @@ + +🚀 Build & Run + + Scheme: MCPTest + Project: example_projects/macOS/MCPTest.xcodeproj + Configuration: Debug + Platform: macOS + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +ℹ️ Resolving app path +✅ Resolving app path +ℹ️ Launching app +✅ Launching app + +✅ Build succeeded. (⏱️ ) +✅ Build & Run complete + ├ App Path: /Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug/MCPTest.app + ├ Bundle ID: io.sentry.MCPTest.macOS + ├ Process ID: + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_run_macos__pid.log + +Next steps: +1. Interact with the launched app in the foreground diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/get-app-path--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/macos/get-app-path--error-wrong-scheme.txt new file mode 100644 index 00000000..0661e112 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/get-app-path--error-wrong-scheme.txt @@ -0,0 +1,13 @@ + +🔍 Get App Path + + Scheme: NONEXISTENT + Project: example_projects/macOS/MCPTest.xcodeproj + Configuration: Debug + Platform: macOS + +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. + +❌ Query failed. diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/mcp/macos/get-app-path--success.txt new file mode 100644 index 00000000..1dbb1e4f --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/get-app-path--success.txt @@ -0,0 +1,14 @@ + +🔍 Get App Path + + Scheme: MCPTest + Project: example_projects/macOS/MCPTest.xcodeproj + Configuration: Debug + Platform: macOS + +✅ Success + └ App Path: /Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug/MCPTest.app + +Next steps: +1. Get bundle ID: get_mac_bundle_id({ appPath: "/Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug/MCPTest.app" }) +2. Launch app: launch_mac_app({ appPath: "/Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug/MCPTest.app" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/get-macos-bundle-id--error-missing-app.txt b/src/snapshot-tests/__fixtures__/mcp/macos/get-macos-bundle-id--error-missing-app.txt new file mode 100644 index 00000000..8448129e --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/get-macos-bundle-id--error-missing-app.txt @@ -0,0 +1,6 @@ + +🔍 Get macOS Bundle ID + + App: /nonexistent/path/Fake.app + +❌ File not found: '/nonexistent/path/Fake.app'. Please check the path and try again. diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/get-macos-bundle-id--success.txt b/src/snapshot-tests/__fixtures__/mcp/macos/get-macos-bundle-id--success.txt new file mode 100644 index 00000000..3e596543 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/get-macos-bundle-id--success.txt @@ -0,0 +1,11 @@ + +🔍 Get macOS Bundle ID + + App: /BundleTest.app + +✅ Bundle ID + └ com.test.snapshot-macos + +Next steps: +1. Launch the app: launch_mac_app({ appPath: "/BundleTest.app" }) +2. Build again: build_macos({ scheme: "SCHEME_NAME" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/launch--error-invalid-app.txt b/src/snapshot-tests/__fixtures__/mcp/macos/launch--error-invalid-app.txt new file mode 100644 index 00000000..352508e4 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/launch--error-invalid-app.txt @@ -0,0 +1,6 @@ + +🚀 Launch macOS App + + App: /NonExistent.app + +❌ File not found: '/NonExistent.app'. Please check the path and try again. diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/launch--success.txt b/src/snapshot-tests/__fixtures__/mcp/macos/launch--success.txt new file mode 100644 index 00000000..4196864c --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/launch--success.txt @@ -0,0 +1,8 @@ + +🚀 Launch macOS App + + App: /Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug/MCPTest.app + +✅ App launched successfully + ├ Bundle ID: io.sentry.MCPTest.macOS + └ Process ID: diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/stop--error-no-app.txt b/src/snapshot-tests/__fixtures__/mcp/macos/stop--error-no-app.txt new file mode 100644 index 00000000..f5380129 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/stop--error-no-app.txt @@ -0,0 +1,6 @@ + +🛑 Stop macOS App + + App: PID 999999 + +❌ Stop macOS app operation failed: kill: 999999: No such process diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/stop--success.txt b/src/snapshot-tests/__fixtures__/mcp/macos/stop--success.txt new file mode 100644 index 00000000..dffe5e4e --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/stop--success.txt @@ -0,0 +1,6 @@ + +🛑 Stop macOS App + + App: MCPTest + +✅ App stopped successfully diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/test--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/macos/test--error-wrong-scheme.txt new file mode 100644 index 00000000..94fa73ba --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/test--error-wrong-scheme.txt @@ -0,0 +1,14 @@ + +🧪 Test + + Scheme: NONEXISTENT + Configuration: Debug + Platform: macOS + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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. + +❌ Test failed. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/test--failure.txt b/src/snapshot-tests/__fixtures__/mcp/macos/test--failure.txt new file mode 100644 index 00000000..7f876426 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/test--failure.txt @@ -0,0 +1,26 @@ + +🧪 Test + + Scheme: MCPTest + Configuration: Debug + Platform: macOS + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +Discovered 4 test(s): + MCPTestTests/MCPTestTests/appNameIsCorrect + MCPTestTests/MCPTestTests/deliberateFailure + MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect + MCPTestTests/MCPTestsXCTests/testDeliberateFailure + +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 + +❌ tests failed, passed, skipped (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/macos/test--success.txt b/src/snapshot-tests/__fixtures__/mcp/macos/test--success.txt new file mode 100644 index 00000000..384ca1bf --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/macos/test--success.txt @@ -0,0 +1,17 @@ + +🧪 Test + + Scheme: MCPTest + Configuration: Debug + Platform: macOS + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + Selective Testing: + MCPTestTests/MCPTestTests/appNameIsCorrect() + MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect + +Discovered 2 test(s): + MCPTestTests/MCPTestTests/appNameIsCorrect + MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect + +✅ 2 tests passed, 0 skipped (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_macos__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/project-discovery/discover-projs--error-invalid-root.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/discover-projs--error-invalid-root.txt new file mode 100644 index 00000000..b8c28324 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/discover-projs--error-invalid-root.txt @@ -0,0 +1,8 @@ + +🔍 Discover Projects + + Workspace root: /nonexistent/path/Fake.app + Scan path: /nonexistent/path + Max depth: 3 + +❌ Failed to access scan path: /nonexistent/path. Error: ENOENT: no such file or directory, stat '/nonexistent/path' diff --git a/src/snapshot-tests/__fixtures__/mcp/project-discovery/discover-projs--success.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/discover-projs--success.txt new file mode 100644 index 00000000..30ab25e4 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/discover-projs--success.txt @@ -0,0 +1,18 @@ + +🔍 Discover Projects + + Workspace root: + Scan path: + Max depth: 3 + +✅ Found 1 project and 1 workspace + +Projects: + example_projects/iOS_Calculator/CalculatorApp.xcodeproj + +Workspaces: + example_projects/iOS_Calculator/CalculatorApp.xcworkspace + +Next steps: +1. Save discovered project/workspace as session defaults: session_set_defaults() +2. Build and run once defaults are set: build_run_sim() diff --git a/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-app-bundle-id--error-missing-app.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-app-bundle-id--error-missing-app.txt new file mode 100644 index 00000000..8eb9031a --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-app-bundle-id--error-missing-app.txt @@ -0,0 +1,6 @@ + +🔍 Get Bundle ID + + App: /nonexistent/path/Fake.app + +❌ File not found: '/nonexistent/path/Fake.app'. Please check the path and try again. diff --git a/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-app-bundle-id--success.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-app-bundle-id--success.txt new file mode 100644 index 00000000..3824c1cb --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-app-bundle-id--success.txt @@ -0,0 +1,13 @@ + +🔍 Get Bundle ID + + App: /BundleTest.app + +✅ Bundle ID + └ com.test.snapshot + +Next steps: +1. Install on simulator: install_app_sim({ simulatorId: "SIMULATOR_UUID", appPath: "/BundleTest.app" }) +2. Launch on simulator: launch_app_sim({ simulatorId: "SIMULATOR_UUID", bundleId: "com.test.snapshot" }) +3. Install on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "/BundleTest.app" }) +4. Launch on device: launch_app_device({ deviceId: "DEVICE_UDID", bundleId: "com.test.snapshot" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-macos-bundle-id--error-missing-app.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-macos-bundle-id--error-missing-app.txt new file mode 100644 index 00000000..8448129e --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-macos-bundle-id--error-missing-app.txt @@ -0,0 +1,6 @@ + +🔍 Get macOS Bundle ID + + App: /nonexistent/path/Fake.app + +❌ File not found: '/nonexistent/path/Fake.app'. Please check the path and try again. diff --git a/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-macos-bundle-id--success.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-macos-bundle-id--success.txt new file mode 100644 index 00000000..c8a66f0e --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/get-macos-bundle-id--success.txt @@ -0,0 +1,11 @@ + +🔍 Get macOS Bundle ID + + App: /BundleTest.app + +✅ Bundle ID + └ com.test.snapshot + +Next steps: +1. Launch the app: launch_mac_app({ appPath: "/BundleTest.app" }) +2. Build again: build_macos({ scheme: "SCHEME_NAME" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/project-discovery/list-schemes--error-invalid-workspace.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/list-schemes--error-invalid-workspace.txt new file mode 100644 index 00000000..d9f29e7f --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/list-schemes--error-invalid-workspace.txt @@ -0,0 +1,7 @@ + +🔍 List Schemes + + Workspace: /nonexistent/path/Fake.xcworkspace + +❌ +xcodebuild: error: '/nonexistent/path/Fake.xcworkspace' does not exist. diff --git a/src/snapshot-tests/__fixtures__/mcp/project-discovery/list-schemes--success.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/list-schemes--success.txt new file mode 100644 index 00000000..c5240453 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/list-schemes--success.txt @@ -0,0 +1,16 @@ + +🔍 List Schemes + + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + +✅ Found 2 schemes + +Schemes: + CalculatorApp + CalculatorAppFeature + +Next steps: +1. Build for macOS: build_macos({ workspacePath: "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", scheme: "CalculatorApp" }) +2. Build and run on iOS Simulator (default for run intent): build_run_sim({ workspacePath: "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", scheme: "CalculatorApp", simulatorName: "iPhone 17" }) +3. Build for iOS Simulator (compile-only): build_sim({ workspacePath: "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", scheme: "CalculatorApp", simulatorName: "iPhone 17" }) +4. Show build settings: show_build_settings({ workspacePath: "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", scheme: "CalculatorApp" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/project-discovery/show-build-settings--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/show-build-settings--error-wrong-scheme.txt new file mode 100644 index 00000000..76f8c183 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/show-build-settings--error-wrong-scheme.txt @@ -0,0 +1,8 @@ + +🔍 Show Build Settings + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + +❌ +xcodebuild: error: 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/project-discovery/show-build-settings--success.txt b/src/snapshot-tests/__fixtures__/mcp/project-discovery/show-build-settings--success.txt new file mode 100644 index 00000000..2ad674a3 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-discovery/show-build-settings--success.txt @@ -0,0 +1,617 @@ + +🔍 Show Build Settings + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + +✅ Build settings retrieved + +Settings + Build settings for action build and target CalculatorApp: + ACTION = build + AD_HOC_CODE_SIGNING_ALLOWED = NO + AGGREGATE_TRACKED_DOMAINS = YES + ALLOW_BUILD_REQUEST_OVERRIDES = NO + ALLOW_TARGET_PLATFORM_SPECIALIZATION = NO + ALTERNATE_GROUP = staff + ALTERNATE_MODE = u+w,go-w,a+rX + ALTERNATE_OWNER = + ALTERNATIVE_DISTRIBUTION_WEB = NO + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO + ALWAYS_SEARCH_USER_PATHS = NO + ALWAYS_USE_SEPARATE_HEADERMAPS = NO + APPLICATION_EXTENSION_API_ONLY = NO + APPLY_RULES_IN_COPY_FILES = NO + APPLY_RULES_IN_COPY_HEADERS = NO + APP_SHORTCUTS_ENABLE_FLEXIBLE_MATCHING = YES + ARCHS = arm64 + ARCHS_BASE = arm64 + ARCHS_STANDARD = arm64 + ARCHS_STANDARD_32_64_BIT = armv7 arm64 + ARCHS_STANDARD_32_BIT = armv7 + ARCHS_STANDARD_64_BIT = arm64 + ARCHS_STANDARD_INCLUDING_64_BIT = arm64 + ARCHS_UNIVERSAL_IPHONE_OS = armv7 arm64 + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor + ASSETCATALOG_FILTER_FOR_DEVICE_MODEL = MacFamily20,1 + ASSETCATALOG_FILTER_FOR_DEVICE_OS_VERSION = 26.3 + ASSETCATALOG_FILTER_FOR_THINNING_DEVICE_CONFIGURATION = MacFamily20,1 + AUTOMATICALLY_MERGE_DEPENDENCIES = NO + AUTOMATION_APPLE_EVENTS = NO + AVAILABLE_PLATFORMS = android appletvos appletvsimulator driverkit freebsd iphoneos iphonesimulator linux macosx none openbsd qnx watchos watchsimulator webassembly xros xrsimulator + BUILD_ACTIVE_RESOURCES_ONLY = YES + BUILD_COMPONENTS = headers build + BUILD_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products + BUILD_LIBRARY_FOR_DISTRIBUTION = NO + BUILD_ONLY_KNOWN_LOCALIZATIONS = NO + BUILD_ROOT = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products + BUILD_STYLE = + BUILD_VARIANTS = normal + BUILT_PRODUCTS_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos + BUNDLE_CONTENTS_FOLDER_PATH_deep = Contents/ + BUNDLE_EXECUTABLE_FOLDER_NAME_deep = MacOS + BUNDLE_EXTENSIONS_FOLDER_PATH = Extensions + BUNDLE_FORMAT = shallow + BUNDLE_FRAMEWORKS_FOLDER_PATH = Frameworks + BUNDLE_PLUGINS_FOLDER_PATH = PlugIns + BUNDLE_PRIVATE_HEADERS_FOLDER_PATH = PrivateHeaders + BUNDLE_PUBLIC_HEADERS_FOLDER_PATH = Headers + CACHE_ROOT = /var/folders/_t/2njffz894t57qpp76v1sw__h0000gn/C/com.apple.DeveloperTools/26.4-17E192/Xcode + CCHROOT = /var/folders/_t/2njffz894t57qpp76v1sw__h0000gn/C/com.apple.DeveloperTools/26.4-17E192/Xcode + CHMOD = /bin/chmod + CHOWN = chown + CLANG_ANALYZER_NONNULL = YES + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE + CLANG_CACHE_FINE_GRAINED_OUTPUTS = YES + CLANG_COVERAGE_MAPPING = YES + CLANG_CXX_LANGUAGE_STANDARD = gnu++20 + CLANG_ENABLE_EXPLICIT_MODULES = YES + CLANG_ENABLE_MODULES = YES + CLANG_ENABLE_OBJC_ARC = YES + CLANG_ENABLE_OBJC_WEAK = YES + CLANG_MODULES_BUILD_SESSION_FILE = /Library/Developer/Xcode/DerivedData/ModuleCache.noindex/Session.modulevalidation + CLANG_PROFILE_DATA_DIRECTORY = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/ProfileData + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES + CLANG_WARN_BOOL_CONVERSION = YES + CLANG_WARN_COMMA = YES + CLANG_WARN_CONSTANT_CONVERSION = YES + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR + CLANG_WARN_DOCUMENTATION_COMMENTS = YES + CLANG_WARN_EMPTY_BODY = YES + CLANG_WARN_ENUM_CONVERSION = YES + CLANG_WARN_INFINITE_RECURSION = YES + CLANG_WARN_INT_CONVERSION = YES + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES + CLANG_WARN_STRICT_PROTOTYPES = YES + CLANG_WARN_SUSPICIOUS_MOVE = YES + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE + CLANG_WARN_UNREACHABLE_CODE = YES + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES + CLASS_FILE_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/JavaClasses + CLEAN_PRECOMPS = YES + CLONE_HEADERS = NO + CODESIGNING_FOLDER_PATH = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app + CODE_SIGNING_ALLOWED = YES + CODE_SIGNING_REQUIRED = YES + CODE_SIGN_CONTEXT_CLASS = XCiPhoneOSCodeSignContext + CODE_SIGN_IDENTITY = Apple Development + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES + CODE_SIGN_STYLE = Automatic + COLOR_DIAGNOSTICS = NO + COMBINE_HIDPI_IMAGES = NO + COMPILATION_CACHE_CAS_PATH = /Library/Developer/Xcode/DerivedData/CompilationCache.noindex + COMPILATION_CACHE_KEEP_CAS_DIRECTORY = YES + COMPILER_INDEX_STORE_ENABLE = Default + COMPOSITE_SDK_DIRS = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CompositeSDKs + COMPRESS_PNG_FILES = YES + CONFIGURATION = Debug + CONFIGURATION_BUILD_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos + CONFIGURATION_TEMP_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos + CONTENTS_FOLDER_PATH = CalculatorApp.app + CONTENTS_FOLDER_PATH_SHALLOW_BUNDLE_NO = CalculatorApp.app/Contents + CONTENTS_FOLDER_PATH_SHALLOW_BUNDLE_YES = CalculatorApp.app + COPYING_PRESERVES_HFS_DATA = NO + COPY_HEADERS_RUN_UNIFDEF = NO + COPY_PHASE_STRIP = NO + CORRESPONDING_SIMULATOR_PLATFORM_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneSimulator.platform + CORRESPONDING_SIMULATOR_PLATFORM_NAME = iphonesimulator + CORRESPONDING_SIMULATOR_SDK_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator26.4.sdk + CORRESPONDING_SIMULATOR_SDK_NAME = iphonesimulator26.4 + CP = /bin/cp + CREATE_INFOPLIST_SECTION_IN_BINARY = NO + CURRENT_ARCH = undefined_arch + CURRENT_PROJECT_VERSION = 1 + CURRENT_VARIANT = normal + DEAD_CODE_STRIPPING = YES + DEBUGGING_SYMBOLS = YES + DEBUG_INFORMATION_FORMAT = dwarf + DEBUG_INFORMATION_VERSION = compiler-default + DEFAULT_COMPILER = com.apple.compilers.llvm.clang.1_0 + DEFAULT_DEXT_INSTALL_PATH = /System/Library/DriverExtensions + DEFAULT_KEXT_INSTALL_PATH = /System/Library/Extensions + DEFINES_MODULE = NO + DEPLOYMENT_LOCATION = NO + DEPLOYMENT_POSTPROCESSING = NO + DEPLOYMENT_TARGET_SETTING_NAME = IPHONEOS_DEPLOYMENT_TARGET + DEPLOYMENT_TARGET_SUGGESTED_VALUES = 12.0 12.1 12.2 12.3 12.4 13.0 13.1 13.2 13.3 13.4 13.5 13.6 14.0 14.1 14.2 14.3 14.4 14.5 14.6 14.7 15.0 15.1 15.2 15.3 15.4 15.5 15.6 16.0 16.1 16.2 16.3 16.4 16.5 16.6 17.0 17.1 17.2 17.3 17.4 17.5 17.6 18.0 18.1 18.2 18.3 18.4 18.5 18.6 26.0 26.2 26.3 26.4 + DERIVED_FILES_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/DerivedSources + DERIVED_FILE_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/DerivedSources + DERIVED_SOURCES_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/DerivedSources + DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = NO + DEVELOPER_APPLICATIONS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Applications + DEVELOPER_BIN_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/usr/bin + DEVELOPER_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer + DEVELOPER_FRAMEWORKS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Library/Frameworks + DEVELOPER_FRAMEWORKS_DIR_QUOTED = /Applications/Xcode-26.4.0.app/Contents/Developer/Library/Frameworks + DEVELOPER_LIBRARY_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Library + DEVELOPER_SDK_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs + DEVELOPER_TOOLS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Tools + DEVELOPER_USR_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/usr + DEVELOPMENT_LANGUAGE = en + DEVELOPMENT_TEAM = BR6WD3M6ZD + DIAGNOSE_MISSING_TARGET_DEPENDENCIES = YES + DIFF = /usr/bin/diff + DOCUMENTATION_FOLDER_PATH = CalculatorApp.app/en.lproj/Documentation + DONT_GENERATE_INFOPLIST_FILE = NO + DRIVERKIT_DEPLOYMENT_TARGET = 25.4 + DSTROOT = /tmp/CalculatorApp.dst + DT_TOOLCHAIN_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain + DUMP_DEPENDENCIES = NO + DUMP_DEPENDENCIES_OUTPUT_PATH = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/CalculatorApp-BuildDependencyInfo.json + DWARF_DSYM_FILE_NAME = CalculatorApp.app.dSYM + DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT = NO + DWARF_DSYM_FOLDER_PATH = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos + DYNAMIC_LIBRARY_EXTENSION = dylib + EAGER_COMPILATION_ALLOW_SCRIPTS = YES + EAGER_LINKING = NO + EFFECTIVE_PLATFORM_NAME = -iphoneos + EFFECTIVE_SWIFT_VERSION = 5 + EMBEDDED_CONTENT_CONTAINS_SWIFT = NO + EMBEDDED_PROFILE_NAME = embedded.mobileprovision + EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO + ENABLE_APP_SANDBOX = NO + ENABLE_CODE_COVERAGE = YES + ENABLE_COHORT_ARCHS = NO + ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS = NO + ENABLE_C_BOUNDS_SAFETY = NO + ENABLE_DEBUG_DYLIB = YES + ENABLE_DEFAULT_HEADER_SEARCH_PATHS = YES + ENABLE_DEFAULT_SEARCH_PATHS = YES + ENABLE_ENHANCED_SECURITY = NO + ENABLE_HARDENED_RUNTIME = NO + ENABLE_HEADER_DEPENDENCIES = YES + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO + ENABLE_ON_DEMAND_RESOURCES = YES + ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO + ENABLE_POINTER_AUTHENTICATION = NO + ENABLE_PREVIEWS = YES + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO + ENABLE_RESOURCE_ACCESS_CALENDARS = NO + ENABLE_RESOURCE_ACCESS_CAMERA = NO + ENABLE_RESOURCE_ACCESS_CONTACTS = NO + ENABLE_RESOURCE_ACCESS_LOCATION = NO + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO + ENABLE_RESOURCE_ACCESS_PRINTING = NO + ENABLE_RESOURCE_ACCESS_USB = NO + ENABLE_SDK_IMPORTS = NO + ENABLE_SECURITY_COMPILER_WARNINGS = NO + ENABLE_STRICT_OBJC_MSGSEND = YES + ENABLE_TESTABILITY = YES + ENABLE_TESTING_SEARCH_PATHS = NO + ENABLE_THREAD_SANITIZER = NO + ENABLE_USER_SCRIPT_SANDBOXING = YES + ENFORCE_VALID_ARCHS = YES + ENTITLEMENTS_ALLOWED = YES + ENTITLEMENTS_DESTINATION = Signature + ENTITLEMENTS_REQUIRED = NO + EXCLUDED_INSTALLSRC_SUBDIRECTORY_PATTERNS = .DS_Store .svn .git .hg CVS + EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = *.nib *.lproj *.framework *.gch *.xcode* *.xcassets *.icon (*) .DS_Store CVS .svn .git .hg *.pbproj *.pbxproj + EXECUTABLES_FOLDER_PATH = CalculatorApp.app/Executables + EXECUTABLE_FOLDER_PATH = CalculatorApp.app + EXECUTABLE_FOLDER_PATH_SHALLOW_BUNDLE_NO = CalculatorApp.app/MacOS + EXECUTABLE_FOLDER_PATH_SHALLOW_BUNDLE_YES = CalculatorApp.app + EXECUTABLE_NAME = CalculatorApp + EXECUTABLE_PATH = CalculatorApp.app/CalculatorApp + EXTENSIONS_FOLDER_PATH = CalculatorApp.app/Extensions + FILE_LIST = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects/LinkFileList + FIXED_FILES_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/FixedFiles + FRAMEWORKS_FOLDER_PATH = CalculatorApp.app/Frameworks + FRAMEWORK_FLAG_PREFIX = -framework + FRAMEWORK_SEARCH_PATHS = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos + FRAMEWORK_VERSION = A + FULL_PRODUCT_NAME = CalculatorApp.app + FUSE_BUILD_PHASES = YES + FUSE_BUILD_SCRIPT_PHASES = NO + GCC3_VERSION = 3.3 + GCC_C_LANGUAGE_STANDARD = gnu17 + GCC_DYNAMIC_NO_PIC = NO + GCC_INLINES_ARE_PRIVATE_EXTERN = YES + GCC_NO_COMMON_BLOCKS = YES + GCC_OPTIMIZATION_LEVEL = 0 + GCC_PFE_FILE_C_DIALECTS = c objective-c c++ objective-c++ + GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 + GCC_SYMBOLS_PRIVATE_EXTERN = NO + GCC_THUMB_SUPPORT = YES + GCC_TREAT_WARNINGS_AS_ERRORS = NO + GCC_VERSION = com.apple.compilers.llvm.clang.1_0 + GCC_VERSION_IDENTIFIER = com_apple_compilers_llvm_clang_1_0 + GCC_WARN_64_TO_32_BIT_CONVERSION = YES + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR + GCC_WARN_UNDECLARED_SELECTOR = YES + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE + GCC_WARN_UNUSED_FUNCTION = YES + GCC_WARN_UNUSED_VARIABLE = YES + GENERATED_MODULEMAP_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/GeneratedModuleMaps-iphoneos + GENERATE_INFOPLIST_FILE = YES + GENERATE_INTERMEDIATE_TEXT_BASED_STUBS = YES + GENERATE_PKGINFO_FILE = YES + GENERATE_PRELINK_OBJECT_FILE = NO + GENERATE_PROFILING_CODE = NO + GENERATE_TEXT_BASED_STUBS = NO + GID = 20 + GROUP = staff + HEADERMAP_INCLUDES_FLAT_ENTRIES_FOR_TARGET_BEING_BUILT = YES + HEADERMAP_INCLUDES_FRAMEWORK_ENTRIES_FOR_ALL_PRODUCT_TYPES = YES + HEADERMAP_INCLUDES_FRAMEWORK_ENTRIES_FOR_TARGETS_NOT_BEING_BUILT = YES + HEADERMAP_INCLUDES_NONPUBLIC_NONPRIVATE_HEADERS = YES + HEADERMAP_INCLUDES_PROJECT_HEADERS = YES + HEADERMAP_USES_FRAMEWORK_PREFIX_ENTRIES = YES + HEADERMAP_USES_VFS = NO + HEADER_SEARCH_PATHS = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/include + HOME = + HOST_ARCH = arm64 + HOST_PLATFORM = macosx + ICONV = /usr/bin/iconv + IMPLICIT_DEPENDENCY_DOMAIN = default + INDEX_STORE_COMPRESS = NO + INDEX_STORE_ONLY_PROJECT_FILES = NO + INFOPLIST_ENABLE_CFBUNDLEICONS_MERGE = YES + INFOPLIST_EXPAND_BUILD_SETTINGS = YES + INFOPLIST_KEY_CFBundleDisplayName = Calculator + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES + INFOPLIST_KEY_UILaunchScreen_Generation = YES + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationLandscapeRight UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationPortrait + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationLandscapeRight UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown + INFOPLIST_OUTPUT_FORMAT = binary + INFOPLIST_PATH = CalculatorApp.app/Info.plist + INFOPLIST_PREPROCESS = NO + INFOSTRINGS_PATH = CalculatorApp.app/en.lproj/InfoPlist.strings + INLINE_PRIVATE_FRAMEWORKS = NO + INSTALLAPI_IGNORE_SKIP_INSTALL = YES + INSTALLHDRS_COPY_PHASE = NO + INSTALLHDRS_SCRIPT_PHASE = NO + INSTALL_DIR = /tmp/CalculatorApp.dst/Applications + INSTALL_GROUP = staff + INSTALL_MODE_FLAG = u+w,go-w,a+rX + INSTALL_OWNER = + INSTALL_PATH = /Applications + INSTALL_ROOT = /tmp/CalculatorApp.dst + IPHONEOS_DEPLOYMENT_TARGET = 17.0 + IS_UNOPTIMIZED_BUILD = YES + JAVAC_DEFAULT_FLAGS = -J-Xms64m -J-XX:NewSize=4M -J-Dfile.encoding=UTF8 + JAVA_APP_STUB = /System/Library/Frameworks/JavaVM.framework/Resources/MacOS/JavaApplicationStub + JAVA_ARCHIVE_CLASSES = YES + JAVA_ARCHIVE_TYPE = JAR + JAVA_COMPILER = /usr/bin/javac + JAVA_FOLDER_PATH = CalculatorApp.app/Java + JAVA_FRAMEWORK_RESOURCES_DIRS = Resources + JAVA_JAR_FLAGS = cv + JAVA_SOURCE_SUBDIR = . + JAVA_USE_DEPENDENCIES = YES + JAVA_ZIP_FLAGS = -urg + JIKES_DEFAULT_FLAGS = +E +OLDCSO + KASAN_CFLAGS_CLASSIC = -DKASAN=1 -DKASAN_CLASSIC=1 -fsanitize=address -mllvm -asan-globals-live-support -mllvm -asan-force-dynamic-shadow + KASAN_CFLAGS_TBI = -DKASAN=1 -DKASAN_TBI=1 -fsanitize=kernel-hwaddress -mllvm -hwasan-recover=0 -mllvm -hwasan-instrument-atomics=0 -mllvm -hwasan-instrument-stack=1 -mllvm -hwasan-generate-tags-with-calls=1 -mllvm -hwasan-instrument-with-calls=1 -mllvm -hwasan-use-short-granules=0 -mllvm -hwasan-memory-access-callback-prefix=__asan_ + KASAN_DEFAULT_CFLAGS = -DKASAN=1 -DKASAN_CLASSIC=1 -fsanitize=address -mllvm -asan-globals-live-support -mllvm -asan-force-dynamic-shadow + KEEP_PRIVATE_EXTERNS = NO + LD_DEPENDENCY_INFO_FILE = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal/undefined_arch/CalculatorApp_dependency_info.dat + LD_EXPORT_GLOBAL_SYMBOLS = YES + LD_EXPORT_SYMBOLS = YES + LD_GENERATE_MAP_FILE = NO + LD_MAP_FILE_PATH = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/CalculatorApp-LinkMap-normal-undefined_arch.txt + LD_NO_PIE = NO + LD_QUOTE_LINKER_ARGUMENTS_FOR_COMPILER_DRIVER = YES + LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks + LD_RUNPATH_SEARCH_PATHS_YES = @loader_path/../Frameworks + LD_SHARED_CACHE_ELIGIBLE = Automatic + LD_WARN_DUPLICATE_LIBRARIES = NO + LD_WARN_UNUSED_DYLIBS = NO + LEGACY_DEVELOPER_DIR = /Applications/Xcode-26.4.0.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer + LEX = lex + LIBRARY_DEXT_INSTALL_PATH = /Library/DriverExtensions + LIBRARY_FLAG_NOSPACE = YES + LIBRARY_FLAG_PREFIX = -l + LIBRARY_KEXT_INSTALL_PATH = /Library/Extensions + LIBRARY_SEARCH_PATHS = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos + LINKER_DISPLAYS_MANGLED_NAMES = NO + LINK_FILE_LIST_normal_arm64 = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal/arm64/CalculatorApp.LinkFileList + LINK_OBJC_RUNTIME = YES + LINK_WITH_STANDARD_LIBRARIES = YES + LLVM_TARGET_TRIPLE_OS_VERSION = ios17.0 + LLVM_TARGET_TRIPLE_VENDOR = apple + LM_AUX_CONST_METADATA_LIST_PATH_normal_arm64 = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal/arm64/CalculatorApp.SwiftConstValuesFileList + LOCALIZATION_EXPORT_SUPPORTED = YES + LOCALIZATION_PREFERS_STRING_CATALOGS = YES + LOCALIZED_RESOURCES_FOLDER_PATH = CalculatorApp.app/en.lproj + LOCALIZED_STRING_CODE_COMMENTS = NO + LOCALIZED_STRING_MACRO_NAMES = NSLocalizedString CFCopyLocalizedString + LOCALIZED_STRING_SWIFTUI_SUPPORT = YES + LOCAL_ADMIN_APPS_DIR = /Applications/Utilities + LOCAL_APPS_DIR = /Applications + LOCAL_DEVELOPER_DIR = /Library/Developer + LOCAL_LIBRARY_DIR = /Library + LOCROOT = /example_projects/iOS_Calculator + LOCSYMROOT = /example_projects/iOS_Calculator + MACH_O_TYPE = mh_execute + MACOSX_DEPLOYMENT_TARGET = 26.4 + MAC_OS_X_PRODUCT_BUILD_VERSION = 25D2128 + MAC_OS_X_VERSION_ACTUAL = 260301 + MAC_OS_X_VERSION_MAJOR = 260000 + MAC_OS_X_VERSION_MINOR = 260300 + MAKE_MERGEABLE = NO + MARKETING_VERSION = 1.0 + MERGEABLE_LIBRARY = NO + MERGED_BINARY_TYPE = none + MERGE_LINKED_LIBRARIES = NO + METAL_LIBRARY_FILE_BASE = default + METAL_LIBRARY_OUTPUT_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app + MODULES_FOLDER_PATH = CalculatorApp.app/Modules + MODULE_CACHE_DIR = /Library/Developer/Xcode/DerivedData/ModuleCache.noindex + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE + MTL_FAST_MATH = YES + NATIVE_ARCH = arm64 + NATIVE_ARCH_32_BIT = arm + NATIVE_ARCH_64_BIT = arm64 + NATIVE_ARCH_ACTUAL = arm64 + NO_COMMON = YES + OBJECT_FILE_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects + OBJECT_FILE_DIR_normal = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal + OBJROOT = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex + ONLY_ACTIVE_ARCH = YES + OS = MACOS + OSAC = /usr/bin/osacompile + PACKAGE_TYPE = com.apple.package-type.wrapper.application + PASCAL_STRINGS = YES + PATH = + PATH_PREFIXES_EXCLUDED_FROM_HEADER_DEPENDENCIES = /usr/include /usr/local/include /System/Library/Frameworks /System/Library/PrivateFrameworks /Applications/Xcode-26.4.0.app/Contents/Developer/Headers /Applications/Xcode-26.4.0.app/Contents/Developer/SDKs /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms + PBDEVELOPMENTPLIST_PATH = CalculatorApp.app/pbdevelopment.plist + PER_ARCH_MODULE_FILE_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal/undefined_arch + PER_ARCH_OBJECT_FILE_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal/undefined_arch + PER_VARIANT_OBJECT_FILE_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal + PKGINFO_FILE_PATH = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/PkgInfo + PKGINFO_PATH = CalculatorApp.app/PkgInfo + PLATFORM_DEVELOPER_APPLICATIONS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Applications + PLATFORM_DEVELOPER_BIN_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin + PLATFORM_DEVELOPER_LIBRARY_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library + PLATFORM_DEVELOPER_SDK_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs + PLATFORM_DEVELOPER_TOOLS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Tools + PLATFORM_DEVELOPER_USR_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr + PLATFORM_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform + PLATFORM_DISPLAY_NAME = iOS + PLATFORM_FAMILY_NAME = iOS + PLATFORM_NAME = iphoneos + PLATFORM_PREFERRED_ARCH = arm64 + PLATFORM_PRODUCT_BUILD_VERSION = 23E237 + PLATFORM_REQUIRES_SWIFT_AUTOLINK_EXTRACT = NO + PLATFORM_REQUIRES_SWIFT_MODULEWRAP = NO + PLATFORM_USES_DSYMS = YES + PLIST_FILE_OUTPUT_FORMAT = binary + PLUGINS_FOLDER_PATH = CalculatorApp.app/PlugIns + PRECOMPS_INCLUDE_HEADERS_FROM_BUILT_PRODUCTS_DIR = YES + PRECOMP_DESTINATION_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/PrefixHeaders + PRIVATE_HEADERS_FOLDER_PATH = CalculatorApp.app/PrivateHeaders + PROCESSED_INFOPLIST_PATH = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal/undefined_arch/Processed-Info.plist + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.calculatorapp + PRODUCT_BUNDLE_PACKAGE_TYPE = APPL + PRODUCT_DISPLAY_NAME = Calculator + PRODUCT_MODULE_NAME = CalculatorApp + PRODUCT_NAME = CalculatorApp + PRODUCT_SETTINGS_PATH = + PRODUCT_TYPE = com.apple.product-type.application + PROFILING_CODE = NO + PROJECT = CalculatorApp + PROJECT_DERIVED_FILE_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/DerivedSources + PROJECT_DIR = /example_projects/iOS_Calculator + PROJECT_FILE_PATH = /example_projects/iOS_Calculator/CalculatorApp.xcodeproj + PROJECT_GUID = 5f13bb9ad2ee840212986da3cd4b87b0 + PROJECT_NAME = CalculatorApp + PROJECT_TEMP_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build + PROJECT_TEMP_ROOT = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex + PROVISIONING_PROFILE_REQUIRED = YES + PROVISIONING_PROFILE_REQUIRED_YES_YES = YES + PROVISIONING_PROFILE_SUPPORTED = YES + PUBLIC_HEADERS_FOLDER_PATH = CalculatorApp.app/Headers + RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET = 15.0 + RECURSIVE_SEARCH_PATHS_FOLLOW_SYMLINKS = YES + REMOVE_CVS_FROM_RESOURCES = YES + REMOVE_GIT_FROM_RESOURCES = YES + REMOVE_HEADERS_FROM_EMBEDDED_BUNDLES = YES + REMOVE_HG_FROM_RESOURCES = YES + REMOVE_STATIC_EXECUTABLES_FROM_EMBEDDED_BUNDLES = YES + REMOVE_SVN_FROM_RESOURCES = YES + RESCHEDULE_INDEPENDENT_HEADERS_PHASES = YES + REZ_COLLECTOR_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/ResourceManagerResources + REZ_OBJECTS_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/ResourceManagerResources/Objects + REZ_SEARCH_PATHS = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos + RPATH_ORIGIN = @loader_path + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO + RUNTIME_EXCEPTION_ALLOW_JIT = NO + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO + SCANNING_PCM_KEEP_CACHE_DIRECTORY = YES + SCAN_ALL_SOURCE_FILES_FOR_INCLUDES = NO + SCRIPTS_FOLDER_PATH = CalculatorApp.app/Scripts + SDKROOT = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.4.sdk + SDK_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.4.sdk + SDK_DIR_iphoneos = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.4.sdk + SDK_DIR_iphoneos26_4 = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.4.sdk + SDK_NAME = iphoneos26.4 + SDK_NAMES = iphoneos26.4 + SDK_PRODUCT_BUILD_VERSION = 23E237 + SDK_STAT_CACHE_DIR = /Library/Developer/Xcode/DerivedData + SDK_STAT_CACHE_ENABLE = YES + SDK_STAT_CACHE_PATH = /Library/Developer/Xcode/DerivedData/SDKStatCaches.noindex/iphoneos26.4-23E237-c1e9a37d8fcda5dee89abd67dc927a23.sdkstatcache + SDK_VERSION = 26.4 + SDK_VERSION_ACTUAL = 260400 + SDK_VERSION_MAJOR = 260000 + SDK_VERSION_MINOR = 260400 + SED = /usr/bin/sed + SEPARATE_STRIP = NO + SEPARATE_SYMBOL_EDIT = NO + SET_DIR_MODE_OWNER_GROUP = YES + SET_FILE_MODE_OWNER_GROUP = NO + SHALLOW_BUNDLE = YES + SHALLOW_BUNDLE_TRIPLE = ios + SHALLOW_BUNDLE_ios_macabi = NO + SHALLOW_BUNDLE_macos = NO + SHARED_DERIVED_FILE_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/DerivedSources + SHARED_FRAMEWORKS_FOLDER_PATH = CalculatorApp.app/SharedFrameworks + SHARED_PRECOMPS_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/PrecompiledHeaders + SHARED_SUPPORT_FOLDER_PATH = CalculatorApp.app/SharedSupport + SKIP_INSTALL = NO + SKIP_MERGEABLE_LIBRARY_BUNDLE_HOOK = NO + SOURCE_ROOT = /example_projects/iOS_Calculator + SRCROOT = /example_projects/iOS_Calculator + STRINGSDATA_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal/undefined_arch + STRINGSDATA_ROOT = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build + STRINGS_FILE_INFOPLIST_RENAME = YES + STRINGS_FILE_OUTPUT_ENCODING = binary + STRING_CATALOG_GENERATE_SYMBOLS = NO + STRIP_BITCODE_FROM_COPIED_FILES = YES + STRIP_INSTALLED_PRODUCT = NO + STRIP_STYLE = all + STRIP_SWIFT_SYMBOLS = YES + SUPPORTED_DEVICE_FAMILIES = 1,2 + SUPPORTED_PLATFORMS = iphoneos iphonesimulator + SUPPORTS_MACCATALYST = NO + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES + SUPPORTS_ON_DEMAND_RESOURCES = YES + SUPPORTS_TEXT_BASED_API = NO + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES + SUPPRESS_WARNINGS = NO + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG + SWIFT_EMIT_CONST_VALUE_PROTOCOLS = AnyResolverProviding AppEntity AppEnum AppExtension AppIntent AppIntentsPackage AppShortcutProviding AppShortcutsProvider AppUnionValue AppUnionValueCasesProviding DynamicOptionsProvider EntityQuery ExtensionPointDefining IntentValueQuery Resolver TransientEntity _AssistantIntentsProvider _GenerativeFunctionExtractable _IntentValueRepresentable + SWIFT_EMIT_LOC_STRINGS = YES + SWIFT_ENABLE_EXPLICIT_MODULES = YES + SWIFT_OPTIMIZATION_LEVEL = -Onone + SWIFT_PLATFORM_TARGET_PREFIX = ios + SWIFT_RESPONSE_FILE_PATH_normal_arm64 = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build/Objects-normal/arm64/CalculatorApp.SwiftFileList + SWIFT_VERSION = 5.0 + SYMROOT = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products + SYSTEM_ADMIN_APPS_DIR = /Applications/Utilities + SYSTEM_APPS_DIR = /Applications + SYSTEM_CORE_SERVICES_DIR = /System/Library/CoreServices + SYSTEM_DEMOS_DIR = /Applications/Extras + SYSTEM_DEVELOPER_APPS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Applications + SYSTEM_DEVELOPER_BIN_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/usr/bin + SYSTEM_DEVELOPER_DEMOS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Applications/Utilities/Built Examples + SYSTEM_DEVELOPER_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer + SYSTEM_DEVELOPER_DOC_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/ADC Reference Library + SYSTEM_DEVELOPER_GRAPHICS_TOOLS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Applications/Graphics Tools + SYSTEM_DEVELOPER_JAVA_TOOLS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Applications/Java Tools + SYSTEM_DEVELOPER_PERFORMANCE_TOOLS_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Applications/Performance Tools + SYSTEM_DEVELOPER_RELEASENOTES_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/ADC Reference Library/releasenotes + SYSTEM_DEVELOPER_TOOLS = /Applications/Xcode-26.4.0.app/Contents/Developer/Tools + SYSTEM_DEVELOPER_TOOLS_DOC_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/ADC Reference Library/documentation/DeveloperTools + SYSTEM_DEVELOPER_TOOLS_RELEASENOTES_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/ADC Reference Library/releasenotes/DeveloperTools + SYSTEM_DEVELOPER_USR_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/usr + SYSTEM_DEVELOPER_UTILITIES_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Applications/Utilities + SYSTEM_DEXT_INSTALL_PATH = /System/Library/DriverExtensions + SYSTEM_DOCUMENTATION_DIR = /Library/Documentation + SYSTEM_EXTENSIONS_FOLDER_PATH = CalculatorApp.app/SystemExtensions + SYSTEM_EXTENSIONS_FOLDER_PATH_SHALLOW_BUNDLE_NO = CalculatorApp.app/Library/SystemExtensions + SYSTEM_EXTENSIONS_FOLDER_PATH_SHALLOW_BUNDLE_YES = CalculatorApp.app/SystemExtensions + SYSTEM_KEXT_INSTALL_PATH = /System/Library/Extensions + SYSTEM_LIBRARY_DIR = /System/Library + TAPI_DEMANGLE = YES + TAPI_ENABLE_PROJECT_HEADERS = NO + TAPI_LANGUAGE = objective-c + TAPI_LANGUAGE_STANDARD = compiler-default + TAPI_USE_SRCROOT = YES + TAPI_VERIFY_MODE = Pedantic + TARGETED_DEVICE_FAMILY = 1,2 + TARGETNAME = CalculatorApp + TARGET_BUILD_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos + TARGET_DEVICE_IDENTIFIER = + TARGET_DEVICE_MODEL = Mac16,8 + TARGET_DEVICE_OS_VERSION = 26.3.1 + TARGET_DEVICE_PLATFORM_NAME = macosx + TARGET_NAME = CalculatorApp + TARGET_TEMP_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build + TEMP_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build + TEMP_FILES_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build + TEMP_FILE_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/CalculatorApp.build/Debug-iphoneos/CalculatorApp.build + TEMP_ROOT = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex + TEMP_SANDBOX_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/TemporaryTaskSandboxes + TEST_FRAMEWORK_SEARCH_PATHS = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Frameworks /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.4.sdk/Developer/Library/Frameworks + TEST_LIBRARY_SEARCH_PATHS = /Applications/Xcode-26.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/lib + TOOLCHAINS = com.apple.dt.toolchain.XcodeDefault + TOOLCHAIN_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain + TREAT_MISSING_BASELINES_AS_TEST_FAILURES = NO + TREAT_MISSING_SCRIPT_PHASE_OUTPUTS_AS_ERRORS = NO + TVOS_DEPLOYMENT_TARGET = 26.4 + UID = + UNINSTALLED_PRODUCTS_DIR = /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Intermediates.noindex/UninstalledProducts + UNLOCALIZED_RESOURCES_FOLDER_PATH = CalculatorApp.app + UNLOCALIZED_RESOURCES_FOLDER_PATH_SHALLOW_BUNDLE_NO = CalculatorApp.app/Resources + UNLOCALIZED_RESOURCES_FOLDER_PATH_SHALLOW_BUNDLE_YES = CalculatorApp.app + UNSTRIPPED_PRODUCT = NO + USER = + USER_APPS_DIR = /Applications + USER_LIBRARY_DIR = /Library + USE_DYNAMIC_NO_PIC = YES + USE_HEADERMAP = YES + USE_HEADER_SYMLINKS = NO + VALIDATE_DEVELOPMENT_ASSET_PATHS = YES_ERROR + VALIDATE_PRODUCT = NO + VALID_ARCHS = arm64 arm64e armv7 armv7s + VERBOSE_PBXCP = NO + VERSIONPLIST_PATH = CalculatorApp.app/version.plist + VERSION_INFO_BUILDER = + VERSION_INFO_FILE = CalculatorApp_vers.c + VERSION_INFO_STRING = "@(#)PROGRAM:CalculatorApp PROJECT:CalculatorApp-1" + WATCHOS_DEPLOYMENT_TARGET = 26.4 + WORKSPACE_DIR = /example_projects/iOS_Calculator + WRAPPER_EXTENSION = app + WRAPPER_NAME = CalculatorApp.app + WRAPPER_SUFFIX = .app + WRAP_ASSET_PACKS_IN_SEPARATE_DIRECTORIES = NO + XCODE_APP_SUPPORT_DIR = /Applications/Xcode-26.4.0.app/Contents/Developer/Library/Xcode + XCODE_PRODUCT_BUILD_VERSION = 17E192 + XCODE_VERSION_ACTUAL = 2640 + XCODE_VERSION_MAJOR = 2600 + XCODE_VERSION_MINOR = 2640 + XPCSERVICES_FOLDER_PATH = CalculatorApp.app/XPCServices + XROS_DEPLOYMENT_TARGET = 26.4 + YACC = yacc + _DISCOVER_COMMAND_LINE_LINKER_INPUTS = YES + _DISCOVER_COMMAND_LINE_LINKER_INPUTS_INCLUDE_WL = YES + _LD_MULTIARCH = YES + _WRAPPER_CONTENTS_DIR_SHALLOW_BUNDLE_NO = /Contents + _WRAPPER_PARENT_PATH_SHALLOW_BUNDLE_NO = /.. + _WRAPPER_RESOURCES_DIR_SHALLOW_BUNDLE_NO = /Resources + __DIAGNOSE_DEPRECATED_ARCHS = YES + __IS_NOT_MACOS = YES + __IS_NOT_MACOS_macosx = NO + __IS_NOT_SIMULATOR = YES + __IS_NOT_SIMULATOR_simulator = NO + __ORIGINAL_SDK_DEFINED_LLVM_TARGET_TRIPLE_SYS = ios + arch = undefined_arch + variant = normal + +Next steps: +1. Build for macOS: build_macos({ workspacePath: "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", scheme: "CalculatorApp" }) +2. Build for iOS Simulator: build_sim({ workspacePath: "example_projects/iOS_Calculator/CalculatorApp.xcworkspace", scheme: "CalculatorApp", simulatorName: "iPhone 17" }) +3. List schemes: list_schemes({ workspacePath: "example_projects/iOS_Calculator/CalculatorApp.xcworkspace" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-ios--error-existing.txt b/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-ios--error-existing.txt new file mode 100644 index 00000000..c0f01f43 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-ios--error-existing.txt @@ -0,0 +1,8 @@ + +📝 Scaffold iOS Project + + Name: SnapshotTestApp + Path: /ios-existing + Platform: iOS + +❌ Xcode project files already exist in /ios-existing diff --git a/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-ios--success.txt b/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-ios--success.txt new file mode 100644 index 00000000..d705138b --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-ios--success.txt @@ -0,0 +1,14 @@ + +📝 Scaffold iOS Project + + Name: SnapshotTestApp + Path: /ios + Platform: iOS + +✅ Project scaffolded successfully + └ /ios + +Next steps: +1. Important: Before working on the project make sure to read the README.md file in the workspace root directory. +2. Build for simulator: build_sim({ workspacePath: "/ios/SnapshotTestApp.xcworkspace", scheme: "SnapshotTestApp", simulatorName: "iPhone 17" }) +3. Build and run on simulator: build_run_sim({ workspacePath: "/ios/SnapshotTestApp.xcworkspace", scheme: "SnapshotTestApp", simulatorName: "iPhone 17" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-macos--error-existing.txt b/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-macos--error-existing.txt new file mode 100644 index 00000000..4f1820af --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-macos--error-existing.txt @@ -0,0 +1,8 @@ + +📝 Scaffold macOS Project + + Name: SnapshotTestMacApp + Path: /macos-existing + Platform: macOS + +❌ Xcode project files already exist in /macos-existing diff --git a/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-macos--success.txt b/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-macos--success.txt new file mode 100644 index 00000000..087c46f0 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/project-scaffolding/scaffold-macos--success.txt @@ -0,0 +1,14 @@ + +📝 Scaffold macOS Project + + Name: SnapshotTestMacApp + Path: /macos + Platform: macOS + +✅ Project scaffolded successfully + └ /macos + +Next steps: +1. Important: Before working on the project make sure to read the README.md file in the workspace root directory. +2. Build for macOS: build_macos({ workspacePath: "/macos/SnapshotTestMacApp.xcworkspace", scheme: "SnapshotTestMacApp" }) +3. Build & Run on macOS: build_run_macos({ workspacePath: "/macos/SnapshotTestMacApp.xcworkspace", scheme: "SnapshotTestMacApp" }) diff --git a/src/snapshot-tests/__fixtures__/resources/devices--success.txt b/src/snapshot-tests/__fixtures__/mcp/resources/devices--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/resources/devices--success.txt rename to src/snapshot-tests/__fixtures__/mcp/resources/devices--success.txt diff --git a/src/snapshot-tests/__fixtures__/resources/doctor--success.txt b/src/snapshot-tests/__fixtures__/mcp/resources/doctor--success.txt similarity index 99% rename from src/snapshot-tests/__fixtures__/resources/doctor--success.txt rename to src/snapshot-tests/__fixtures__/mcp/resources/doctor--success.txt index b536948a..711e585e 100644 --- a/src/snapshot-tests/__fixtures__/resources/doctor--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/resources/doctor--success.txt @@ -122,4 +122,5 @@ Troubleshooting Tips If incremental build support is not available, install xcodemake (https://github.com/cameroncooke/xcodemake) and ensure it is executable and available in your PATH To enable xcodemake, set environment variable: export INCREMENTAL_BUILDS_ENABLED=1 For mise integration, follow instructions in the README.md file + ✅ Doctor diagnostics complete diff --git a/src/snapshot-tests/__fixtures__/resources/session-status--success.txt b/src/snapshot-tests/__fixtures__/mcp/resources/session-status--success.txt similarity index 88% rename from src/snapshot-tests/__fixtures__/resources/session-status--success.txt rename to src/snapshot-tests/__fixtures__/mcp/resources/session-status--success.txt index d5b696d8..2bb3bc45 100644 --- a/src/snapshot-tests/__fixtures__/resources/session-status--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/resources/session-status--success.txt @@ -1,7 +1,8 @@ { "logging": { "simulator": { - "activeSessionIds": [] + "activeSessionIds": [], + "activeLaunchOsLogSessions": [] }, "device": { "activeSessionIds": [] diff --git a/src/snapshot-tests/__fixtures__/resources/simulators--success.txt b/src/snapshot-tests/__fixtures__/mcp/resources/simulators--success.txt similarity index 83% rename from src/snapshot-tests/__fixtures__/resources/simulators--success.txt rename to src/snapshot-tests/__fixtures__/mcp/resources/simulators--success.txt index 2ba85f03..57299228 100644 --- a/src/snapshot-tests/__fixtures__/resources/simulators--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/resources/simulators--success.txt @@ -5,16 +5,16 @@ iOS Simulators: iOS 26.4: - 📱 [] iPhone 17 Pro (Shutdown) + 📱 [] iPhone 17 Pro (Booted) UDID: - 📱 [] iPhone 17 Pro Max (Shutdown) + 📱 [] iPhone 17 Pro Max (Booted) UDID: - 📱 [] iPhone 17e (Shutdown) + 📱 [] iPhone 17e (Booted) UDID: - 📱 [] iPhone Air (Shutdown) + 📱 [] iPhone Air (Booted) UDID: 📱 [] iPhone 17 (Booted) @@ -37,6 +37,7 @@ iOS Simulators: 📱 [] iPad (A16) (Shutdown) UDID: + ✅ 11 simulators available (11 iOS). Hints diff --git a/src/snapshot-tests/__fixtures__/session-management/session-clear-defaults--success.txt b/src/snapshot-tests/__fixtures__/mcp/session-management/session-clear-defaults--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/session-management/session-clear-defaults--success.txt rename to src/snapshot-tests/__fixtures__/mcp/session-management/session-clear-defaults--success.txt diff --git a/src/snapshot-tests/__fixtures__/mcp-integration/session-set-defaults--scheme.txt b/src/snapshot-tests/__fixtures__/mcp/session-management/session-set-defaults--scheme.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/mcp-integration/session-set-defaults--scheme.txt rename to src/snapshot-tests/__fixtures__/mcp/session-management/session-set-defaults--scheme.txt diff --git a/src/snapshot-tests/__fixtures__/session-management/session-set-defaults--success.txt b/src/snapshot-tests/__fixtures__/mcp/session-management/session-set-defaults--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/session-management/session-set-defaults--success.txt rename to src/snapshot-tests/__fixtures__/mcp/session-management/session-set-defaults--success.txt diff --git a/src/snapshot-tests/__fixtures__/mcp-integration/session-show-defaults--empty.txt b/src/snapshot-tests/__fixtures__/mcp/session-management/session-show-defaults--empty.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/mcp-integration/session-show-defaults--empty.txt rename to src/snapshot-tests/__fixtures__/mcp/session-management/session-show-defaults--empty.txt diff --git a/src/snapshot-tests/__fixtures__/session-management/session-show-defaults--success.txt b/src/snapshot-tests/__fixtures__/mcp/session-management/session-show-defaults--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/session-management/session-show-defaults--success.txt rename to src/snapshot-tests/__fixtures__/mcp/session-management/session-show-defaults--success.txt diff --git a/src/snapshot-tests/__fixtures__/session-management/session-sync-xcode-defaults--success.txt b/src/snapshot-tests/__fixtures__/mcp/session-management/session-sync-xcode-defaults--success.txt similarity index 85% rename from src/snapshot-tests/__fixtures__/session-management/session-sync-xcode-defaults--success.txt rename to src/snapshot-tests/__fixtures__/mcp/session-management/session-sync-xcode-defaults--success.txt index f3579f8c..4d87c486 100644 --- a/src/snapshot-tests/__fixtures__/session-management/session-sync-xcode-defaults--success.txt +++ b/src/snapshot-tests/__fixtures__/mcp/session-management/session-sync-xcode-defaults--success.txt @@ -3,5 +3,4 @@ ✅ Synced session defaults from Xcode IDE (default profile) ├ scheme: CalculatorApp - ├ simulatorId: └ bundleId: io.sentry.calculatorapp diff --git a/src/snapshot-tests/__fixtures__/session-management/session-use-defaults-profile--success.txt b/src/snapshot-tests/__fixtures__/mcp/session-management/session-use-defaults-profile--success.txt similarity index 100% rename from src/snapshot-tests/__fixtures__/session-management/session-use-defaults-profile--success.txt rename to src/snapshot-tests/__fixtures__/mcp/session-management/session-use-defaults-profile--success.txt diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/boot--error-invalid-id.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/boot--error-invalid-id.txt new file mode 100644 index 00000000..a732897c --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/boot--error-invalid-id.txt @@ -0,0 +1,6 @@ + +📱 Boot Simulator + + Simulator: + +❌ Boot simulator operation failed: Invalid device or device pair: diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/boot--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/boot--success.txt new file mode 100644 index 00000000..db86c72c --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/boot--success.txt @@ -0,0 +1,11 @@ + +📱 Boot Simulator + + Simulator: + +✅ Simulator booted successfully + +Next steps: +1. Open the Simulator app (makes it visible): open_sim() +2. Install an app: install_app_sim({ simulatorId: "", appPath: "PATH_TO_YOUR_APP" }) +3. Launch an app: launch_app_sim({ simulatorId: "", bundleId: "YOUR_APP_BUNDLE_ID" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/erase--error-invalid-id.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/erase--error-invalid-id.txt new file mode 100644 index 00000000..561f2bb2 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/erase--error-invalid-id.txt @@ -0,0 +1,6 @@ + +🗑 Erase Simulator + + Simulator: + +❌ Failed to erase simulator: Invalid device: diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/erase--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/erase--success.txt new file mode 100644 index 00000000..f8055402 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/erase--success.txt @@ -0,0 +1,6 @@ + +🗑 Erase Simulator + + Simulator: + +✅ Simulators were erased successfully diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/list--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/list--success.txt new file mode 100644 index 00000000..6e8eb467 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/list--success.txt @@ -0,0 +1,52 @@ + +📱 List Simulators + +iOS Simulators: + + iOS 26.4: + + 📱 [✓] iPhone 17 Pro (Booted) + UDID: + + 📱 [✓] iPhone 17 Pro Max (Booted) + UDID: + + 📱 [✓] iPhone 17e (Booted) + UDID: + + 📱 [✗] iPhone Air (Shutdown) + UDID: + + 📱 [✓] iPhone 17 (Booted) + UDID: + + 📱 [✗] iPad Pro 13-inch (M5) (Shutdown) + UDID: + + 📱 [✗] iPad Pro 11-inch (M5) (Shutdown) + UDID: + + 📱 [✗] iPad mini (A17 Pro) (Shutdown) + UDID: + + 📱 [✗] iPad Air 13-inch (M4) (Shutdown) + UDID: + + 📱 [✗] iPad Air 11-inch (M4) (Shutdown) + UDID: + + 📱 [✗] iPad (A16) (Shutdown) + UDID: + +✅ 11 simulators available (11 iOS). + +Hints + Use the simulator ID/UDID from above when required by other tools. + Save a default simulator with session-set-defaults { simulatorId: 'SIMULATOR_UDID' }. + Before running boot/build/run tools, set the desired simulator identifier in session defaults. + +Next steps: +1. Boot a simulator: boot_sim({ simulatorId: "UUID_FROM_ABOVE" }) +2. Open the simulator UI: open_sim() +3. Build for simulator: build_sim({ scheme: "YOUR_SCHEME", simulatorId: "UUID_FROM_ABOVE" }) +4. Get app path: get_sim_app_path({ scheme: "YOUR_SCHEME", platform: "iOS Simulator", simulatorId: "UUID_FROM_ABOVE" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/open--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/open--success.txt new file mode 100644 index 00000000..e4bc504b --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/open--success.txt @@ -0,0 +1,7 @@ + +📱 Open Simulator + +✅ Simulator opened successfully + +Next steps: +1. Boot a simulator for manual workflows: boot_sim({ simulatorId: "UUID_FROM_LIST_SIMS" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/reset-location--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/reset-location--error-invalid-simulator.txt new file mode 100644 index 00000000..429bddde --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/reset-location--error-invalid-simulator.txt @@ -0,0 +1,6 @@ + +📍 Reset Location + + Simulator: + +❌ Failed to reset simulator location: Invalid device: diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/reset-location--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/reset-location--success.txt new file mode 100644 index 00000000..0626956e --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/reset-location--success.txt @@ -0,0 +1,6 @@ + +📍 Reset Location + + Simulator: + +✅ Location successfully reset to default diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-appearance--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-appearance--error-invalid-simulator.txt new file mode 100644 index 00000000..7dd258fc --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-appearance--error-invalid-simulator.txt @@ -0,0 +1,7 @@ + +🎨 Set Appearance + + Simulator: + Mode: dark + +❌ Failed to set simulator appearance: Invalid device: diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-appearance--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-appearance--success.txt new file mode 100644 index 00000000..bd3a256c --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-appearance--success.txt @@ -0,0 +1,7 @@ + +🎨 Set Appearance + + Simulator: + Mode: dark + +✅ Appearance successfully set to dark mode diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-location--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-location--error-invalid-simulator.txt new file mode 100644 index 00000000..a1d6aace --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-location--error-invalid-simulator.txt @@ -0,0 +1,7 @@ + +📍 Set Location + + Simulator: + Coordinates: 37.7749,-122.4194 + +❌ Failed to set simulator location: Invalid device: diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-location--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-location--success.txt new file mode 100644 index 00000000..783dc9f2 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/set-location--success.txt @@ -0,0 +1,7 @@ + +📍 Set Location + + Simulator: + Coordinates: 37.7749,-122.4194 + +✅ Location set successfully diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/statusbar--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/statusbar--error-invalid-simulator.txt new file mode 100644 index 00000000..0c00f0cd --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/statusbar--error-invalid-simulator.txt @@ -0,0 +1,7 @@ + +📱 Statusbar + + Simulator: + Data Network: wifi + +❌ Failed to set status bar: Invalid device: diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator-management/statusbar--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator-management/statusbar--success.txt new file mode 100644 index 00000000..3d93eeaf --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator-management/statusbar--success.txt @@ -0,0 +1,7 @@ + +📱 Statusbar + + Simulator: + Data Network: wifi + +✅ Status bar data network set successfully diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/build--error-missing-params.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/build--error-missing-params.txt new file mode 100644 index 00000000..ff3584ae --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/build--error-missing-params.txt @@ -0,0 +1,10 @@ +MCP error -32602: Input validation error: Invalid arguments for tool build_sim: [ + { + "expected": "string", + "code": "invalid_type", + "path": [ + "scheme" + ], + "message": "Invalid input: expected string, received undefined" + } +] diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/build--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/build--error-wrong-scheme.txt new file mode 100644 index 00000000..de358b6d --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/build--error-wrong-scheme.txt @@ -0,0 +1,16 @@ + +🔨 Build + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_sim__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/build--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/build--success.txt new file mode 100644 index 00000000..2b0df214 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/build--success.txt @@ -0,0 +1,15 @@ + +🔨 Build + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +✅ Build succeeded. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_sim__pid.log + +Next steps: +1. Get built app path in simulator derived data: get_sim_app_path({ simulatorName: "iPhone 17", scheme: "CalculatorApp", platform: "iOS Simulator" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/build-and-run--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/build-and-run--error-wrong-scheme.txt new file mode 100644 index 00000000..13a8d794 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/build-and-run--error-wrong-scheme.txt @@ -0,0 +1,16 @@ + +🚀 Build & Run + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_run_sim__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/build-and-run--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/build-and-run--success.txt new file mode 100644 index 00000000..e06333b0 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/build-and-run--success.txt @@ -0,0 +1,29 @@ + +🚀 Build & Run + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +ℹ️ Resolving app path +✅ Resolving app path +ℹ️ Booting simulator +✅ Booting simulator +ℹ️ Installing app +✅ Installing app +ℹ️ Launching app + +✅ Build succeeded. (⏱️ ) +✅ Build & Run complete + ├ App Path: /Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug-iphonesimulator/CalculatorApp.app + ├ Bundle ID: io.sentry.calculatorapp + ├ Process ID: + ├ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_run_sim__pid.log + ├ Runtime Logs: /Library/Developer/XcodeBuildMCP/logs/io.sentry.calculatorapp__pid.log + └ OSLog: /Library/Developer/XcodeBuildMCP/logs/io.sentry.calculatorapp_oslog__pid.log + +Next steps: +1. Stop app in simulator: stop_app_sim({ simulatorId: "", bundleId: "io.sentry.calculatorapp" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/get-app-path--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/get-app-path--error-wrong-scheme.txt new file mode 100644 index 00000000..202d727e --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/get-app-path--error-wrong-scheme.txt @@ -0,0 +1,14 @@ + +🔍 Get App Path + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + +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. + +❌ Failed to get app path diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/get-app-path--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/get-app-path--success.txt new file mode 100644 index 00000000..80373630 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/get-app-path--success.txt @@ -0,0 +1,17 @@ + +🔍 Get App Path + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + +✅ Get app path successful (⏱️ ) + └ App Path: /Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug-iphonesimulator/CalculatorApp.app + +Next steps: +1. Get bundle ID: get_app_bundle_id({ appPath: "/Library/Developer/XcodeBuildMCP/DerivedData/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/DerivedData/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/simulator/install--error-invalid-app.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/install--error-invalid-app.txt new file mode 100644 index 00000000..ed46d6a9 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/install--error-invalid-app.txt @@ -0,0 +1,12 @@ + +📦 Install App + + Simulator: + App Path: /NotAnApp.app + +❌ Install app in simulator operation failed: An error was encountered processing the command (domain=IXErrorDomain, code=13): +Simulator device failed to install the application. +Missing bundle ID. +Underlying error (domain=IXErrorDomain, code=13): + Failed to get bundle ID from /NotAnApp.app + Missing bundle ID. diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/install--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/install--success.txt new file mode 100644 index 00000000..518afcf3 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/install--success.txt @@ -0,0 +1,11 @@ + +📦 Install App + + Simulator: + App Path: /Library/Developer/XcodeBuildMCP/DerivedData/Build/Products/Debug-iphonesimulator/CalculatorApp.app + +✅ App installed successfully + +Next steps: +1. Open the Simulator app: open_sim() +2. Launch the app: launch_app_sim({ simulatorId: "", bundleId: "io.sentry.calculatorapp" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/launch-app--error-not-installed.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/launch-app--error-not-installed.txt new file mode 100644 index 00000000..ebdb9854 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/launch-app--error-not-installed.txt @@ -0,0 +1,7 @@ + +🚀 Launch App + + Simulator: + Bundle ID: com.nonexistent.app + +❌ App is not installed on the simulator. Please use install_app_sim before launching. Workflow: build -> install -> launch. diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/launch-app--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/launch-app--success.txt new file mode 100644 index 00000000..4642de2b --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/launch-app--success.txt @@ -0,0 +1,14 @@ + +🚀 Launch App + + Simulator: + Bundle ID: io.sentry.calculatorapp + +✅ App launched successfully + ├ Process ID: + ├ Runtime Logs: /Library/Developer/XcodeBuildMCP/logs/io.sentry.calculatorapp__pid.log + └ OSLog: /Library/Developer/XcodeBuildMCP/logs/io.sentry.calculatorapp_oslog__pid.log + +Next steps: +1. Open Simulator app to see it: open_sim() +2. Stop app in simulator: stop_app_sim({ simulatorId: "", bundleId: "io.sentry.calculatorapp" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/list--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/list--success.txt new file mode 100644 index 00000000..8bdf245b --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/list--success.txt @@ -0,0 +1,52 @@ + +📱 List Simulators + +iOS Simulators: + + iOS 26.4: + + 📱 [✓] iPhone 17 Pro (Booted) + UDID: + + 📱 [✓] iPhone 17 Pro Max (Booted) + UDID: + + 📱 [✗] iPhone 17e (Shutdown) + UDID: + + 📱 [✗] iPhone Air (Shutdown) + UDID: + + 📱 [✓] iPhone 17 (Booted) + UDID: + + 📱 [✗] iPad Pro 13-inch (M5) (Shutdown) + UDID: + + 📱 [✗] iPad Pro 11-inch (M5) (Shutdown) + UDID: + + 📱 [✗] iPad mini (A17 Pro) (Shutdown) + UDID: + + 📱 [✗] iPad Air 13-inch (M4) (Shutdown) + UDID: + + 📱 [✗] iPad Air 11-inch (M4) (Shutdown) + UDID: + + 📱 [✗] iPad (A16) (Shutdown) + UDID: + +✅ 11 simulators available (11 iOS). + +Hints + Use the simulator ID/UDID from above when required by other tools. + Save a default simulator with session-set-defaults { simulatorId: 'SIMULATOR_UDID' }. + Before running boot/build/run tools, set the desired simulator identifier in session defaults. + +Next steps: +1. Boot a simulator: boot_sim({ simulatorId: "UUID_FROM_ABOVE" }) +2. Open the simulator UI: open_sim() +3. Build for simulator: build_sim({ scheme: "YOUR_SCHEME", simulatorId: "UUID_FROM_ABOVE" }) +4. Get app path: get_sim_app_path({ scheme: "YOUR_SCHEME", platform: "iOS Simulator", simulatorId: "UUID_FROM_ABOVE" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/screenshot--error-invalid-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/screenshot--error-invalid-simulator.txt new file mode 100644 index 00000000..66ce3224 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/screenshot--error-invalid-simulator.txt @@ -0,0 +1,6 @@ + +📷 Screenshot + + Simulator: + +❌ System error executing screenshot: Failed to capture screenshot: Invalid device: diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/screenshot--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/screenshot--success.txt new file mode 100644 index 00000000..4a990b95 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/screenshot--success.txt @@ -0,0 +1,9 @@ + +📷 Screenshot + + Simulator: + +✅ Screenshot captured + ├ Screenshot: + ├ Format: image/jpeg + └ Size: 368x800px diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/stop--error-no-app.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/stop--error-no-app.txt new file mode 100644 index 00000000..a6c6ba51 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/stop--error-no-app.txt @@ -0,0 +1,12 @@ + +🛑 Stop App + + Simulator: + Bundle ID: com.nonexistent.app + +❌ Stop app in simulator operation failed: An error was encountered processing the command (domain=NSPOSIXErrorDomain, code=3): +Simulator device failed to terminate com.nonexistent.app. +found nothing to terminate +Underlying error (domain=NSPOSIXErrorDomain, code=3): + The request to terminate "com.nonexistent.app" failed. found nothing to terminate + found nothing to terminate diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/stop--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/stop--success.txt new file mode 100644 index 00000000..855e0888 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/stop--success.txt @@ -0,0 +1,7 @@ + +🛑 Stop App + + Simulator: + Bundle ID: io.sentry.calculatorapp + +✅ App stopped successfully diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/test--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/test--error-wrong-scheme.txt new file mode 100644 index 00000000..50619e67 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/test--error-wrong-scheme.txt @@ -0,0 +1,15 @@ + +🧪 Test + + Scheme: NONEXISTENT + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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. + +❌ Test failed. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_sim__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/test--failure.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/test--failure.txt new file mode 100644 index 00000000..9db430ad --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/test--failure.txt @@ -0,0 +1,30 @@ + +🧪 Test + + Scheme: CalculatorApp + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +Discovered 52 test(s): + CalculatorAppFeatureTests/CalculatorBasicTests/testClear + CalculatorAppFeatureTests/CalculatorBasicTests/testInitialState + CalculatorAppFeatureTests/CalculatorBasicTests/testIntentionalFailure + CalculatorAppFeatureTests/CalculatorIntegrationTests/testChainCalculations + CalculatorAppFeatureTests/CalculatorIntegrationTests/testComplexCalculation + CalculatorAppFeatureTests/CalculatorIntegrationTests/testExpressionDisplay + (...and 46 more) + +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 + +IntentionalFailureTests + ✗ test: + - XCTAssertTrue failed - This test should fail to verify error reporting + example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:286 + +❌ tests failed, passed, skipped (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_sim__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/simulator/test--success.txt b/src/snapshot-tests/__fixtures__/mcp/simulator/test--success.txt new file mode 100644 index 00000000..d0fb32a1 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/simulator/test--success.txt @@ -0,0 +1,16 @@ + +🧪 Test + + Scheme: CalculatorApp + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + Selective Testing: + CalculatorAppTests/CalculatorAppTests/testAddition + +Discovered 1 test(s): + CalculatorAppTests/CalculatorAppTests/testAddition + +✅ 1 test passed, 0 skipped (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/test_sim__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/build--error-bad-path.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/build--error-bad-path.txt new file mode 100644 index 00000000..75cdb71c --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/build--error-bad-path.txt @@ -0,0 +1,11 @@ + +📦 Swift Package Build + + Package: /example_projects/NONEXISTENT + +Errors (1): + + ✗ chdir error: No such file or directory (2): /example_projects/NONEXISTENT + +❌ Build failed. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_spm__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/build--success.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/build--success.txt new file mode 100644 index 00000000..8d1af4d4 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/build--success.txt @@ -0,0 +1,7 @@ + +📦 Swift Package Build + + Package: /example_projects/spm + +✅ Build succeeded. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_spm__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/clean--error-bad-path.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/clean--error-bad-path.txt new file mode 100644 index 00000000..ee0fa6bf --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/clean--error-bad-path.txt @@ -0,0 +1,6 @@ + +🧹 Swift Package Clean + + Package: /example_projects/NONEXISTENT + +❌ Swift package clean failed: error: chdir error: No such file or directory (2): /example_projects/NONEXISTENT diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/clean--success.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/clean--success.txt new file mode 100644 index 00000000..a0e316a0 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/clean--success.txt @@ -0,0 +1,6 @@ + +🧹 Swift Package Clean + + Package: /example_projects/spm + +✅ Swift package cleaned successfully diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/list--no-processes.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/list--no-processes.txt new file mode 100644 index 00000000..3d744593 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/list--no-processes.txt @@ -0,0 +1,4 @@ + +📦 Swift Package Processes + +ℹ️ No Swift Package processes currently running. diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/list--success.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/list--success.txt new file mode 100644 index 00000000..17a38338 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/list--success.txt @@ -0,0 +1,8 @@ + +📦 Swift Package Processes + +Running Processes (1): + + 🟢 spm + PID: | Uptime: + Package: /example_projects/spm diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/run--error-bad-executable.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/run--error-bad-executable.txt new file mode 100644 index 00000000..04928338 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/run--error-bad-executable.txt @@ -0,0 +1,11 @@ + +🚀 Swift Package Run + + Package: /example_projects/spm + Executable: nonexistent-executable + +Errors (1): + + ✗ no executable product named 'nonexistent-executable' + +❌ Build failed. (⏱️ ) diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/run--success.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/run--success.txt new file mode 100644 index 00000000..8fee095e --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/run--success.txt @@ -0,0 +1,14 @@ + +🚀 Swift Package Run + + Package: /example_projects/spm + Executable: spm + +✅ Build succeeded. (⏱️ ) +✅ Build & Run complete + ├ App Path: example_projects/spm/.build/arm64-apple-macosx/debug/spm + ├ Process ID: + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/build_run_spm__pid.log + +Output + Hello, world! diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/stop--error-no-process.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/stop--error-no-process.txt new file mode 100644 index 00000000..4e59cb21 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/stop--error-no-process.txt @@ -0,0 +1,6 @@ + +🛑 Swift Package Stop + + PID: + +❌ No running process found with PID 999999. Use swift_package_list to check active processes. diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/test--error-bad-path.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/test--error-bad-path.txt new file mode 100644 index 00000000..e6b6b48b --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/test--error-bad-path.txt @@ -0,0 +1,14 @@ + +🧪 Swift Package Test + + Scheme: NONEXISTENT + Configuration: debug + Platform: Swift Package + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +Errors (1): + + ✗ chdir error: No such file or directory (2): /example_projects/NONEXISTENT + +❌ Test failed. (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/swift_package_test__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/test--failure.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/test--failure.txt new file mode 100644 index 00000000..a36e1525 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/test--failure.txt @@ -0,0 +1,21 @@ + +🧪 Swift Package Test + + Scheme: spm + Configuration: debug + Platform: Swift Package + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +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 + +❌ tests failed, passed, skipped (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/swift_package_test__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/swift-package/test--success.txt b/src/snapshot-tests/__fixtures__/mcp/swift-package/test--success.txt new file mode 100644 index 00000000..a5145e7a --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/swift-package/test--success.txt @@ -0,0 +1,10 @@ + +🧪 Swift Package Test + + Scheme: spm + Configuration: debug + Platform: Swift Package + Derived Data: /Library/Developer/XcodeBuildMCP/DerivedData + +✅ 1 test passed, 0 skipped (⏱️ ) + └ Build Logs: /Library/Developer/XcodeBuildMCP/logs/swift_package_test__pid.log diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/button--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/button--error-no-simulator.txt new file mode 100644 index 00000000..79190ab0 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/button--error-no-simulator.txt @@ -0,0 +1,9 @@ + +👆 Button + + Simulator: + +❌ Failed to press button 'home': axe command 'button' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/button--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/button--success.txt new file mode 100644 index 00000000..58a78296 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/button--success.txt @@ -0,0 +1,6 @@ + +👆 Button + + Simulator: + +✅ Hardware button 'home' pressed successfully. diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/gesture--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/gesture--error-no-simulator.txt new file mode 100644 index 00000000..3e391377 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/gesture--error-no-simulator.txt @@ -0,0 +1,9 @@ + +👆 Gesture + + Simulator: + +❌ Failed to execute gesture 'scroll-down': axe command 'gesture' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/gesture--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/gesture--success.txt new file mode 100644 index 00000000..f7cbf673 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/gesture--success.txt @@ -0,0 +1,6 @@ + +👆 Gesture + + Simulator: + +✅ Gesture 'scroll-down' executed successfully. diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-press--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-press--error-no-simulator.txt new file mode 100644 index 00000000..3be3e3c9 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-press--error-no-simulator.txt @@ -0,0 +1,9 @@ + +⌨️ Key Press + + Simulator: + +❌ Failed to simulate key press (code: 4): axe command 'key' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-press--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-press--success.txt new file mode 100644 index 00000000..c687f6b6 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-press--success.txt @@ -0,0 +1,6 @@ + +⌨️ Key Press + + Simulator: + +✅ Key press (code: 4) simulated successfully. diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-sequence--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-sequence--error-no-simulator.txt new file mode 100644 index 00000000..3b886b57 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-sequence--error-no-simulator.txt @@ -0,0 +1,9 @@ + +⌨️ Key Sequence + + Simulator: + +❌ Failed to execute key sequence: axe command 'key-sequence' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-sequence--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-sequence--success.txt new file mode 100644 index 00000000..6950454c --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/key-sequence--success.txt @@ -0,0 +1,6 @@ + +⌨️ Key Sequence + + Simulator: + +✅ Key sequence [4,5,6] executed successfully. diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/long-press--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/long-press--error-no-simulator.txt new file mode 100644 index 00000000..006987ff --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/long-press--error-no-simulator.txt @@ -0,0 +1,9 @@ + +👆 Long Press + + Simulator: + +❌ Failed to simulate long press at (100, 400): axe command 'touch' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/long-press--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/long-press--success.txt new file mode 100644 index 00000000..901c2463 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/long-press--success.txt @@ -0,0 +1,8 @@ + +👆 Long Press + + Simulator: + +✅ Long press at (100, 400) for 500ms simulated successfully. + +⚠️ Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots. diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/snapshot-ui--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/snapshot-ui--error-no-simulator.txt new file mode 100644 index 00000000..f55fe099 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/snapshot-ui--error-no-simulator.txt @@ -0,0 +1,9 @@ + +📷 Snapshot UI + + Simulator: + +❌ Failed to get accessibility hierarchy: axe command 'describe-ui' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/snapshot-ui--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/snapshot-ui--success.txt new file mode 100644 index 00000000..93249bed --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/snapshot-ui--success.txt @@ -0,0 +1,336 @@ + +📷 Snapshot UI + + Simulator: + +✅ Accessibility hierarchy retrieved successfully. + +Accessibility Hierarchy + ```json + [ + { + "AXFrame" : "{{0, 0}, {402, 874}}", + "AXUniqueId" : null, + "frame" : { + "y" : 0, + "x" : 0, + "width" : 402, + "height" : 874 + }, + "role_description" : "application", + "AXLabel" : " ", + "content_required" : false, + "type" : "Application", + "title" : null, + "help" : null, + "custom_actions" : [ + + ], + "AXValue" : null, + "enabled" : true, + "role" : "AXApplication", + "children" : [ + { + "AXFrame" : "{{28.333333333333371, 88}, {68, 90.666666666666657}}", + "AXUniqueId" : "Fitness", + "frame" : { + "y" : 88, + "x" : 28.3, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Fitness", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{120.66666666666674, 88}, {68, 90.666666666666657}}", + "AXUniqueId" : "Watch", + "frame" : { + "y" : 88, + "x" : 120.7, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Watch", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{211.33333333333326, 88}, {72.333333333333314, 90.666666666666657}}", + "AXUniqueId" : "Contacts", + "frame" : { + "y" : 88, + "x" : 211.3, + "width" : 72.3, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Contacts", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{306, 88}, {68, 90.666666666666657}}", + "AXUniqueId" : "Files", + "frame" : { + "y" : 88, + "x" : 306, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Files", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{28.333333333333371, 188.33333333333334}, {68, 90.666666666666714}}", + "AXUniqueId" : "Preview", + "frame" : { + "y" : 188.3, + "x" : 28.3, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Preview", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{120.66666666666674, 188.33333333333334}, {68, 90.666666666666714}}", + "AXUniqueId" : "Utilities", + "frame" : { + "y" : 188.3, + "x" : 120.7, + "width" : 68, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Utilities folder", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "2 apps", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{207.66666666666663, 188.33333333333334}, {79.333333333333314, 90.666666666666714}}", + "AXUniqueId" : "Calculator", + "frame" : { + "y" : 188.3, + "x" : 207.7, + "width" : 79.3, + "height" : 90.7 + }, + "role_description" : "button", + "AXLabel" : "Calculator", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{162, 703.66666666666674}, {78, 30}}", + "AXUniqueId" : "spotlight-pill", + "frame" : { + "y" : 703.7, + "x" : 162, + "width" : 78, + "height" : 30 + }, + "role_description" : "slider", + "AXLabel" : "Search", + "content_required" : false, + "type" : "Slider", + "title" : null, + "help" : "Activate to open Spotlight.", + "custom_actions" : [ + + ], + "AXValue" : "Page 2 of 2", + "enabled" : true, + "role" : "AXSlider", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{123, 771.33333333333326}, {68, 68}}", + "AXUniqueId" : "Safari", + "frame" : { + "y" : 771.3, + "x" : 123, + "width" : 68, + "height" : 68 + }, + "role_description" : "button", + "AXLabel" : "Safari", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + }, + { + "AXFrame" : "{{211, 771.33333333333326}, {68, 68}}", + "AXUniqueId" : "Messages", + "frame" : { + "y" : 771.3, + "x" : 211, + "width" : 68, + "height" : 68 + }, + "role_description" : "button", + "AXLabel" : "Messages", + "content_required" : false, + "type" : "Button", + "title" : null, + "help" : "Double-tap to open", + "custom_actions" : [ + "Edit mode", + "Today", + "App Library" + ], + "AXValue" : "", + "enabled" : true, + "role" : "AXButton", + "children" : [ + + ], + "subrole" : null, + "pid" : + } + ], + "subrole" : null, + "pid" : + } +] + ``` + +Tips + - Use frame coordinates for tap/swipe (center: x+width/2, y+height/2) + - If a debugger is attached, ensure the app is running (not stopped on breakpoints) + - Screenshots are for visual verification only + +Next steps: +1. Refresh after layout changes: snapshot_ui({ simulatorId: "" }) +2. Tap on element: tap({ simulatorId: "", x: 0, y: 0 }) +3. Take screenshot for verification: screenshot({ simulatorId: "" }) diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/swipe--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/swipe--error-no-simulator.txt new file mode 100644 index 00000000..c7d6bf08 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/swipe--error-no-simulator.txt @@ -0,0 +1,9 @@ + +👆 Swipe + + Simulator: + +❌ Failed to simulate swipe: axe command 'swipe' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/swipe--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/swipe--success.txt new file mode 100644 index 00000000..bae0cfea --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/swipe--success.txt @@ -0,0 +1,8 @@ + +👆 Swipe + + Simulator: + +✅ Swipe from (200, 400) to (200, 200) simulated successfully. + +⚠️ Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots. diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/tap--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/tap--error-no-simulator.txt new file mode 100644 index 00000000..62a48049 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/tap--error-no-simulator.txt @@ -0,0 +1,9 @@ + +👆 Tap + + Simulator: + +❌ Failed to simulate tap at (100, 100): axe command 'tap' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/tap--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/tap--success.txt new file mode 100644 index 00000000..a3a27d96 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/tap--success.txt @@ -0,0 +1,8 @@ + +👆 Tap + + Simulator: + +✅ Tap at (100, 400) simulated successfully. + +⚠️ Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots. diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/touch--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/touch--error-no-simulator.txt new file mode 100644 index 00000000..b9607fd0 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/touch--error-no-simulator.txt @@ -0,0 +1,9 @@ + +👆 Touch + + Simulator: + +❌ Failed to execute touch event: axe command 'touch' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/touch--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/touch--success.txt new file mode 100644 index 00000000..d29ac812 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/touch--success.txt @@ -0,0 +1,8 @@ + +👆 Touch + + Simulator: + +✅ Touch event (touch down+up) at (100, 400) executed successfully. + +⚠️ Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots. diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/type-text--error-no-simulator.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/type-text--error-no-simulator.txt new file mode 100644 index 00000000..7d305290 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/type-text--error-no-simulator.txt @@ -0,0 +1,9 @@ + +⌨️ Type Text + + Simulator: + +❌ Failed to simulate text typing: axe command 'type' failed. + +Details + Error: CLIError(errorDescription: "Simulator with UDID not found in set.") diff --git a/src/snapshot-tests/__fixtures__/mcp/ui-automation/type-text--success.txt b/src/snapshot-tests/__fixtures__/mcp/ui-automation/type-text--success.txt new file mode 100644 index 00000000..72a6ac50 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/ui-automation/type-text--success.txt @@ -0,0 +1,6 @@ + +⌨️ Type Text + + Simulator: + +✅ Text typing simulated successfully. diff --git a/src/snapshot-tests/__fixtures__/mcp/utilities/clean--error-wrong-scheme.txt b/src/snapshot-tests/__fixtures__/mcp/utilities/clean--error-wrong-scheme.txt new file mode 100644 index 00000000..5bb17c0f --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/utilities/clean--error-wrong-scheme.txt @@ -0,0 +1,9 @@ + +🧹 Clean + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + +❌ Clean failed: xcodebuild: error: 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/utilities/clean--success.txt b/src/snapshot-tests/__fixtures__/mcp/utilities/clean--success.txt new file mode 100644 index 00000000..3e0c7025 --- /dev/null +++ b/src/snapshot-tests/__fixtures__/mcp/utilities/clean--success.txt @@ -0,0 +1,9 @@ + +🧹 Clean + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + +✅ Clean successful diff --git a/src/snapshot-tests/__fixtures__/simulator-management/boot--success.txt b/src/snapshot-tests/__fixtures__/simulator-management/boot--success.txt deleted file mode 100644 index a0fd81d1..00000000 --- a/src/snapshot-tests/__fixtures__/simulator-management/boot--success.txt +++ /dev/null @@ -1,6 +0,0 @@ - -📱 Boot Simulator - - Simulator: - -✅ Simulator booted successfully diff --git a/src/snapshot-tests/__fixtures__/ui-automation/snapshot-ui--success.txt b/src/snapshot-tests/__fixtures__/ui-automation/snapshot-ui--success.txt deleted file mode 100644 index feb40b50..00000000 --- a/src/snapshot-tests/__fixtures__/ui-automation/snapshot-ui--success.txt +++ /dev/null @@ -1,588 +0,0 @@ - -📷 Snapshot UI - - Simulator: - -✅ Accessibility hierarchy retrieved successfully. - -Accessibility Hierarchy - ```json - [ - { - "AXFrame" : "{{0, 0}, {402, 874}}", - "AXUniqueId" : null, - "frame" : { - "y" : 0, - "x" : 0, - "width" : 402, - "height" : 874 - }, - "role_description" : "application", - "AXLabel" : "Calculator", - "content_required" : false, - "type" : "Application", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXApplication", - "children" : [ - { - "AXFrame" : "{{344, 250.5}, {34, 67}}", - "AXUniqueId" : null, - "frame" : { - "y" : 250.5, - "x" : 344, - "width" : 34, - "height" : 67 - }, - "role_description" : "text", - "AXLabel" : "0", - "content_required" : false, - "type" : "StaticText", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXStaticText", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{19.5, 357.5}, {82.666664123535156, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 357.5, - "x" : 19.5, - "width" : 82.7, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "C", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{113.16666412353516, 357.5}, {82.333335876464844, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 357.5, - "x" : 113.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "±", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{206.5, 357.5}, {82.666656494140625, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 357.5, - "x" : 206.5, - "width" : 82.7, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "%", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{300.16665649414062, 357.5}, {82.333343505859375, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 357.5, - "x" : 300.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "÷", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{19.5, 449.5}, {82.666664123535156, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 449.5, - "x" : 19.5, - "width" : 82.7, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "7", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{113.16666412353516, 449.5}, {82.333335876464844, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 449.5, - "x" : 113.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "8", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{206.5, 449.5}, {82.666656494140625, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 449.5, - "x" : 206.5, - "width" : 82.7, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "9", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{300.16665649414062, 449.5}, {82.333343505859375, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 449.5, - "x" : 300.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "×", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{19.5, 541.5}, {82.666664123535156, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 541.5, - "x" : 19.5, - "width" : 82.7, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "4", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{113.16666412353516, 541.5}, {82.333335876464844, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 541.5, - "x" : 113.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "5", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{206.5, 541.5}, {82.666656494140625, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 541.5, - "x" : 206.5, - "width" : 82.7, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "6", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{300.16665649414062, 541.5}, {82.333343505859375, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 541.5, - "x" : 300.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "-", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{19.5, 633.5}, {82.666664123535156, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 633.5, - "x" : 19.5, - "width" : 82.7, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "1", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{113.16666412353516, 633.5}, {82.333335876464844, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 633.5, - "x" : 113.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "2", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{206.5, 633.5}, {82.666656494140625, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 633.5, - "x" : 206.5, - "width" : 82.7, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "3", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{300.16665649414062, 633.5}, {82.333343505859375, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 633.5, - "x" : 300.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "+", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{113.16666412353516, 725.5}, {82.333335876464844, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 725.5, - "x" : 113.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "0", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{206.5, 725.5}, {82.666656494140625, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 725.5, - "x" : 206.5, - "width" : 82.7, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : ".", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - }, - { - "AXFrame" : "{{300.16665649414062, 725.5}, {82.333343505859375, 81}}", - "AXUniqueId" : null, - "frame" : { - "y" : 725.5, - "x" : 300.2, - "width" : 82.3, - "height" : 81 - }, - "role_description" : "button", - "AXLabel" : "=", - "content_required" : false, - "type" : "Button", - "title" : null, - "help" : null, - "custom_actions" : [ - - ], - "AXValue" : null, - "enabled" : true, - "role" : "AXButton", - "children" : [ - - ], - "subrole" : null, - "pid" : - } - ], - "subrole" : null, - "pid" : - } -] - ``` - -Tips - - Use frame coordinates for tap/swipe (center: x+width/2, y+height/2) - - If a debugger is attached, ensure the app is running (not stopped on breakpoints) - - Screenshots are for visual verification only - -Next steps: -1. Refresh after layout changes: xcodebuildmcp simulator snapshot-ui --simulator-id "" -2. Tap on element: xcodebuildmcp ui-automation tap --simulator-id "" --x "0" --y "0" -3. Take screenshot for verification: xcodebuildmcp simulator screenshot --simulator-id "" diff --git a/src/snapshot-tests/__tests__/coverage.snapshot.test.ts b/src/snapshot-tests/__tests__/coverage.snapshot.test.ts index 5a471139..89c7c9a9 100644 --- a/src/snapshot-tests/__tests__/coverage.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/coverage.snapshot.test.ts @@ -1,107 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { execSync } from 'node:child_process'; -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { createSnapshotHarness, ensureSimulatorBooted } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; +import { registerCoverageSnapshotSuite } from '../suites/coverage-suite.ts'; -const WORKSPACE = 'example_projects/iOS_Calculator/CalculatorApp.xcworkspace'; - -describe('coverage workflow', () => { - let harness: SnapshotHarness; - let xcresultPath: string; - let invalidXcresultPath: string; - - beforeAll(async () => { - vi.setConfig({ testTimeout: 120_000 }); - harness = await createSnapshotHarness(); - await ensureSimulatorBooted('iPhone 17'); - - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'coverage-snapshot-')); - xcresultPath = path.join(tmpDir, 'TestResults.xcresult'); - const derivedDataPath = path.join(tmpDir, 'DerivedData'); - - // Create a fake .xcresult directory that passes file-exists validation - // but makes xcrun xccov fail with a real executable error - invalidXcresultPath = path.join(tmpDir, 'invalid.xcresult'); - fs.mkdirSync(invalidXcresultPath); - - // Uses a fresh derived data path to ensure a fully clean build so coverage - // targets are deterministic. The Calculator example app has an intentionally - // failing test, so xcodebuild exits non-zero but the xcresult is still produced. - try { - execSync( - [ - 'xcodebuild test', - `-workspace ${WORKSPACE}`, - '-scheme CalculatorApp', - "-destination 'platform=iOS Simulator,name=iPhone 17'", - '-enableCodeCoverage YES', - `-derivedDataPath ${derivedDataPath}`, - `-resultBundlePath ${xcresultPath}`, - '-quiet', - ].join(' '), - { encoding: 'utf8', timeout: 120_000, stdio: 'pipe' }, - ); - } catch { - // Expected: test suite has an intentional failure - } - - if (!fs.existsSync(xcresultPath)) { - throw new Error(`Failed to generate xcresult at ${xcresultPath}`); - } - }, 120_000); - - afterAll(() => { - harness.cleanup(); - if (xcresultPath) { - const tmpDir = path.dirname(xcresultPath); - fs.rmSync(tmpDir, { recursive: true, force: true }); - } - }); - - describe('get-coverage-report', () => { - it('success', async () => { - // Filter to CalculatorAppTests which is always present and deterministic. - // The unfiltered report can include SPM framework targets non-deterministically. - const { text, isError } = await harness.invoke('coverage', 'get-coverage-report', { - xcresultPath, - target: 'CalculatorAppTests', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'get-coverage-report--success'); - }); - - it('error - invalid bundle', async () => { - const { text, isError } = await harness.invoke('coverage', 'get-coverage-report', { - xcresultPath: invalidXcresultPath, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'get-coverage-report--error-invalid-bundle'); - }); - }); - - describe('get-file-coverage', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('coverage', 'get-file-coverage', { - xcresultPath, - file: 'CalculatorService.swift', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'get-file-coverage--success'); - }); - - it('error - invalid bundle', async () => { - const { text, isError } = await harness.invoke('coverage', 'get-file-coverage', { - xcresultPath: invalidXcresultPath, - file: 'SomeFile.swift', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'get-file-coverage--error-invalid-bundle'); - }); - }); -}); +registerCoverageSnapshotSuite('cli'); +registerCoverageSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/debugging.snapshot.test.ts b/src/snapshot-tests/__tests__/debugging.snapshot.test.ts index 6662354d..c76eeba4 100644 --- a/src/snapshot-tests/__tests__/debugging.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/debugging.snapshot.test.ts @@ -1,214 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { execSync } from 'node:child_process'; -import { createSnapshotHarness, ensureSimulatorBooted } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; +import { registerDebuggingSnapshotSuite } from '../suites/debugging-suite.ts'; -const WORKSPACE = 'example_projects/iOS_Calculator/CalculatorApp.xcworkspace'; -const BUNDLE_ID = 'io.sentry.calculatorapp'; - -describe('debugging workflow', () => { - let harness: SnapshotHarness; - - beforeAll(async () => { - harness = await createSnapshotHarness(); - }); - - afterAll(() => { - harness.cleanup(); - }); - - describe('error paths (no session)', () => { - it('continue - error no session', async () => { - const { text, isError } = await harness.invoke('debugging', 'continue', {}); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'continue--error-no-session'); - }, 30_000); - - it('detach - error no session', async () => { - const { text, isError } = await harness.invoke('debugging', 'detach', {}); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'detach--error-no-session'); - }, 30_000); - - it('stack - error no session', async () => { - const { text, isError } = await harness.invoke('debugging', 'stack', {}); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'stack--error-no-session'); - }, 30_000); - - it('variables - error no session', async () => { - const { text, isError } = await harness.invoke('debugging', 'variables', {}); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'variables--error-no-session'); - }, 30_000); - - it('add-breakpoint - error no session', async () => { - const { text, isError } = await harness.invoke('debugging', 'add-breakpoint', { - file: 'test.swift', - line: 1, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'add-breakpoint--error-no-session'); - }, 30_000); - - it('remove-breakpoint - error no session', async () => { - const { text, isError } = await harness.invoke('debugging', 'remove-breakpoint', { - breakpointId: 1, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'remove-breakpoint--error-no-session'); - }, 30_000); - - it('lldb-command - error no session', async () => { - const { text, isError } = await harness.invoke('debugging', 'lldb-command', { - command: 'bt', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'lldb-command--error-no-session'); - }, 30_000); - - it('attach - error no process', async () => { - const { text, isError } = await harness.invoke('debugging', 'attach', { - simulatorId: '00000000-0000-0000-0000-000000000000', - bundleId: 'com.nonexistent.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'attach--error-no-process'); - }, 30_000); - }); - - describe('happy path (live debugger session)', () => { - let simulatorUdid: string; - - beforeAll(async () => { - vi.setConfig({ testTimeout: 120_000 }); - simulatorUdid = await ensureSimulatorBooted('iPhone 17'); - - // Kill any stale lldb-dap processes to ensure a clean attach - try { - execSync('pkill -f lldb-dap', { stdio: 'pipe' }); - await new Promise((resolve) => setTimeout(resolve, 1000)); - } catch { - /* ignore if none running */ - } - - execSync( - [ - 'xcodebuild build', - `-workspace ${WORKSPACE}`, - '-scheme CalculatorApp', - `-destination 'platform=iOS Simulator,id=${simulatorUdid}'`, - '-quiet', - ].join(' '), - { encoding: 'utf8', timeout: 120_000, stdio: 'pipe' }, - ); - - execSync(`xcrun simctl launch --terminate-running-process ${simulatorUdid} ${BUNDLE_ID}`, { - encoding: 'utf8', - stdio: 'pipe', - }); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - }, 120_000); - - afterAll(async () => { - try { - await harness.invoke('debugging', 'detach', {}); - } catch { - // best-effort cleanup - } - }); - - it('attach - success', async () => { - const { text, isError } = await harness.invoke('debugging', 'attach', { - simulatorId: simulatorUdid, - bundleId: BUNDLE_ID, - continueOnAttach: false, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'attach--success'); - }, 30_000); - - it('pause via lldb', async () => { - // Attach with continueOnAttach: false now pauses execution for DAP sessions. - // Keep this step as a semantic checkpoint without issuing a second interrupt. - await new Promise((resolve) => setTimeout(resolve, 250)); - }, 30_000); - - it('stack - success', async () => { - const { text, isError } = await harness.invoke('debugging', 'stack', {}); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'stack--success'); - }, 30_000); - - it('variables - success', async () => { - const { text, isError } = await harness.invoke('debugging', 'variables', {}); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'variables--success'); - }, 30_000); - - it('add-breakpoint - success', async () => { - const { text, isError } = await harness.invoke('debugging', 'add-breakpoint', { - file: 'ContentView.swift', - line: 42, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'add-breakpoint--success'); - }, 30_000); - - it('continue - success', async () => { - const { text, isError } = await harness.invoke('debugging', 'continue', {}); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'continue--success'); - }, 30_000); - - it('lldb-command - success', async () => { - const { text, isError } = await harness.invoke('debugging', 'lldb-command', { - command: 'breakpoint list', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'lldb-command--success'); - }, 30_000); - - it('remove-breakpoint - success', async () => { - const { text, isError } = await harness.invoke('debugging', 'remove-breakpoint', { - breakpointId: 1, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'remove-breakpoint--success'); - }, 30_000); - - it('detach - success', async () => { - const { text, isError } = await harness.invoke('debugging', 'detach', {}); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'detach--success'); - }, 30_000); - - it('attach - success (continue on attach)', async () => { - execSync(`xcrun simctl launch --terminate-running-process ${simulatorUdid} ${BUNDLE_ID}`, { - encoding: 'utf8', - stdio: 'pipe', - }); - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const { text, isError } = await harness.invoke('debugging', 'attach', { - simulatorId: simulatorUdid, - bundleId: BUNDLE_ID, - continueOnAttach: true, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'attach--success-continue'); - }, 30_000); - - it('detach after continue-on-attach', async () => { - const { isError } = await harness.invoke('debugging', 'detach', {}); - expect(isError).toBe(false); - }, 30_000); - }); -}); +registerDebuggingSnapshotSuite('cli'); +registerDebuggingSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/device.snapshot.test.ts b/src/snapshot-tests/__tests__/device.snapshot.test.ts index b9fa2359..859e6943 100644 --- a/src/snapshot-tests/__tests__/device.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/device.snapshot.test.ts @@ -1,206 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { execSync } from 'node:child_process'; -import { createSnapshotHarness } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; +import { registerDeviceSnapshotSuite } from '../suites/device-suite.ts'; -const WORKSPACE = 'example_projects/iOS_Calculator/CalculatorApp.xcworkspace'; -const BUNDLE_ID = 'io.sentry.calculatorapp'; -const DEVICE_ID = process.env.DEVICE_ID; - -describe('device workflow', () => { - let harness: SnapshotHarness; - - beforeAll(async () => { - vi.setConfig({ testTimeout: 120_000 }); - harness = await createSnapshotHarness(); - }, 120_000); - - afterAll(() => { - harness.cleanup(); - }); - - describe('list', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('device', 'list', {}); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'list--success'); - }); - }); - - describe('build', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('device', 'build', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'build--success'); - }); - - it('error - wrong scheme', async () => { - const { text, isError } = await harness.invoke('device', 'build', { - workspacePath: WORKSPACE, - scheme: 'NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'build--error-wrong-scheme'); - }); - }); - - describe('get-app-path', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('device', 'get-app-path', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'get-app-path--success'); - }); - - it('error - wrong scheme', async () => { - const { text, isError } = await harness.invoke('device', 'get-app-path', { - workspacePath: WORKSPACE, - scheme: 'NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'get-app-path--error-wrong-scheme'); - }); - }); - - describe('install', () => { - it('error - invalid app path', async () => { - const { text, isError } = await harness.invoke('device', 'install', { - deviceId: '00000000-0000-0000-0000-000000000000', - appPath: '/tmp/nonexistent.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'install--error-invalid-app'); - }); - }); - - describe('launch', () => { - it('error - invalid bundle', async () => { - const { text, isError } = await harness.invoke('device', 'launch', { - deviceId: '00000000-0000-0000-0000-000000000000', - bundleId: 'com.nonexistent.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'launch--error-invalid-bundle'); - }); - }); - - describe('stop', () => { - it('error - no app', async () => { - const { text, isError } = await harness.invoke('device', 'stop', { - deviceId: '00000000-0000-0000-0000-000000000000', - processId: 99999, - bundleId: 'com.nonexistent.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'stop--error-no-app'); - }); - }); - - describe.runIf(DEVICE_ID)('build-and-run (requires device)', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('device', 'build-and-run', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - deviceId: DEVICE_ID, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'build-and-run--success'); - }); - }); - - describe.runIf(DEVICE_ID)('install (requires device)', () => { - it('success', async () => { - const appPathOutput = execSync( - [ - 'xcodebuild -workspace', - WORKSPACE, - '-scheme CalculatorApp', - `-destination 'id=${DEVICE_ID}'`, - '-showBuildSettings', - ].join(' '), - { encoding: 'utf8', timeout: 30_000, stdio: 'pipe' }, - ); - const builtProductsDir = appPathOutput - .split('\n') - .find((l) => l.includes('BUILT_PRODUCTS_DIR')) - ?.split('=')[1] - ?.trim(); - const appPath = `${builtProductsDir}/CalculatorApp.app`; - - const { text, isError } = await harness.invoke('device', 'install', { - deviceId: DEVICE_ID, - appPath, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'install--success'); - }, 60_000); - }); - - describe.runIf(DEVICE_ID)('launch (requires device)', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('device', 'launch', { - deviceId: DEVICE_ID, - bundleId: BUNDLE_ID, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'launch--success'); - }, 60_000); - }); - - describe.runIf(DEVICE_ID)('stop (requires device)', () => { - it('success', async () => { - const tmpJson = `/tmp/devicectl-launch-${Date.now()}.json`; - execSync( - `xcrun devicectl device process launch --device ${DEVICE_ID} ${BUNDLE_ID} --json-output ${tmpJson}`, - { encoding: 'utf8', timeout: 30_000, stdio: 'pipe' }, - ); - const launchData = JSON.parse(require('fs').readFileSync(tmpJson, 'utf8')); - require('fs').unlinkSync(tmpJson); - const pid = launchData?.result?.process?.processIdentifier; - expect(pid).toBeGreaterThan(0); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const { text, isError } = await harness.invoke('device', 'stop', { - deviceId: DEVICE_ID, - processId: pid, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'stop--success'); - }, 60_000); - }); - - describe.runIf(DEVICE_ID)('test (requires device)', () => { - it('success - targeted passing test', async () => { - const { text, isError } = await harness.invoke('device', 'test', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - deviceId: DEVICE_ID, - extraArgs: ['-only-testing:CalculatorAppTests/CalculatorAppTests/testAddition'], - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'test--success'); - }, 300_000); - - it('failure - intentional test failure', async () => { - const { text, isError } = await harness.invoke('device', 'test', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - deviceId: DEVICE_ID, - }); - expect(isError).toBe(true); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'test--failure'); - }, 300_000); - }); -}); +registerDeviceSnapshotSuite('cli'); +registerDeviceSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/macos.snapshot.test.ts b/src/snapshot-tests/__tests__/macos.snapshot.test.ts index 7bb773f7..6e2a0051 100644 --- a/src/snapshot-tests/__tests__/macos.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/macos.snapshot.test.ts @@ -1,222 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { execSync } from 'node:child_process'; -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { createSnapshotHarness } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; -import { DERIVED_DATA_DIR } from '../../utils/log-paths.ts'; +import { registerMacosSnapshotSuite } from '../suites/macos-suite.ts'; -const PROJECT = 'example_projects/macOS/MCPTest.xcodeproj'; - -describe('macos workflow', () => { - let harness: SnapshotHarness; - let tmpDir: string; - let fakeAppPath: string; - let bundleIdAppPath: string; - - beforeAll(async () => { - harness = await createSnapshotHarness(); - - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'macos-snapshot-')); - - fakeAppPath = path.join(tmpDir, 'Fake.app'); - fs.mkdirSync(fakeAppPath); - - bundleIdAppPath = path.join(tmpDir, 'BundleTest.app'); - fs.mkdirSync(bundleIdAppPath); - const contentsDir = path.join(bundleIdAppPath, 'Contents'); - fs.mkdirSync(contentsDir); - fs.writeFileSync( - path.join(contentsDir, 'Info.plist'), - ` - - - - CFBundleIdentifier - com.test.snapshot-macos - -`, - ); - }); - - afterAll(() => { - harness.cleanup(); - if (tmpDir) { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } - }); - - describe('build', () => { - it('success', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'build', { - projectPath: PROJECT, - scheme: 'MCPTest', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'build--success'); - }); - - it('error - wrong scheme', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'build', { - projectPath: PROJECT, - scheme: 'NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'build--error-wrong-scheme'); - }); - }); - - describe('build-and-run', () => { - it('success', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'build-and-run', { - projectPath: PROJECT, - scheme: 'MCPTest', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'build-and-run--success'); - }); - - it('error - wrong scheme', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'build-and-run', { - projectPath: PROJECT, - scheme: 'NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'build-and-run--error-wrong-scheme'); - }); - }); - - describe('test', () => { - it('success', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'test', { - projectPath: PROJECT, - scheme: 'MCPTest', - extraArgs: [ - '-only-testing:MCPTestTests/MCPTestTests/appNameIsCorrect()', - '-only-testing:MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect', - ], - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'test--success'); - }); - - it('failure - intentional test failure', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'test', { - projectPath: PROJECT, - scheme: 'MCPTest', - }); - expect(isError).toBe(true); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'test--failure'); - }); - - it('error - wrong scheme', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'test', { - projectPath: PROJECT, - scheme: 'NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'test--error-wrong-scheme'); - }); - }); - - describe('get-app-path', () => { - it('success', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'get-app-path', { - projectPath: PROJECT, - scheme: 'MCPTest', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'get-app-path--success'); - }); - - it('error - wrong scheme', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'get-app-path', { - projectPath: PROJECT, - scheme: 'NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'get-app-path--error-wrong-scheme'); - }); - }); - - describe('launch', () => { - it('success', { timeout: 120000 }, async () => { - const settingsOutput = execSync( - `xcodebuild -project ${PROJECT} -scheme MCPTest -showBuildSettings -derivedDataPath '${DERIVED_DATA_DIR}' 2>/dev/null`, - { encoding: 'utf8' }, - ); - const match = settingsOutput.match(/BUILT_PRODUCTS_DIR = (.+)/); - const appPath = `${match![1]!.trim()}/MCPTest.app`; - - const { text, isError } = await harness.invoke('macos', 'launch', { - appPath, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'launch--success'); - }); - - it('error - invalid app', { timeout: 120000 }, async () => { - const nonExistentApp = path.join(tmpDir, 'NonExistent.app'); - const { text, isError } = await harness.invoke('macos', 'launch', { - appPath: nonExistentApp, - }); - expect(isError).toBe(true); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'launch--error-invalid-app'); - }); - }); - - describe('stop', () => { - it('success', { timeout: 120000 }, async () => { - const settingsOutput = execSync( - `xcodebuild -project ${PROJECT} -scheme MCPTest -showBuildSettings -derivedDataPath '${DERIVED_DATA_DIR}' 2>/dev/null`, - { encoding: 'utf8' }, - ); - const match = settingsOutput.match(/BUILT_PRODUCTS_DIR = (.+)/); - const appPath = `${match![1]!.trim()}/MCPTest.app`; - - await harness.invoke('macos', 'launch', { appPath }); - - const { text, isError } = await harness.invoke('macos', 'stop', { - appName: 'MCPTest', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'stop--success'); - }); - - it('error - no app', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'stop', { - processId: 999999, - }); - expect(isError).toBe(true); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'stop--error-no-app'); - }); - }); - - describe('get-macos-bundle-id', () => { - it('success', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'get-macos-bundle-id', { - appPath: bundleIdAppPath, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'get-macos-bundle-id--success'); - }); - - it('error - missing app', { timeout: 120000 }, async () => { - const { text, isError } = await harness.invoke('macos', 'get-macos-bundle-id', { - appPath: '/nonexistent/path/Fake.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'get-macos-bundle-id--error-missing-app'); - }); - }); -}); +registerMacosSnapshotSuite('cli'); +registerMacosSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/mcp-integration.snapshot.test.ts b/src/snapshot-tests/__tests__/mcp-integration.snapshot.test.ts deleted file mode 100644 index bdccf82c..00000000 --- a/src/snapshot-tests/__tests__/mcp-integration.snapshot.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { createMcpSnapshotHarness, type McpSnapshotHarness } from '../mcp-harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; - -describe('MCP Integration Snapshots', () => { - let harness: McpSnapshotHarness; - - beforeAll(async () => { - harness = await createMcpSnapshotHarness(); - }, 30_000); - - afterAll(async () => { - await harness.cleanup(); - }); - - describe('session-management', () => { - it('session_show_defaults -- empty', async () => { - await harness.client.callTool({ - name: 'session_clear_defaults', - arguments: { all: true }, - }); - const { text, isError } = await harness.callTool('session_show_defaults', {}); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'session-show-defaults--empty'); - }); - - it('session_set_defaults -- set scheme', async () => { - await harness.client.callTool({ - name: 'session_clear_defaults', - arguments: { all: true }, - }); - const { text, isError } = await harness.callTool('session_set_defaults', { - scheme: 'CalculatorApp', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'session-set-defaults--scheme'); - }); - }); - - describe('error-paths', () => { - it('build_sim -- missing required params', async () => { - await harness.client.callTool({ - name: 'session_clear_defaults', - arguments: { all: true }, - }); - const { text, isError } = await harness.callTool('build_sim', {}); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'build-sim--missing-params'); - }); - }); -}); diff --git a/src/snapshot-tests/__tests__/project-discovery.snapshot.test.ts b/src/snapshot-tests/__tests__/project-discovery.snapshot.test.ts index bb3e9b77..c9667d55 100644 --- a/src/snapshot-tests/__tests__/project-discovery.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/project-discovery.snapshot.test.ts @@ -1,150 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { createSnapshotHarness } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; +import { registerProjectDiscoverySnapshotSuite } from '../suites/project-discovery-suite.ts'; -const WORKSPACE = 'example_projects/iOS_Calculator/CalculatorApp.xcworkspace'; - -describe('project-discovery workflow', () => { - let harness: SnapshotHarness; - let tmpDir: string; - let bundleIdAppPath: string; - - beforeAll(async () => { - harness = await createSnapshotHarness(); - - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proj-discovery-')); - bundleIdAppPath = path.join(tmpDir, 'BundleTest.app'); - fs.mkdirSync(bundleIdAppPath); - fs.writeFileSync( - path.join(bundleIdAppPath, 'Info.plist'), - ` - - - - CFBundleIdentifier - com.test.snapshot - -`, - ); - const contentsDir = path.join(bundleIdAppPath, 'Contents'); - fs.mkdirSync(contentsDir); - fs.writeFileSync( - path.join(contentsDir, 'Info.plist'), - ` - - - - CFBundleIdentifier - com.test.snapshot - -`, - ); - }); - - afterAll(() => { - harness.cleanup(); - if (tmpDir) { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } - }); - - describe('list-schemes', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'list-schemes', { - workspacePath: WORKSPACE, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'list-schemes--success'); - }); - - it('error - invalid workspace', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'list-schemes', { - workspacePath: '/nonexistent/path/Fake.xcworkspace', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'list-schemes--error-invalid-workspace'); - }); - }); - - describe('show-build-settings', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'show-build-settings', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'show-build-settings--success'); - }); - - it('error - wrong scheme', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'show-build-settings', { - workspacePath: WORKSPACE, - scheme: 'NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'show-build-settings--error-wrong-scheme'); - }); - }); - - describe('discover-projs', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'discover-projects', { - workspaceRoot: 'example_projects/iOS_Calculator', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'discover-projs--success'); - }); - - it('error - invalid root', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'discover-projects', { - workspaceRoot: '/nonexistent/path/Fake.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'discover-projs--error-invalid-root'); - }); - }); - - describe('get-app-bundle-id', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'get-app-bundle-id', { - appPath: bundleIdAppPath, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'get-app-bundle-id--success'); - }); - - it('error - missing app', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'get-app-bundle-id', { - appPath: '/nonexistent/path/Fake.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'get-app-bundle-id--error-missing-app'); - }); - }); - - describe('get-macos-bundle-id', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'get-macos-bundle-id', { - appPath: bundleIdAppPath, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'get-macos-bundle-id--success'); - }); - - it('error - missing app', async () => { - const { text, isError } = await harness.invoke('project-discovery', 'get-macos-bundle-id', { - appPath: '/nonexistent/path/Fake.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'get-macos-bundle-id--error-missing-app'); - }); - }); -}); +registerProjectDiscoverySnapshotSuite('cli'); +registerProjectDiscoverySnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/project-scaffolding.snapshot.test.ts b/src/snapshot-tests/__tests__/project-scaffolding.snapshot.test.ts index 235574fb..b8f97b7a 100644 --- a/src/snapshot-tests/__tests__/project-scaffolding.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/project-scaffolding.snapshot.test.ts @@ -1,97 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { mkdtempSync, rmSync, mkdirSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { createSnapshotHarness } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; +import { registerProjectScaffoldingSnapshotSuite } from '../suites/project-scaffolding-suite.ts'; -function normalizeTmpDir(text: string, tmpDir: string): string { - const escaped = tmpDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - return text.replace(new RegExp(escaped, 'g'), ''); -} - -describe('project-scaffolding workflow', () => { - let harness: SnapshotHarness; - let tmpDir: string; - - beforeAll(async () => { - harness = await createSnapshotHarness(); - tmpDir = mkdtempSync(join(tmpdir(), 'xbm-scaffold-')); - }); - - afterAll(() => { - harness.cleanup(); - rmSync(tmpDir, { recursive: true, force: true }); - }); - - describe('scaffold-ios', () => { - it('success', async () => { - const outputPath = join(tmpDir, 'ios'); - const { text, isError } = await harness.invoke('project-scaffolding', 'scaffold-ios', { - projectName: 'SnapshotTestApp', - outputPath, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(normalizeTmpDir(text, tmpDir), __filename, 'scaffold-ios--success'); - }, 120000); - - it('error - existing project', async () => { - const outputPath = join(tmpDir, 'ios-existing'); - mkdirSync(outputPath, { recursive: true }); - - // Scaffold once to create the project files - await harness.invoke('project-scaffolding', 'scaffold-ios', { - projectName: 'SnapshotTestApp', - outputPath, - }); - - // Scaffold again into the same directory to trigger the error - const { text, isError } = await harness.invoke('project-scaffolding', 'scaffold-ios', { - projectName: 'SnapshotTestApp', - outputPath, - }); - expect(isError).toBe(true); - expectMatchesFixture( - normalizeTmpDir(text, tmpDir), - __filename, - 'scaffold-ios--error-existing', - ); - }, 120000); - }); - - describe('scaffold-macos', () => { - it('success', async () => { - const outputPath = join(tmpDir, 'macos'); - const { text, isError } = await harness.invoke('project-scaffolding', 'scaffold-macos', { - projectName: 'SnapshotTestMacApp', - outputPath, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(normalizeTmpDir(text, tmpDir), __filename, 'scaffold-macos--success'); - }, 120000); - - it('error - existing project', async () => { - const outputPath = join(tmpDir, 'macos-existing'); - mkdirSync(outputPath, { recursive: true }); - - await harness.invoke('project-scaffolding', 'scaffold-macos', { - projectName: 'SnapshotTestMacApp', - outputPath, - }); - - const { text, isError } = await harness.invoke('project-scaffolding', 'scaffold-macos', { - projectName: 'SnapshotTestMacApp', - outputPath, - }); - expect(isError).toBe(true); - expectMatchesFixture( - normalizeTmpDir(text, tmpDir), - __filename, - 'scaffold-macos--error-existing', - ); - }, 120000); - }); -}); +registerProjectScaffoldingSnapshotSuite('cli'); +registerProjectScaffoldingSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/resources.snapshot.test.ts b/src/snapshot-tests/__tests__/resources.snapshot.test.ts index dfc2ff69..02406b07 100644 --- a/src/snapshot-tests/__tests__/resources.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/resources.snapshot.test.ts @@ -1,44 +1,3 @@ -import { describe, it, expect, beforeAll } from 'vitest'; -import { invokeResource } from '../resource-harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import { ensureSimulatorBooted, shutdownAllSimulatorsExcept } from '../harness.ts'; +import { registerResourcesSnapshotSuite } from '../suites/resources-suite.ts'; -describe('resources', () => { - let simulatorUdid: string; - - beforeAll(async () => { - simulatorUdid = await ensureSimulatorBooted('iPhone 17'); - shutdownAllSimulatorsExcept([simulatorUdid]); - }, 30_000); - describe('devices', () => { - it('success', async () => { - const { text } = await invokeResource('devices'); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'devices--success'); - }); - }); - - describe('doctor', () => { - it('success', async () => { - const { text } = await invokeResource('doctor'); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'doctor--success'); - }); - }); - - describe('session-status', () => { - it('success', async () => { - const { text } = await invokeResource('session-status'); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'session-status--success'); - }); - }); - - describe('simulators', () => { - it('success', async () => { - const { text } = await invokeResource('simulators'); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'simulators--success'); - }); - }); -}); +registerResourcesSnapshotSuite(); diff --git a/src/snapshot-tests/__tests__/session-management.snapshot.test.ts b/src/snapshot-tests/__tests__/session-management.snapshot.test.ts index b6199ad8..c727a803 100644 --- a/src/snapshot-tests/__tests__/session-management.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/session-management.snapshot.test.ts @@ -1,83 +1,3 @@ -import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest'; -import { createSnapshotHarness } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; +import { registerSessionManagementSnapshotSuite } from '../suites/session-management-suite.ts'; -const WORKSPACE = 'example_projects/iOS_Calculator/CalculatorApp.xcworkspace'; - -describe('session-management workflow', () => { - let harness: SnapshotHarness; - - async function seedSessionDefaults(): Promise { - await harness.invoke('session-management', 'clear-defaults', { all: true }); - await harness.invoke('session-management', 'set-defaults', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - }); - await harness.invoke('session-management', 'set-defaults', { - profile: 'MyCustomProfile', - createIfNotExists: true, - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - }); - await harness.invoke('session-management', 'use-defaults-profile', { global: true }); - } - - beforeAll(async () => { - harness = await createSnapshotHarness(); - }); - - beforeEach(async () => { - await seedSessionDefaults(); - }); - - afterAll(() => { - harness.cleanup(); - }); - - describe('session-set-defaults', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('session-management', 'set-defaults', { - scheme: 'CalculatorApp', - workspacePath: WORKSPACE, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'session-set-defaults--success'); - }); - }); - - describe('session-show-defaults', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('session-management', 'show-defaults', {}); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'session-show-defaults--success'); - }); - }); - - describe('session-clear-defaults', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('session-management', 'clear-defaults', {}); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'session-clear-defaults--success'); - }); - }); - - describe('session-use-defaults-profile', () => { - it('success', async () => { - const { text } = await harness.invoke('session-management', 'use-defaults-profile', { - profile: 'MyCustomProfile', - }); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'session-use-defaults-profile--success'); - }); - }); - - describe('session-sync-xcode-defaults', () => { - it('success', async () => { - const { text } = await harness.invoke('session-management', 'sync-xcode-defaults', {}); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'session-sync-xcode-defaults--success'); - }); - }); -}); +registerSessionManagementSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/simulator-management.snapshot.test.ts b/src/snapshot-tests/__tests__/simulator-management.snapshot.test.ts index 9c6d33ce..7c1125cb 100644 --- a/src/snapshot-tests/__tests__/simulator-management.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/simulator-management.snapshot.test.ts @@ -1,155 +1,4 @@ -import { execSync } from 'node:child_process'; -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { createSnapshotHarness, ensureSimulatorBooted } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; +import { registerSimulatorManagementSnapshotSuite } from '../suites/simulator-management-suite.ts'; -describe('simulator-management workflow', () => { - let harness: SnapshotHarness; - let simulatorUdid: string; - - beforeAll(async () => { - simulatorUdid = await ensureSimulatorBooted('iPhone 17'); - harness = await createSnapshotHarness(); - }); - - afterAll(() => { - harness.cleanup(); - }); - - describe('list', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'list', {}); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'list--success'); - }); - }); - - describe('boot', () => { - it('error - invalid id', async () => { - const { text } = await harness.invoke('simulator-management', 'boot', { - simulatorId: '00000000-0000-0000-0000-000000000000', - }); - expectMatchesFixture(text, __filename, 'boot--error-invalid-id'); - }); - }); - - describe('open', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'open', {}); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'open--success'); - }); - }); - - describe('set-appearance', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'set-appearance', { - simulatorId: simulatorUdid, - mode: 'dark', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'set-appearance--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'set-appearance', { - simulatorId: '00000000-0000-0000-0000-000000000000', - mode: 'dark', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'set-appearance--error-invalid-simulator'); - }); - }); - - describe('set-location', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'set-location', { - simulatorId: simulatorUdid, - latitude: 37.7749, - longitude: -122.4194, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'set-location--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'set-location', { - simulatorId: '00000000-0000-0000-0000-000000000000', - latitude: 37.7749, - longitude: -122.4194, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'set-location--error-invalid-simulator'); - }); - }); - - describe('reset-location', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'reset-location', { - simulatorId: simulatorUdid, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'reset-location--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'reset-location', { - simulatorId: '00000000-0000-0000-0000-000000000000', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'reset-location--error-invalid-simulator'); - }); - }); - - describe('statusbar', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'statusbar', { - simulatorId: simulatorUdid, - dataNetwork: 'wifi', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'statusbar--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'statusbar', { - simulatorId: '00000000-0000-0000-0000-000000000000', - dataNetwork: 'wifi', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'statusbar--error-invalid-simulator'); - }); - }); - - describe('erase', () => { - it('error - invalid id', async () => { - const { text, isError } = await harness.invoke('simulator-management', 'erase', { - simulatorId: '00000000-0000-0000-0000-000000000000', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'erase--error-invalid-id'); - }); - - it('success', async () => { - const throwawayUdid = execSync('xcrun simctl create "SnapshotTestThrowaway" "iPhone 16"', { - encoding: 'utf8', - }).trim(); - - try { - const { text, isError } = await harness.invoke('simulator-management', 'erase', { - simulatorId: throwawayUdid, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'erase--success'); - } finally { - try { - execSync(`xcrun simctl delete ${throwawayUdid}`); - } catch { - // Simulator may already be deleted - } - } - }, 60_000); - }); -}); +registerSimulatorManagementSnapshotSuite('cli'); +registerSimulatorManagementSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/simulator.snapshot.test.ts b/src/snapshot-tests/__tests__/simulator.snapshot.test.ts index 6ddce2e7..b5a4b13a 100644 --- a/src/snapshot-tests/__tests__/simulator.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/simulator.snapshot.test.ts @@ -1,245 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { execSync } from 'node:child_process'; -import { - createSnapshotHarness, - ensureSimulatorBooted, - shutdownAllSimulatorsExcept, -} from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; -import { DERIVED_DATA_DIR } from '../../utils/log-paths.ts'; +import { registerSimulatorSnapshotSuite } from '../suites/simulator-suite.ts'; -const WORKSPACE = 'example_projects/iOS_Calculator/CalculatorApp.xcworkspace'; - -describe('simulator workflow', () => { - let harness: SnapshotHarness; - let simulatorUdid: string; - - beforeAll(async () => { - vi.setConfig({ testTimeout: 120_000 }); - harness = await createSnapshotHarness(); - simulatorUdid = await ensureSimulatorBooted('iPhone 17'); - }, 120_000); - - afterAll(() => { - harness.cleanup(); - }); - - describe('build', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator', 'build', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - simulatorName: 'iPhone 17', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'build--success'); - }, 120_000); - - it('error - wrong scheme', async () => { - const { text, isError } = await harness.invoke('simulator', 'build', { - workspacePath: WORKSPACE, - scheme: 'NONEXISTENT', - simulatorName: 'iPhone 17', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'build--error-wrong-scheme'); - }, 120_000); - }); - - describe('build-and-run', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator', 'build-and-run', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - simulatorName: 'iPhone 17', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'build-and-run--success'); - }, 120_000); - - it('error - wrong scheme', async () => { - const { text, isError } = await harness.invoke('simulator', 'build-and-run', { - workspacePath: WORKSPACE, - scheme: 'NONEXISTENT', - simulatorName: 'iPhone 17', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'build-and-run--error-wrong-scheme'); - }, 120_000); - }); - - describe('test', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator', 'test', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - simulatorName: 'iPhone 17', - extraArgs: ['-only-testing:CalculatorAppTests/CalculatorAppTests/testAddition'], - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'test--success'); - }, 120_000); - - it('failure - intentional test failure', async () => { - const { text, isError } = await harness.invoke('simulator', 'test', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - simulatorName: 'iPhone 17', - }); - expect(isError).toBe(true); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'test--failure'); - }, 120_000); - - it('error - wrong scheme', async () => { - const { text, isError } = await harness.invoke('simulator', 'test', { - workspacePath: WORKSPACE, - scheme: 'NONEXISTENT', - simulatorName: 'iPhone 17', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'test--error-wrong-scheme'); - }, 120_000); - }); - - describe('get-app-path', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator', 'get-app-path', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - platform: 'iOS Simulator', - simulatorName: 'iPhone 17', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'get-app-path--success'); - }, 120_000); - - it('error - wrong scheme', async () => { - const { text, isError } = await harness.invoke('simulator', 'get-app-path', { - workspacePath: WORKSPACE, - scheme: 'NONEXISTENT', - platform: 'iOS Simulator', - simulatorName: 'iPhone 17', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'get-app-path--error-wrong-scheme'); - }, 120_000); - }); - - describe('list', () => { - it('success', async () => { - shutdownAllSimulatorsExcept([simulatorUdid]); - const { text, isError } = await harness.invoke('simulator', 'list', {}); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'list--success'); - }, 120_000); - }); - - describe('install', () => { - it('success', async () => { - const settingsOutput = execSync( - `xcodebuild -workspace ${WORKSPACE} -scheme CalculatorApp -showBuildSettings -derivedDataPath '${DERIVED_DATA_DIR}' -destination 'platform=iOS Simulator,name=iPhone 17' 2>/dev/null`, - { encoding: 'utf8' }, - ); - const match = settingsOutput.match(/BUILT_PRODUCTS_DIR = (.+)/); - const appPath = `${match![1]!.trim()}/CalculatorApp.app`; - - const { text, isError } = await harness.invoke('simulator', 'install', { - simulatorId: simulatorUdid, - appPath, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'install--success'); - }, 120_000); - - it('error - invalid app', async () => { - const fs = await import('node:fs'); - const os = await import('node:os'); - const path = await import('node:path'); - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sim-install-')); - const fakeApp = path.join(tmpDir, 'NotAnApp.app'); - fs.mkdirSync(fakeApp); - try { - const { text } = await harness.invoke('simulator', 'install', { - simulatorId: simulatorUdid, - appPath: fakeApp, - }); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'install--error-invalid-app'); - } finally { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } - }, 120_000); - }); - - describe('launch-app', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator', 'launch-app', { - simulatorId: simulatorUdid, - bundleId: 'io.sentry.calculatorapp', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'launch-app--success'); - }, 120_000); - - it('error - not installed', async () => { - const { text, isError } = await harness.invoke('simulator', 'launch-app', { - simulatorId: simulatorUdid, - bundleId: 'com.nonexistent.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'launch-app--error-not-installed'); - }, 120_000); - }); - - describe('screenshot', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('simulator', 'screenshot', { - simulatorId: simulatorUdid, - returnFormat: 'path', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'screenshot--success'); - }, 120_000); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('simulator', 'screenshot', { - simulatorId: '00000000-0000-0000-0000-000000000000', - returnFormat: 'path', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'screenshot--error-invalid-simulator'); - }, 120_000); - }); - - describe('stop', () => { - it('success', async () => { - await harness.invoke('simulator', 'launch-app', { - simulatorId: simulatorUdid, - bundleId: 'io.sentry.calculatorapp', - }); - - const { text, isError } = await harness.invoke('simulator', 'stop', { - simulatorId: simulatorUdid, - bundleId: 'io.sentry.calculatorapp', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'stop--success'); - }, 120_000); - - it('error - no app', async () => { - const { text, isError } = await harness.invoke('simulator', 'stop', { - simulatorId: simulatorUdid, - bundleId: 'com.nonexistent.app', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'stop--error-no-app'); - }, 120_000); - }); -}); +registerSimulatorSnapshotSuite('cli'); +registerSimulatorSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/swift-package.snapshot.test.ts b/src/snapshot-tests/__tests__/swift-package.snapshot.test.ts index c0163159..98639ad4 100644 --- a/src/snapshot-tests/__tests__/swift-package.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/swift-package.snapshot.test.ts @@ -1,135 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { createSnapshotHarness } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; -import { clearAllProcesses } from '../../mcp/tools/swift-package/active-processes.ts'; +import { registerSwiftPackageSnapshotSuite } from '../suites/swift-package-suite.ts'; -const PACKAGE_PATH = 'example_projects/spm'; - -describe('swift-package workflow', () => { - let harness: SnapshotHarness; - - beforeAll(async () => { - vi.setConfig({ testTimeout: 120_000 }); - harness = await createSnapshotHarness(); - }, 120_000); - - afterAll(() => { - harness.cleanup(); - }); - - describe('build', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('swift-package', 'build', { - packagePath: PACKAGE_PATH, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'build--success'); - }, 120_000); - - it('error - bad path', async () => { - const { text, isError } = await harness.invoke('swift-package', 'build', { - packagePath: 'example_projects/NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'build--error-bad-path'); - }); - }); - - describe('test', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('swift-package', 'test', { - packagePath: PACKAGE_PATH, - filter: 'basicTruthTest', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'test--success'); - }, 120_000); - - it('failure - intentional test failure', async () => { - const { text, isError } = await harness.invoke('swift-package', 'test', { - packagePath: PACKAGE_PATH, - }); - expect(isError).toBe(true); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'test--failure'); - }, 120_000); - - it('error - bad path', async () => { - const { text, isError } = await harness.invoke('swift-package', 'test', { - packagePath: 'example_projects/NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'test--error-bad-path'); - }); - }); - - describe('clean', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('swift-package', 'clean', { - packagePath: PACKAGE_PATH, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'clean--success'); - }); - - it('error - bad path', async () => { - const { text, isError } = await harness.invoke('swift-package', 'clean', { - packagePath: 'example_projects/NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'clean--error-bad-path'); - }); - }); - - describe('run', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('swift-package', 'run', { - packagePath: PACKAGE_PATH, - executableName: 'spm', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(0); - expectMatchesFixture(text, __filename, 'run--success'); - }, 120_000); - - it('error - bad executable', async () => { - const { text, isError } = await harness.invoke('swift-package', 'run', { - packagePath: PACKAGE_PATH, - executableName: 'nonexistent-executable', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'run--error-bad-executable'); - }, 120_000); - }); - - describe('list', () => { - it('success', async () => { - await harness.invoke('swift-package', 'run', { - packagePath: PACKAGE_PATH, - executableName: 'spm', - background: true, - }); - - try { - const { text, isError } = await harness.invoke('swift-package', 'list', {}); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'list--success'); - } finally { - clearAllProcesses(); - } - }, 120_000); - }); - - describe('stop', () => { - it('error - no process', async () => { - const { text, isError } = await harness.invoke('swift-package', 'stop', { - pid: 999999, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'stop--error-no-process'); - }); - }); -}); +registerSwiftPackageSnapshotSuite('cli'); +registerSwiftPackageSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/ui-automation.snapshot.test.ts b/src/snapshot-tests/__tests__/ui-automation.snapshot.test.ts index 97f3a8fe..e974ebbe 100644 --- a/src/snapshot-tests/__tests__/ui-automation.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/ui-automation.snapshot.test.ts @@ -1,254 +1,4 @@ -import { execSync } from 'node:child_process'; -import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { createSnapshotHarness, ensureSimulatorBooted } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; +import { registerUiAutomationSnapshotSuite } from '../suites/ui-automation-suite.ts'; -const WORKSPACE = 'example_projects/iOS_Calculator/CalculatorApp.xcworkspace'; -const BUNDLE_ID = 'io.sentry.calculatorapp'; -const INVALID_SIMULATOR_ID = '00000000-0000-0000-0000-000000000000'; - -describe('ui-automation workflow', () => { - let harness: SnapshotHarness; - let simulatorUdid: string; - - beforeAll(async () => { - vi.setConfig({ testTimeout: 120_000 }); - simulatorUdid = await ensureSimulatorBooted('iPhone 17'); - harness = await createSnapshotHarness(); - - await harness.invoke('simulator', 'build-and-run', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - simulatorName: 'iPhone 17', - }); - - try { - execSync(`xcrun simctl launch ${simulatorUdid} ${BUNDLE_ID}`, { encoding: 'utf8' }); - } catch { - // App may already be running - } - await new Promise((resolve) => setTimeout(resolve, 3000)); - }); - - afterAll(() => { - harness.cleanup(); - }); - - describe('snapshot-ui', () => { - it('success - calculator app', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'snapshot-ui', { - simulatorId: simulatorUdid, - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(100); - expectMatchesFixture(text, __filename, 'snapshot-ui--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'snapshot-ui', { - simulatorId: INVALID_SIMULATOR_ID, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'snapshot-ui--error-no-simulator'); - }); - }); - - describe('tap', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'tap', { - simulatorId: simulatorUdid, - x: 100, - y: 400, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'tap--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'tap', { - simulatorId: INVALID_SIMULATOR_ID, - x: 100, - y: 100, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'tap--error-no-simulator'); - }); - }); - - describe('touch', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'touch', { - simulatorId: simulatorUdid, - x: 100, - y: 400, - down: true, - up: true, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'touch--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'touch', { - simulatorId: INVALID_SIMULATOR_ID, - x: 100, - y: 400, - down: true, - up: true, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'touch--error-no-simulator'); - }); - }); - - describe('long-press', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'long-press', { - simulatorId: simulatorUdid, - x: 100, - y: 400, - duration: 500, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'long-press--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'long-press', { - simulatorId: INVALID_SIMULATOR_ID, - x: 100, - y: 400, - duration: 500, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'long-press--error-no-simulator'); - }); - }); - - describe('swipe', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'swipe', { - simulatorId: simulatorUdid, - x1: 200, - y1: 400, - x2: 200, - y2: 200, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'swipe--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'swipe', { - simulatorId: INVALID_SIMULATOR_ID, - x1: 200, - y1: 400, - x2: 200, - y2: 200, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'swipe--error-no-simulator'); - }); - }); - - describe('gesture', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'gesture', { - simulatorId: simulatorUdid, - preset: 'scroll-down', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'gesture--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'gesture', { - simulatorId: INVALID_SIMULATOR_ID, - preset: 'scroll-down', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'gesture--error-no-simulator'); - }); - }); - - describe('button', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'button', { - simulatorId: simulatorUdid, - buttonType: 'home', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'button--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'button', { - simulatorId: INVALID_SIMULATOR_ID, - buttonType: 'home', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'button--error-no-simulator'); - }); - }); - - describe('key-press', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'key-press', { - simulatorId: simulatorUdid, - keyCode: 4, - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'key-press--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'key-press', { - simulatorId: INVALID_SIMULATOR_ID, - keyCode: 4, - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'key-press--error-no-simulator'); - }); - }); - - describe('key-sequence', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'key-sequence', { - simulatorId: simulatorUdid, - keyCodes: [4, 5, 6], - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'key-sequence--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'key-sequence', { - simulatorId: INVALID_SIMULATOR_ID, - keyCodes: [4, 5, 6], - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'key-sequence--error-no-simulator'); - }); - }); - - describe('type-text', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'type-text', { - simulatorId: simulatorUdid, - text: 'hello', - }); - expect(isError).toBe(false); - expectMatchesFixture(text, __filename, 'type-text--success'); - }); - - it('error - invalid simulator', async () => { - const { text, isError } = await harness.invoke('ui-automation', 'type-text', { - simulatorId: INVALID_SIMULATOR_ID, - text: 'hello', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'type-text--error-no-simulator'); - }); - }); -}); +registerUiAutomationSnapshotSuite('cli'); +registerUiAutomationSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/__tests__/utilities.snapshot.test.ts b/src/snapshot-tests/__tests__/utilities.snapshot.test.ts index e05b5e21..c2ade4f3 100644 --- a/src/snapshot-tests/__tests__/utilities.snapshot.test.ts +++ b/src/snapshot-tests/__tests__/utilities.snapshot.test.ts @@ -1,39 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { createSnapshotHarness } from '../harness.ts'; -import { expectMatchesFixture } from '../fixture-io.ts'; -import type { SnapshotHarness } from '../harness.ts'; +import { registerUtilitiesSnapshotSuite } from '../suites/utilities-suite.ts'; -const WORKSPACE = 'example_projects/iOS_Calculator/CalculatorApp.xcworkspace'; - -describe('utilities workflow', () => { - let harness: SnapshotHarness; - - beforeAll(async () => { - harness = await createSnapshotHarness(); - }); - - afterAll(() => { - harness.cleanup(); - }); - - describe('clean', () => { - it('success', async () => { - const { text, isError } = await harness.invoke('utilities', 'clean', { - workspacePath: WORKSPACE, - scheme: 'CalculatorApp', - }); - expect(isError).toBe(false); - expect(text.length).toBeGreaterThan(10); - expectMatchesFixture(text, __filename, 'clean--success'); - }, 120000); - - it('error - wrong scheme', async () => { - const { text, isError } = await harness.invoke('utilities', 'clean', { - workspacePath: WORKSPACE, - scheme: 'NONEXISTENT', - }); - expect(isError).toBe(true); - expectMatchesFixture(text, __filename, 'clean--error-wrong-scheme'); - }, 120000); - }); -}); +registerUtilitiesSnapshotSuite('cli'); +registerUtilitiesSnapshotSuite('mcp'); diff --git a/src/snapshot-tests/capture-debug-output.mjs b/src/snapshot-tests/capture-debug-output.mjs index 0a40c1cb..f468dcef 100644 --- a/src/snapshot-tests/capture-debug-output.mjs +++ b/src/snapshot-tests/capture-debug-output.mjs @@ -2,50 +2,76 @@ * Script to capture actual output from debugging tools for fixture comparison. * Run with: node --experimental-vm-modules src/snapshot-tests/capture-debug-output.mjs */ -import { execSync } from 'node:child_process'; -import { createRequire } from 'node:module'; +import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; -import fs from 'node:fs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, '../..'); - +const CLI_PATH = path.join(projectRoot, 'build/cli.js'); const WORKSPACE = 'example_projects/iOS_Calculator/CalculatorApp.xcworkspace'; -const BUNDLE_ID = 'io.sentry.calculatorapp'; - -// Find simulator -const listOutput = execSync('xcrun simctl list devices available --json', { encoding: 'utf8' }); -const data = JSON.parse(listOutput); -let simulatorUdid = null; -for (const runtime of Object.values(data.devices)) { - for (const device of runtime) { - if (device.name === 'iPhone 17') { - if (device.state !== 'Booted') { - execSync(`xcrun simctl boot ${device.udid}`, { encoding: 'utf8' }); - } - simulatorUdid = device.udid; - break; - } + +function runCli(workflow, cliToolName, args, output = 'text') { + return execFileSync( + 'node', + [CLI_PATH, workflow, cliToolName, '--json', JSON.stringify(args), '--output', output], + { + encoding: 'utf8', + cwd: projectRoot, + stdio: 'pipe', + }, + ); +} + +function parseSimulatorList(output) { + const simulators = []; + const lines = output.split('\n'); + + for (let index = 0; index < lines.length; index += 1) { + const simulatorLine = lines[index]?.match(/^\s*📱\s+\[[✓✗]\]\s+(.+)\s+\((Booted|Shutdown)\)\s*$/u); + if (!simulatorLine) continue; + + const udidLine = lines[index + 1]?.match(/^\s*UDID:\s+([0-9A-Fa-f-]+)\s*$/); + if (!udidLine?.[1]) continue; + + simulators.push({ + name: simulatorLine[1], + state: simulatorLine[2], + udid: udidLine[1], + }); + index += 1; } - if (simulatorUdid) break; + + return simulators; +} + +const simulators = parseSimulatorList(runCli('simulator-management', 'list', {}, 'text')); +const simulator = simulators.find((entry) => entry.name === 'iPhone 17'); + +if (!simulator) { + throw new Error('iPhone 17 simulator not found'); +} + +if (simulator.state !== 'Booted') { + runCli('simulator-management', 'boot', { simulatorId: simulator.udid }); } -console.log('Simulator UDID:', simulatorUdid); +console.log('Simulator UDID:', simulator.udid); console.log('Launching app...'); -execSync(`xcrun simctl launch --terminate-running-process ${simulatorUdid} ${BUNDLE_ID}`, { - encoding: 'utf8', - stdio: 'pipe', +runCli('simulator', 'build-and-run', { + workspacePath: WORKSPACE, + scheme: 'CalculatorApp', + simulatorId: simulator.udid, }); await new Promise((r) => setTimeout(r, 2000)); -// Now dynamically import the tool modules const { importToolModule } = await import(`${projectRoot}/build/core/manifest/import-tool-module.js`); const { normalizeSnapshotOutput } = await import(`${projectRoot}/build/snapshot-tests/normalize.js`).catch(() => { - // If not in build, use the project normalize return import(`${projectRoot}/src/snapshot-tests/normalize.ts`); }); +void importToolModule; +void normalizeSnapshotOutput; console.log('Modules loaded'); diff --git a/src/snapshot-tests/contracts.ts b/src/snapshot-tests/contracts.ts new file mode 100644 index 00000000..9abdc161 --- /dev/null +++ b/src/snapshot-tests/contracts.ts @@ -0,0 +1,22 @@ +export type SnapshotRuntime = 'cli' | 'mcp'; + +export interface FixtureKey { + runtime: SnapshotRuntime; + workflow: string; + scenario: string; +} + +export interface SnapshotResult { + text: string; + rawText: string; + isError: boolean; +} + +export interface WorkflowSnapshotHarness { + invoke( + workflow: string, + cliToolName: string, + args: Record, + ): Promise; + cleanup(): Promise; +} diff --git a/src/snapshot-tests/fixture-io.ts b/src/snapshot-tests/fixture-io.ts index 7f0b6cb6..9d63dcfd 100644 --- a/src/snapshot-tests/fixture-io.ts +++ b/src/snapshot-tests/fixture-io.ts @@ -1,22 +1,34 @@ import fs from 'node:fs'; import path from 'node:path'; import { expect } from 'vitest'; +import type { FixtureKey, SnapshotRuntime } from './contracts.ts'; const FIXTURES_DIR = path.resolve(process.cwd(), 'src/snapshot-tests/__fixtures__'); -function shouldUpdateSnapshots(): boolean { +export interface FixtureMatchOptions { + allowUpdate?: boolean; +} + +function shouldUpdateSnapshots(options?: FixtureMatchOptions): boolean { + if (options?.allowUpdate === false) { + return false; + } + return process.env.UPDATE_SNAPSHOTS === '1' || process.env.UPDATE_SNAPSHOTS === 'true'; } -export function fixturePathFor(testFilePath: string, scenario: string): string { - const workflow = path.basename(testFilePath, '.snapshot.test.ts'); - return path.join(FIXTURES_DIR, workflow, `${scenario}.txt`); +export function fixturePathFor(key: FixtureKey): string { + return path.join(FIXTURES_DIR, key.runtime, key.workflow, `${key.scenario}.txt`); } -export function expectMatchesFixture(actual: string, testFilePath: string, scenario: string): void { - const fixturePath = fixturePathFor(testFilePath, scenario); +export function expectMatchesFixture( + actual: string, + key: FixtureKey, + options?: FixtureMatchOptions, +): void { + const fixturePath = fixturePathFor(key); - if (shouldUpdateSnapshots()) { + if (shouldUpdateSnapshots(options)) { const dir = path.dirname(fixturePath); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(fixturePath, actual, 'utf8'); @@ -33,3 +45,13 @@ export function expectMatchesFixture(actual: string, testFilePath: string, scena const expected = fs.readFileSync(fixturePath, 'utf8'); expect(actual).toBe(expected); } + +export function createFixtureMatcher( + runtime: SnapshotRuntime, + workflow: string, + options?: FixtureMatchOptions, +) { + return (actual: string, scenario: string): void => { + expectMatchesFixture(actual, { runtime, workflow, scenario }, options); + }; +} diff --git a/src/snapshot-tests/harness.ts b/src/snapshot-tests/harness.ts index c9de8331..6c03ff07 100644 --- a/src/snapshot-tests/harness.ts +++ b/src/snapshot-tests/harness.ts @@ -1,101 +1,39 @@ import { spawnSync, execSync } from 'node:child_process'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { randomUUID } from 'node:crypto'; import { normalizeSnapshotOutput } from './normalize.ts'; -import { loadManifest } from '../core/manifest/load-manifest.ts'; -import { getEffectiveCliName } from '../core/manifest/schema.ts'; -import { importToolModule } from '../core/manifest/import-tool-module.ts'; -import type { ToolManifestEntry } from '../core/manifest/schema.ts'; -import { postProcessSession } from '../runtime/tool-invoker.ts'; -import { createToolCatalog } from '../runtime/tool-catalog.ts'; -import type { ToolDefinition } from '../runtime/types.ts'; -import type { ToolHandlerContext } from '../rendering/types.ts'; -import { createRenderSession } from '../rendering/render.ts'; +import type { SnapshotResult, WorkflowSnapshotHarness } from './contracts.ts'; +import { resolveSnapshotToolManifest } from './tool-manifest-resolver.ts'; const CLI_PATH = path.resolve(process.cwd(), 'build/cli.js'); -export interface SnapshotHarness { - invoke( - workflow: string, - cliToolName: string, - args: Record, - ): Promise; - cleanup(): void; -} +export type SnapshotHarness = WorkflowSnapshotHarness; +export type { SnapshotResult }; -export interface SnapshotResult { - text: string; - rawText: string; - isError: boolean; +function getSnapshotHarnessEnv(): Record { + const { VITEST: _vitest, NODE_ENV: _nodeEnv, ...rest } = process.env; + return Object.fromEntries( + Object.entries(rest).filter((entry): entry is [string, string] => entry[1] !== undefined), + ); } -function resolveToolManifest( - workflowId: string, +function runSnapshotCli( + workflow: string, cliToolName: string, -): { - toolModulePath: string; - isMcpOnly: boolean; - isStateful: boolean; - manifestEntry: ToolManifestEntry; -} | null { - const manifest = loadManifest(); - const workflow = manifest.workflows.get(workflowId); - if (!workflow) return null; - - const isMcpOnly = !workflow.availability.cli; - - for (const toolId of workflow.tools) { - const tool = manifest.tools.get(toolId); - if (!tool) continue; - if (getEffectiveCliName(tool) === cliToolName) { - return { - toolModulePath: tool.module, - isMcpOnly, - isStateful: tool.routing?.stateful === true, - manifestEntry: tool, - }; - } + args: Record, + output: 'text' | 'json' = 'text', +): ReturnType { + const commandArgs = [CLI_PATH, workflow, cliToolName, '--json', JSON.stringify(args)]; + if (output !== 'text') { + commandArgs.push('--output', output); } - return null; -} - -function buildMinimalToolCatalog( - manifestEntry: ToolManifestEntry, - handler: ToolDefinition['handler'], -): { tool: ToolDefinition; catalog: ReturnType } { - const manifest = loadManifest(); - const noopHandler: ToolDefinition['handler'] = async () => {}; - - const allTools: ToolDefinition[] = Array.from(manifest.tools.values()).map((toolEntry) => ({ - id: toolEntry.id, - cliName: getEffectiveCliName(toolEntry), - mcpName: toolEntry.names.mcp, - workflow: '', - description: toolEntry.description, - nextStepTemplates: toolEntry.nextSteps, - mcpSchema: {} as ToolDefinition['mcpSchema'], - cliSchema: {} as ToolDefinition['cliSchema'], - stateful: toolEntry.routing?.stateful ?? false, - handler: toolEntry.id === manifestEntry.id ? handler : noopHandler, - })); - - const catalog = createToolCatalog(allTools); - const tool = catalog.getByToolId(manifestEntry.id) ?? allTools[0]!; - return { tool, catalog }; -} - -async function importSnapshotToolModule(toolModulePath: string) { - const sourceModulePath = path.resolve(process.cwd(), 'src', `${toolModulePath}.ts`); - const sourceModuleUrl = pathToFileURL(sourceModulePath).href; - - try { - return (await import(sourceModuleUrl)) as { - handler: (params: Record, ctx?: ToolHandlerContext) => Promise; - }; - } catch { - return importToolModule(toolModulePath); - } + return spawnSync('node', commandArgs, { + encoding: 'utf8', + timeout: 120000, + cwd: process.cwd(), + env: getSnapshotHarnessEnv(), + }); } export async function createSnapshotHarness(): Promise { @@ -104,30 +42,20 @@ export async function createSnapshotHarness(): Promise { cliToolName: string, args: Record, ): Promise { - const resolved = resolveToolManifest(workflow, cliToolName); + const resolved = resolveSnapshotToolManifest(workflow, cliToolName); - if (resolved?.isMcpOnly || resolved?.isStateful) { - return invokeDirect(resolved.toolModulePath, resolved.manifestEntry, args); + if (!resolved) { + throw new Error(`Tool '${cliToolName}' not found in workflow '${workflow}'`); } - return invokeCli(workflow, cliToolName, args); - } + if (resolved.isMcpOnly) { + throw new Error(`Tool '${cliToolName}' in workflow '${workflow}' is not CLI-available`); + } - async function invokeCli( - workflow: string, - cliToolName: string, - args: Record, - ): Promise { - const jsonArg = JSON.stringify(args); - const { VITEST, NODE_ENV, ...cleanEnv } = process.env; - const result = spawnSync('node', [CLI_PATH, workflow, cliToolName, '--json', jsonArg], { - encoding: 'utf8', - timeout: 120000, - cwd: process.cwd(), - env: cleanEnv, - }); + const result = runSnapshotCli(workflow, cliToolName, args); + const stdout = + typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString('utf8') ?? ''); - const stdout = result.stdout ?? ''; return { text: normalizeSnapshotOutput(stdout), rawText: stdout, @@ -135,81 +63,25 @@ export async function createSnapshotHarness(): Promise { }; } - async function invokeDirect( - toolModulePath: string, - manifestEntry: ToolManifestEntry, - args: Record, - ): Promise { - const toolModule = await importSnapshotToolModule(toolModulePath); - const session = createRenderSession('text'); - const ctx: ToolHandlerContext = { - emit: (event) => { - session.emit(event); - }, - attach: (image) => { - session.attach(image); - }, - }; - await toolModule.handler(args, ctx); - - const { tool, catalog } = buildMinimalToolCatalog( - manifestEntry, - toolModule.handler as ToolDefinition['handler'], - ); - postProcessSession({ - tool, - session, - ctx, - catalog, - runtime: 'mcp', - applyTemplateNextSteps: ctx.nextStepParams != null, - }); - - const rawText = session.finalize() + '\n'; - return { - text: normalizeSnapshotOutput(rawText), - rawText, - isError: session.isError(), - }; - } - - function cleanup(): void {} + async function cleanup(): Promise {} return { invoke, cleanup }; } -/** - * Shut down all booted simulators except those in the keep list. - * Use before list/resource tests to guarantee a deterministic simulator state. - */ -export function shutdownAllSimulatorsExcept(keepUdids: string[] = []): void { +type SimctlAvailableDevices = { + devices: Record>; +}; + +function getAvailableDevices(): SimctlAvailableDevices { const listOutput = execSync('xcrun simctl list devices available --json', { encoding: 'utf8', }); - const data = JSON.parse(listOutput) as { - devices: Record>; - }; - const keepSet = new Set(keepUdids); - for (const runtime of Object.values(data.devices)) { - for (const device of runtime) { - if (device.state === 'Booted' && !keepSet.has(device.udid)) { - try { - execSync(`xcrun simctl shutdown ${device.udid}`, { encoding: 'utf8' }); - } catch { - // Ignore shutdown failures (device may already be shutting down). - } - } - } - } + + return JSON.parse(listOutput) as SimctlAvailableDevices; } export async function ensureSimulatorBooted(simulatorName: string): Promise { - const listOutput = execSync('xcrun simctl list devices available --json', { - encoding: 'utf8', - }); - const data = JSON.parse(listOutput) as { - devices: Record>; - }; + const data = getAvailableDevices(); for (const runtime of Object.values(data.devices)) { for (const device of runtime) { @@ -224,3 +96,34 @@ export async function ensureSimulatorBooted(simulatorName: string): Promise { + const tempSimulatorName = `xcodebuildmcp-snapshot-${simulatorName}-${randomUUID()}`; + const udid = execSync( + `xcrun simctl create "${tempSimulatorName}" "${simulatorName}" "${runtimeIdentifier}"`, + { + encoding: 'utf8', + }, + ).trim(); + + if (!udid) { + throw new Error(`Failed to create temporary simulator "${tempSimulatorName}"`); + } + + return udid; +} + +export async function shutdownSimulator(simulatorId: string): Promise { + execSync(`xcrun simctl shutdown ${simulatorId}`, { + encoding: 'utf8', + }); +} + +export async function deleteSimulator(simulatorId: string): Promise { + execSync(`xcrun simctl delete ${simulatorId}`, { + encoding: 'utf8', + }); +} diff --git a/src/snapshot-tests/mcp-harness.ts b/src/snapshot-tests/mcp-harness.ts index a298692a..6be0c5d7 100644 --- a/src/snapshot-tests/mcp-harness.ts +++ b/src/snapshot-tests/mcp-harness.ts @@ -1,47 +1,126 @@ import path from 'node:path'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; -import { extractText } from '../smoke-tests/test-helpers.ts'; import { normalizeSnapshotOutput } from './normalize.ts'; +import type { SnapshotResult, WorkflowSnapshotHarness } from './contracts.ts'; +import { resolveSnapshotToolManifest } from './tool-manifest-resolver.ts'; const CLI_PATH = path.resolve(process.cwd(), 'build/cli.js'); +const MCP_SNAPSHOT_PARITY_WORKFLOWS = [ + 'coverage', + 'debugging', + 'device', + 'macos', + 'project-discovery', + 'project-scaffolding', + 'session-management', + 'simulator', + 'simulator-management', + 'swift-package', + 'ui-automation', + 'utilities', +] as const; -export interface McpSnapshotHarness { - callTool(name: string, args: Record): Promise; +export interface McpSnapshotHarness extends WorkflowSnapshotHarness { + callTool(name: string, args: Record): Promise; client: Client; cleanup(): Promise; } -export interface McpSnapshotResult { - text: string; - rawText: string; - isError: boolean; +export interface CreateMcpSnapshotHarnessOptions { + enabledWorkflows?: string[]; } -export async function createMcpSnapshotHarness(): Promise { +function extractSnapshotTextContent(result: unknown): string { + const content = (result as { content?: unknown }).content; + if (!Array.isArray(content)) { + throw new Error('MCP snapshot result did not include a content array.'); + } + + let text = ''; + let textBlockCount = 0; + + for (const part of content) { + if (!part || typeof part !== 'object') { + throw new Error('MCP snapshot result contained an invalid content block.'); + } + + const typedPart = part as { type?: unknown; text?: unknown }; + if (typedPart.type === 'text') { + if (typeof typedPart.text !== 'string') { + throw new Error('MCP snapshot result contained a text block without string text.'); + } + textBlockCount += 1; + if (textBlockCount > 1) { + throw new Error( + 'MCP snapshot result contained multiple text blocks; snapshot extraction refuses to invent separators.', + ); + } + text += typedPart.text; + } + } + + return text; +} + +function createSnapshotHarnessEnv(overrides: Record): Record { + const { VITEST: _vitest, NODE_ENV: _nodeEnv, ...rest } = process.env; + const env = Object.fromEntries( + Object.entries(rest).filter((entry): entry is [string, string] => entry[1] !== undefined), + ); + return { ...env, ...overrides }; +} + +export async function createMcpSnapshotHarness( + opts: CreateMcpSnapshotHarnessOptions = {}, +): Promise { + const enabledWorkflows = opts.enabledWorkflows ?? [...MCP_SNAPSHOT_PARITY_WORKFLOWS]; const transport = new StdioClientTransport({ command: 'node', args: [CLI_PATH, 'mcp'], + env: createSnapshotHarnessEnv({ + XCODEBUILDMCP_ENABLED_WORKFLOWS: enabledWorkflows.join(','), + XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS: 'true', + XCODEBUILDMCP_DISABLE_XCODE_AUTO_SYNC: '1', + XCODEBUILDMCP_TEST_FORCE_TOOL_EXPOSURE: 'sync_xcode_defaults', + }), stderr: 'pipe', }); const client = new Client({ name: 'snapshot-test-client', version: '1.0.0' }); await client.connect(transport, { timeout: 30_000 }); - async function callTool(name: string, args: Record): Promise { - const result = await client.callTool({ name, arguments: args }); - const rawText = extractText(result) + '\n'; + async function callTool(name: string, args: Record): Promise { + const result = await client.callTool({ name, arguments: args }, undefined, { + timeout: 120_000, + }); + const rawText = extractSnapshotTextContent(result); const text = normalizeSnapshotOutput(rawText); const isError = (result as { isError?: boolean }).isError ?? false; return { text, rawText, isError }; } + async function invoke( + workflow: string, + cliToolName: string, + args: Record, + ): Promise { + const resolved = resolveSnapshotToolManifest(workflow, cliToolName); + if (!resolved) { + throw new Error(`Tool '${cliToolName}' not found in workflow '${workflow}'`); + } + if (!resolved.isMcpAvailable) { + throw new Error(`Tool '${cliToolName}' in workflow '${workflow}' is not MCP-available`); + } + + return callTool(resolved.mcpToolName, args); + } + return { + invoke, callTool, client, - async cleanup(): Promise { - await client.close(); - }, + cleanup: () => client.close(), }; } diff --git a/src/snapshot-tests/normalize.ts b/src/snapshot-tests/normalize.ts index 09e65e04..00307e08 100644 --- a/src/snapshot-tests/normalize.ts +++ b/src/snapshot-tests/normalize.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-control-regex, no-regex-spaces */ import os from 'node:os'; import path from 'node:path'; @@ -13,6 +14,7 @@ const PID_JSON_REGEX = /"pid"\s*:\s*\d+/g; const PROCESS_ID_REGEX = /Process ID: \d+/g; const PROCESS_INLINE_PID_REGEX = /process \d+/g; const CLI_PROCESS_ID_ARG_REGEX = /--process-id "\d+"/g; +const MCP_PROCESS_ID_ARG_REGEX = /(processId:\s*)\d+/g; const THREAD_ID_REGEX = /Thread \d{5,}/g; const HEX_ADDRESS_REGEX = /0x[0-9a-fA-F]{8,}/g; @@ -23,8 +25,10 @@ const LLDB_LOWER_FRAMES_REGEX = /( frame #\d+: (?: at [^\n]*|(?: at const LLDB_FRAME_NUMBER_REGEX = / frame #\d+:/g; const LLDB_BREAKPOINT_LOCATIONS_REGEX = /locations = .+$/gm; const LLDB_BREAKPOINT_SUB_LOCATION_REGEX = /^\s+\d+\.\d+: where = [^\n]+\n?/gm; +const LLDB_RUNTIME_ROOT_FRAME_REGEX = + /^\s*frame #(?:\d+|): .*\/Library\/Developer\/CoreSimulator\/Volumes\/[^`\n]+`[^\n]*\n?/gm; const DERIVED_DATA_HASH_REGEX = /(DerivedData\/[A-Za-z0-9_]+)-[a-z]{28}\b/g; -const PROGRESS_LINE_REGEX = /^›.*\n*/gm; +const PROGRESS_LINE_REGEX = /^›.*$/gm; const WARNINGS_BLOCK_REGEX = /Warnings \(\d+\):\n(?:\n? *⚠[^\n]*\n?)*/g; const XCODE_INFRA_ERRORS_REGEX = /Compiler Errors \(\d+\):\n(?:\n? *✗ (?:unable to rename temporary|failed to emit precompiled|accessing build database)[^\n]*\n?(?:\n? {4}[^\n]*\n?)*)*/g; @@ -47,6 +51,7 @@ const CODEX_WORKTREE_NODE_MODULES_REGEX = const ACQUIRED_USAGE_ASSERTION_TIME_REGEX = /(^\s*)\d{2}:\d{2}:\d{2}( {2}Acquired usage assertion\.)$/gm; const BUILD_SETTINGS_PATH_REGEX = /^( {6}PATH = ).+$/gm; +const ACTIVE_LAUNCH_OSLOG_SESSIONS_REGEX = /("activeLaunchOsLogSessions"\s*:\s*)\[[\s\S]*?\]/g; const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm; function sortLinesInBlock(text: string, marker: RegExp): string { @@ -117,6 +122,7 @@ export function normalizeSnapshotOutput(text: string): string { normalized = normalized.replace(PROCESS_ID_REGEX, 'Process ID: '); normalized = normalized.replace(PROCESS_INLINE_PID_REGEX, 'process '); normalized = normalized.replace(CLI_PROCESS_ID_ARG_REGEX, '--process-id ""'); + normalized = normalized.replace(MCP_PROCESS_ID_ARG_REGEX, '$1'); normalized = normalized.replace(UPTIME_REGEX, 'Uptime: '); normalized = normalized.replace(THREAD_ID_REGEX, 'Thread '); normalized = normalized.replace(HEX_ADDRESS_REGEX, ''); @@ -126,6 +132,7 @@ export function normalizeSnapshotOutput(text: string): string { normalized = normalized.replace(LLDB_FRAME_NUMBER_REGEX, ' frame #:'); normalized = normalized.replace(LLDB_BREAKPOINT_LOCATIONS_REGEX, 'locations = '); normalized = normalized.replace(LLDB_BREAKPOINT_SUB_LOCATION_REGEX, ''); + normalized = normalized.replace(LLDB_RUNTIME_ROOT_FRAME_REGEX, ''); normalized = normalized.replace(RESULT_BUNDLE_LINE_REGEX, ''); normalized = normalized.replace(PROGRESS_LINE_REGEX, ''); normalized = normalized.replace(WARNINGS_BLOCK_REGEX, ''); @@ -140,6 +147,7 @@ export function normalizeSnapshotOutput(text: string): string { normalized = normalized.replace(TARGET_DEVICE_IDENTIFIER_REGEX, '$1'); normalized = normalized.replace(BUILD_SETTINGS_PATH_REGEX, '$1'); + normalized = normalized.replace(ACTIVE_LAUNCH_OSLOG_SESSIONS_REGEX, '$1[]'); normalized = normalized.replace(CODEX_ARG0_PATH_REGEX, '/.codex/tmp/arg0/codex-arg0'); normalized = normalized.replace(ACQUIRED_USAGE_ASSERTION_TIME_REGEX, '$1