Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/TOOLS-CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,4 @@ XcodeBuildMCP provides 71 canonical tools organized into 13 workflow groups.

---

*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-07T22:29:44.282Z UTC*
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-08T12:09:33.648Z UTC*
2 changes: 1 addition & 1 deletion docs/TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,4 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov

---

*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-07T22:29:44.282Z UTC*
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-08T12:09:33.648Z UTC*
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# Investigation + Plan: Simulator Selector Normalization, CLI Determinism, and Platform Inference

## Purpose

This document separates three related but distinct problem domains and defines a concrete implementation plan for each.

## Problem Domains

1. Inconsistent handling of `simulatorId` and `simulatorName` across logic paths.
2. CLI hydrates session defaults, which makes CLI behavior non-deterministic.
3. Non-iOS simulator targets (watchOS/tvOS/visionOS) can use incorrect platform names and fail builds.

## Scope

1. Keep existing CLI argument UX and command surface.
2. Keep existing runtime validation behavior (`oneOf`/`allOf`/XOR checks).
3. Change only session-default hydration behavior, selector normalization behavior, and platform inference behavior.
4. Stateful CLI behavior (daemon, logs, debug sessions) remains by design and is out of scope.

## Decision Snapshot

1. Platform inference must support non-iOS simulators using simulator runtime metadata, with build-settings only as fallback.
2. Session defaults must be MCP-only runtime behavior (no CLI/daemon hydration into `sessionStore`).
3. Store both `simulatorId` and `simulatorName` in session defaults/config and disambiguate at tool boundary via shared helper logic.
4. Persist only `simulatorPlatform` as platform cache; do not persist `simulatorRuntime` or timestamp fields.

---

## Domain 1: `simulatorId` / `simulatorName` Normalization

## Current State (verified)

1. `session_set_defaults` can keep both `simulatorId` and `simulatorName`.
2. Config normalization drops `simulatorName` when both are present.
3. Session-aware factory prunes exclusive pairs at merge-time and prefers first key when both come from defaults.
4. Some tools still enforce schema-level XOR, creating layered/duplicated enforcement.

Net effect: behavior is usually correct, but inconsistent and hard to reason about.

## Decision

Use a single normalized model everywhere:

1. Store both values in session defaults/config.
2. `simulatorId` is authoritative for tools that require UUID.
3. `simulatorName` is preserved for portability and tools that can use name.
4. Explicit user args that provide both remain invalid.
5. Each tool receives exactly one effective selector value at execution boundary.

## Why this model

1. `simulatorName` survives simulator resets better than UUIDs.
2. Some operations fundamentally require UUID; others can run with name.
3. Keeping both in storage avoids repeated lossy conversion and supports both execution modes.

## Required Invariants

1. Storage layer may contain both selector fields.
2. Explicit invocation args may not contain both selector fields.
3. Tool execution input must be disambiguated to one selector by a shared helper.
4. Disambiguation precedence must be deterministic and test-covered.

## Implementation Plan

1. Add `inferSimulatorSelectorForTool(...)` helper (or equivalent) used by simulator tools.
2. Normalize config/session behavior to stop dropping `simulatorName` when both exist.
3. Keep factory-level explicit XOR validation.
4. Keep per-tool requirement checks (`oneOf`/`allOf`) unchanged.
5. Reduce duplicated tool-local selector branching by centralizing selector choice.

## Validation Plan

1. Unit tests for helper precedence across explicit args, stored defaults, and missing values.
2. Regression tests for config-load behavior preserving both fields.
3. Integration tests for tools that require UUID vs tools that accept name.
4. Real-world MCP run validating both selector paths.

---

## Domain 2: CLI Determinism and Session Defaults

## Current State (verified)

1. `session-management` workflow is not exposed in CLI.
2. CLI runtime still bootstraps config and hydrates `config.sessionDefaults` into `sessionStore`.
3. Result: CLI can pick up hidden persisted defaults that the user cannot inspect/mutate via CLI commands.

Net effect: CLI behavior can vary based on hidden config state.

## Decision

Make session-default hydration MCP-only.

1. MCP runtime hydrates `config.sessionDefaults` into `sessionStore`.
2. CLI and daemon runtimes do not hydrate `sessionDefaults` into `sessionStore`.
3. CLI/daemon still read non-session config (workflow filters, debug flags, timeouts, etc.).
4. No CLI command/flag redesign is required.

## Required Invariants

1. CLI tool behavior must not depend on persisted `sessionDefaults`.
2. CLI behavior remains explicit-argument driven.
3. Existing runtime validation for required parameters remains intact.
4. `disableSessionDefaults=true` behavior for MCP tools remains consistent with current expectations.

## Implementation Plan

1. Gate session-default hydration by runtime in `bootstrapRuntime`.
2. Ensure daemon startup path also does not hydrate selector defaults.
3. Add tests that verify:
- MCP hydrates session defaults.
- CLI and daemon do not.
4. Keep all existing CLI validation paths and error messages unless a bug is found.

## Validation Plan

1. Unit/integration tests for runtime hydration boundaries.
2. CLI real-world test with persisted `sessionDefaults` present in config:
- missing required args should still fail.
- explicit args should succeed.
3. MCP real-world test confirming session-default convenience still works.

---

## Domain 3: Non-iOS Simulator Platform Inference

## Current State (verified)

1. iOS paths are generally reliable.
2. Non-iOS simulator targets can infer wrong platform and fail destination matching.
3. Build settings lookups are slower and should not be the first-line source for simulator platform inference.

## Decision

Platform inference for simulator tools must use simulator metadata first, then fallback.

1. Primary source: simulator runtime metadata (via `simctl` resolution).
2. Derived output: correct simulator platform string (`iOS/watchOS/tvOS/visionOS Simulator`).
3. Secondary source: build settings only when simulator metadata cannot resolve.
4. Final fallback: explicit warning + `iOS Simulator` only when no better signal exists.

Cache policy:

1. Persist `simulatorPlatform` as the cached output.
2. Recompute `simulatorPlatform` during MCP startup hydration.
3. Recompute `simulatorPlatform` whenever simulator selector (`simulatorId`/`simulatorName`) changes.
4. Do not persist `simulatorRuntime` or timestamp fields.

## Required Invariants

1. If runtime indicates non-iOS simulator, platform must not default to iOS.
2. Platform inference source should be logged for observability.
3. Selector normalization and platform inference should be reusable utilities, not tool-local variants.

## Implementation Plan

1. Introduce/standardize `inferPlatform(...)` utility contract around selector + runtime metadata.
2. Ensure simulator-name and simulator-id paths both resolve runtime/platform deterministically.
3. Add normalized mapping from CoreSimulator runtime to xcodebuild destination platform.
4. Use build-settings only as fallback path.

## Validation Plan

1. Unit tests for runtime-to-platform mapping.
2. Integration tests for iOS + non-iOS simulator selectors.
3. Real-world CLI/MCP checks for watchOS/tvOS/visionOS flows where available.

---

## Cross-Cutting Architecture

## Shared Helpers

1. `inferSimulatorSelectorForTool(...)`
- Input: explicit params + stored defaults + tool capability (`requiresId` vs `acceptsNameOrId`).
- Output: exactly one effective selector (or deterministic validation error).
2. `inferPlatform(...)`
- Input: resolved selector + scheme/path context.
- Output: `{ platform, source, runtime? }`.

## Data Model

Keep/add simulator metadata fields in session/config:

1. `simulatorId`
2. `simulatorName`
3. `simulatorPlatform` (optional cache)

`simulatorRuntime` can still be used transiently inside resolver/helper logic, but is not persisted.

## Runtime Boundary Rules

1. MCP: may hydrate/use session defaults.
2. CLI: no session-default hydration; explicit invocation only.
3. Daemon (CLI backend): same as CLI for hydration semantics.

---

## Delivery Plan

## Phase 0: Lock Decisions

1. Approve this document’s three-domain decisions.
2. Confirm no CLI UX changes are in scope.

## Phase 1: Runtime Boundary Fix (Domain 2)

1. Implement MCP-only session-default hydration.
2. Add runtime boundary tests.

## Phase 2: Selector Normalization (Domain 1)

1. Implement shared selector helper.
2. Align config/session behavior to preserve both selector fields.
3. Remove path-specific inconsistencies.

## Phase 3: Platform Inference Hardening (Domain 3)

1. Consolidate non-iOS platform inference on simulator metadata.
2. Add mapping tests and fallback tests.

## Phase 4: Regression + Real-World Validation

1. Run project test/lint/typecheck/build pipeline.
2. Run real-world MCP and CLI smoke tests for selector + platform behavior.
3. Document outcomes in PR notes.

---

## Risks and Mitigations

1. Risk: duplicate validation behavior drifts across tools.
- Mitigation: central helper + contract tests.
2. Risk: config backward-compat surprises.
- Mitigation: additive fields and migration-safe parsing.
3. Risk: non-iOS paths regress silently.
- Mitigation: explicit non-iOS test coverage and runtime-source logging.

## Out of Scope

1. Redesigning CLI command structure or argument names.
2. Changing general daemon stateful behavior.
3. Introducing auto-retry heuristics on command failure.
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ describe('session-set-defaults tool', () => {
const current = sessionStore.getAll();
expect(current.scheme).toBe('MyScheme');
expect(current.simulatorName).toBe('iPhone 16');
// simulatorId should be auto-resolved from simulatorName
expect(current.simulatorId).toBe('RESOLVED-SIM-UUID');
// simulatorId resolution happens in background; immediate update keeps explicit inputs only
expect(current.simulatorId).toBeUndefined();
expect(current.useLatestOS).toBe(true);
expect(current.arch).toBe('arm64');
});
Expand Down Expand Up @@ -115,28 +115,43 @@ describe('session-set-defaults tool', () => {
);
});

it('should clear simulatorName when simulatorId is explicitly set', async () => {
sessionStore.setDefaults({ simulatorName: 'iPhone 16' });
const result = await sessionSetDefaultsLogic({ simulatorId: 'SIM-UUID' }, createContext());
it('should clear stale simulatorName when simulatorId is explicitly set', async () => {
sessionStore.setDefaults({ simulatorName: 'Old Name' });
const result = await sessionSetDefaultsLogic(
{ simulatorId: 'RESOLVED-SIM-UUID' },
createContext(),
);
const current = sessionStore.getAll();
expect(current.simulatorId).toBe('SIM-UUID');
expect(current.simulatorId).toBe('RESOLVED-SIM-UUID');
expect(current.simulatorName).toBeUndefined();
expect(result.content[0].text).toContain(
'Cleared simulatorName because simulatorId was explicitly set.',
'Cleared simulatorName because simulatorId was set; background resolution will repopulate it.',
);
});

it('should auto-resolve simulatorName to simulatorId when only simulatorName is set', async () => {
it('should clear stale simulatorId when only simulatorName is set', async () => {
sessionStore.setDefaults({ simulatorId: 'OLD-SIM-UUID' });
const result = await sessionSetDefaultsLogic({ simulatorName: 'iPhone 16' }, createContext());
const current = sessionStore.getAll();
// Both should be set now - name provided, id resolved
// simulatorId resolution happens in background; stale id is cleared immediately
expect(current.simulatorName).toBe('iPhone 16');
expect(current.simulatorId).toBe('RESOLVED-SIM-UUID');
expect(result.content[0].text).toContain('Resolved simulatorName');
expect(current.simulatorId).toBeUndefined();
expect(result.content[0].text).toContain(
'Cleared simulatorId because simulatorName was set; background resolution will repopulate it.',
);
});

it('should return error when simulatorName cannot be resolved', async () => {
it('does not claim simulatorName was cleared when none existed', async () => {
sessionStore.setDefaults({ simulatorId: 'RESOLVED-SIM-UUID' });
const result = await sessionSetDefaultsLogic(
{ simulatorId: 'RESOLVED-SIM-UUID' },
createContext(),
);

expect(result.content[0].text).not.toContain('Cleared simulatorName');
});

it('should not fail when simulatorName cannot be resolved immediately', async () => {
const contextWithFailingExecutor = {
executor: vi.fn().mockImplementation(async (command: string[]) => {
if (command.includes('simctl') && command.includes('list')) {
Expand All @@ -159,8 +174,8 @@ describe('session-set-defaults tool', () => {
{ simulatorName: 'NonExistentSimulator' },
contextWithFailingExecutor,
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Failed to resolve simulator name');
expect(result.isError).toBe(false);
expect(sessionStore.getAll().simulatorName).toBe('NonExistentSimulator');
});

it('should prefer workspacePath when both projectPath and workspacePath are provided', async () => {
Expand Down Expand Up @@ -222,7 +237,11 @@ describe('session-set-defaults tool', () => {
await initConfigStore({ cwd, fs });

const result = await sessionSetDefaultsLogic(
{ workspacePath: '/new/App.xcworkspace', simulatorId: 'SIM-1', persist: true },
{
workspacePath: '/new/App.xcworkspace',
simulatorId: 'RESOLVED-SIM-UUID',
persist: true,
},
createContext(),
);

Expand All @@ -235,8 +254,7 @@ describe('session-set-defaults tool', () => {
};
expect(parsed.sessionDefaults?.workspacePath).toBe('/new/App.xcworkspace');
expect(parsed.sessionDefaults?.projectPath).toBeUndefined();
expect(parsed.sessionDefaults?.simulatorId).toBe('SIM-1');
// simulatorName is cleared because simulatorId was explicitly set
expect(parsed.sessionDefaults?.simulatorId).toBe('RESOLVED-SIM-UUID');
expect(parsed.sessionDefaults?.simulatorName).toBeUndefined();
});

Expand Down
Loading
Loading