fix(simulator): Normalize selector refresh and platform inference#206
Conversation
commit: |
WalkthroughThis pull request introduces platform inference logic for Xcode simulators and refactors simulator selector handling. New utility modules enable dynamic platform detection from simulator metadata, build settings, and simulator names. Simulator selector handling is reworked to support both simulatorId and simulatorName with background resolution via a new refresh scheduler. Session bootstrap logic is updated to hydrate session defaults for MCP runtime only. Simulator build, run, and test tools are refactored to use inferred platforms instead of hard-coded values. Session store and schema definitions are extended with simulatorPlatform caching and revision tracking. Corresponding test coverage is added for platform inference, detection, and bootstrap behaviour. 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/mcp/tools/simulator/build_run_sim.ts`:
- Line 471: Remove the emoji from user-facing log/output strings: replace
occurrences like log('info', `✅ ${platformName} simulator build & run
succeeded.`) and the similar string at the other occurrence with plain-text
messages (e.g., "Simulator build & run succeeded" or "Successfully built and ran
the simulator for ${platformName}") so that platformName remains interpolated
but no emoji characters are present; update the strings in build_run_sim.ts
where log(...) and the other user-facing output are created.
In `@src/runtime/bootstrap-runtime.ts`:
- Around line 75-78: hydrateSessionDefaultsForMcp currently may return early
when config.sessionDefaults is empty, but the calling code always logs
"simulator metadata refresh scheduled"; change hydrateSessionDefaultsForMcp to
return a boolean indicating whether it actually scheduled a refresh (true when
defaults present and refresh scheduled, false otherwise) and update the caller
in bootstrap-runtime (the if (opts.runtime === 'mcp') block) to only call
log('info', '[Session] Hydrated MCP session defaults; simulator metadata refresh
scheduled.') when hydrateSessionDefaultsForMcp(config.sessionDefaults) returns
true; ensure the boolean return is documented in the function signature and
handled accordingly by the caller.
In `@src/utils/infer-platform.ts`:
- Around line 152-168: The current resolveProjectFromSession can return both
projectPath and workspacePath when params don't supply either but session
defaults contain both; update resolveProjectFromSession so that when
params.projectPath and params.workspacePath are both undefined and defaults
contain both paths, it prefers workspacePath (sets projectPath to undefined and
workspacePath to defaults.workspacePath) to avoid passing mutually-exclusive
both values into detectPlatformFromScheme; keep existing behavior when one param
is provided or only one default exists, and ensure scheme still falls back to
defaults.scheme.
🧹 Nitpick comments (12)
src/utils/platform-detection.ts (1)
41-51:settingNameis internally controlled, but consider a minor hardening.Static analysis flags a potential ReDoS via
new RegExp(...)constructed from a variable. In practice,extractBuildSettingValuesis only called with literal strings ('SDKROOT','SUPPORTED_PLATFORMS'), so this is safe today. If this helper were ever exported or called with external input, the unescapedsettingNamewould become exploitable. A light safeguard would be to escape the input or mark the function's contract explicitly.This is not blocking — just a defensive note for future maintainability.
src/utils/__tests__/platform-detection.test.ts (1)
6-115: Good coverage — consider adding a test for the "neither path provided" error path.The tests thoroughly cover SDKROOT detection, fallback to
SUPPORTED_PLATFORMS, non-simulator SDKROOTs, multi-block output, mutual exclusivity, and command failure. One untested branch is when bothprojectPathandworkspacePathareundefined, which returns an error at lines 74–81 ofplatform-detection.ts.src/utils/simulator-resolver.ts (1)
75-121: Consider extracting shared simctl device-lookup logic to reduce duplication.
resolveSimulatorIdToNameandresolveSimulatorNameToId(lines 20–66) are nearly identical — the only differences are thefindpredicate and the log messages. A shared internal helper (e.g.findDevice(executor, predicate)) would eliminate the duplicated executor call, JSON parsing, and iteration logic.♻️ Sketch of a shared helper
+type DevicePredicate = (device: { udid: string; name: string }) => boolean; + +async function findSimulatorDevice( + executor: CommandExecutor, + predicate: DevicePredicate, +): Promise<{ udid: string; name: string } | { error: string }> { + const result = await executor( + ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'], + 'List Simulators', + false, + ); + if (!result.success) { + return { error: `Failed to list simulators: ${result.error}` }; + } + let simulatorsData: { devices: Record<string, Array<{ udid: string; name: string }>> }; + try { + simulatorsData = JSON.parse(result.output) as typeof simulatorsData; + } catch (parseError) { + return { error: `Failed to parse simulator list: ${parseError}` }; + } + for (const runtime in simulatorsData.devices) { + const match = simulatorsData.devices[runtime].find(predicate); + if (match) return match; + } + return { error: 'not found' }; +}Both public functions would then delegate to this helper and only handle logging and result-shaping.
src/mcp/tools/simulator/build_sim.ts (1)
104-106: String manipulation forlogPrefixis slightly fragile.
detectedPlatform.replace(' Simulator', '')works for all current simulator platform enum values, but silently produces an odd prefix (e.g."macOS Simulator Build") ifinferPlatformever returns a non-simulator platform. Since this is a simulator-only tool the risk is low, but a minor guard or mapping would make it more robust.src/utils/__tests__/infer-platform.test.ts (1)
8-224: Missing test coverage for thesimulator-nameinference source.The
inferPlatformfunction has asimulator-namepath (usinginferPlatformFromSimulatorName) that triggers when simctl lookup fails but a recognisable simulator name is provided. None of the current tests exercise this source. Consider adding a test where simctl returns empty devices and a descriptivesimulatorName(e.g."Apple Watch Ultra 2") is provided — the expected result should be{ platform: watchOSSimulator, source: 'simulator-name' }.📝 Suggested test case
+ it('infers platform from simulator name when runtime lookup returns no match', async () => { + const mockExecutor: CommandExecutor = async () => + createMockCommandResponse({ + success: true, + output: JSON.stringify({ devices: {} }), + }); + + const result = await inferPlatform({ simulatorName: 'Apple Watch Ultra 2' }, mockExecutor); + + expect(result.platform).toBe(XcodePlatform.watchOSSimulator); + expect(result.source).toBe('simulator-name'); + });src/mcp/tools/simulator/__tests__/build_sim.test.ts (1)
187-196:createTrackingExecutorreturn type is implicit — consider annotating it.The returned async function is structurally compatible with
CommandExecutorbut not explicitly typed, which means a future signature change could silently break these tests without a compile-time error.Minor type annotation
- function createTrackingExecutor(callHistory: Array<{ command: string[]; logPrefix?: string }>) { - return async (command: string[], logPrefix?: string) => { + function createTrackingExecutor(callHistory: Array<{ command: string[]; logPrefix?: string }>): CommandExecutor { + return async (command, logPrefix) => {docs/investigations/platform-inference-staleness-and-xor-normalization.md (1)
31-37: Duplicate heading names trigger markdownlint MD024 warnings.Each domain section reuses the same sub-heading names (
Current State (verified),Decision,Required Invariants, etc.). While the structure is clear, this causes MD024 violations and makes anchor-link navigation ambiguous. Consider prefixing sub-headings with the domain name, e.g.### Domain 1: Current Stateor using###level consistently to differentiate.src/runtime/bootstrap-runtime.ts (1)
36-52: Hydration function does not pass an executor to the background refresh.
scheduleSimulatorDefaultsRefreshis called without anexecutorproperty. In this context,refreshSimulatorDefaultswill fall back togetDefaultCommandExecutor()(line 47 ofsimulator-defaults-refresh.ts), which is likely fine for MCP runtime. However,bootstrapRuntimealready has aFileSystemExecutor(fs) available. If the intent is to allow test injection of the executor for bootstrap flows, this omission may make the hydration path harder to test in isolation.src/mcp/tools/session-management/session_set_defaults.ts (1)
131-143: Background refresh is scheduled even when the selector hasn't changed.When
selectorProvidedis true butselectorChangedis false (i.e., the user re-sets the same simulator ID/name), a background refresh is still scheduled. This is harmless — it may resolve previously missing counterparts or a missing platform — but it's worth noting as intentional if the goal is self-healing.src/mcp/tools/simulator/build_run_sim.ts (1)
168-169: Unnecessary intermediate variableplatformDestination.
platformDestinationon line 168 is simply assigneddetectedPlatformwith no transformation. You could usedetectedPlatformdirectly in lines 189, 191, and 194, which would reduce cognitive overhead.src/utils/simulator-defaults-refresh.ts (1)
20-22: Test-environment skip is a pragmatic choice, but consider making it injectable.
shouldSkipBackgroundRefreshchecksNODE_ENVandVITESTenvironment variables to skip scheduling in tests. This works but couples the module to environment variables. If testability becomes a concern later, an injectable skip predicate could be cleaner.src/utils/infer-platform.ts (1)
170-228:inferPlatformFromSimctl— solid defensive handling, one small robustness nit.The error handling (simctl failure, JSON parse errors, missing
deviceskey, non-array device lists, non-object entries) is thorough. The availability belt-and-suspenders check on line 221 is a nice touch even though theavailablefilter is already in the command.One minor point:
result.outputmay be typed asstring | undefined.JSON.parse(undefined)throws aSyntaxErrorthat is caught by thecatchblock, so correctness is fine, but guarding with a fallback would make intent clearer and avoid a misleading stack trace:- parsed = JSON.parse(result.output); + parsed = JSON.parse(result.output ?? '');
Run simulatorId/simulatorName resolution and platform inference asynchronously during MCP startup and session-set-defaults so tool execution is not blocked. Cache simulatorPlatform, clear stale selector fields when the selector changes, and apply async refresh results only when the session revision still matches. Persist startup hydration refresh results to self-heal stale config defaults. Co-Authored-By: Claude <noreply@anthropic.com>
a84063d to
a32ed1a
Compare
Resolve project, workspace, and derived data paths to absolute paths before assembling xcodebuild commands so relative CLI inputs keep working when cwd changes. Refresh MCPTest integration fixture expectations to the current simulator UUID/name so parser and Xcode defaults sync tests remain deterministic.
|
Addressed the open review issues in the current branch:
Also added/updated tests for the changed behavior:
Validation run locally:
|
Return explicit scheduling status from simulator defaults refresh so MCP bootstrap logs only claim refresh scheduling when one is actually queued. Tighten session default notices so cleared selector messages only appear when a counterpart existed, and changed messages only appear on actual selector changes. Also prefer workspace defaults over project defaults when both are present, remove an unnecessary infer-platform export, and drop emoji from simulator build/run user-facing success strings. Co-Authored-By: Codex <noreply@openai.com>
Return explicit hydration status so bootstrap logging reflects what happened. Log hydration even when background simulator refresh is not scheduled. Also remove a trivial simulator selector pass-through in infer-platform to reduce indirection, and add coverage for scheme-only defaults. Refs #206 Co-Authored-By: Codex <noreply@openai.com>
Run xcodemake with per-process cwd instead of process.chdir to prevent cross-request working-directory races in concurrent execution. Add regression tests to verify executeXcodemakeCommand leaves process cwd unchanged and forwards cwd to the command executor. Refs #206
Return explicit hydration status so bootstrap logging reflects what happened. Log hydration even when background simulator refresh is not scheduled. Also remove a trivial simulator selector pass-through in infer-platform to reduce indirection, and add coverage for scheme-only defaults. Refs #206 Co-Authored-By: Codex <noreply@openai.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
|
|
||
| expect(process.cwd()).toBe(originalCwd); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
New test file uses banned Vitest mocking APIs
Medium Severity
This newly added test file uses vi.hoisted, vi.fn, vi.mock, executorMock.mockReset, executorMock.mockResolvedValue, and executorMock.mockRejectedValue, all of which are banned by the project's testing rules. The rule states: "Ban on Vitest mocking (vi.mock, vi.fn, vi.spyOn, .mock*) ⇒ critical. Use createMockExecutor / createMockFileSystemExecutor." The root cause is that executeXcodemakeCommand calls getDefaultCommandExecutor() internally without accepting an injected executor, forcing the test to use module mocking instead of dependency injection.


Improve simulator platform inference and session-default selector handling so simulator tools stay correct, deterministic, and non-blocking.
The earlier approach in #196 solved a key part of the problem by introducing build-settings-based detection, but we still had architectural gaps: stale selector/platform state in session defaults, blocking name/id lookups on startup/update paths, and inconsistent normalization between config/session/tool boundaries.
This PR keeps the useful detection direction while broadening the fix to include a runtime-first inference path (simctl metadata first, build settings fallback), cached simulator platform reuse with staleness invalidation, MCP-only hydration behavior, and background refresh with revision guards to avoid stale async writes.
Alternative considered: keep platform detection only in build-settings lookups and leave selector resolution synchronous. That was simpler but still left stale state risks and avoidable latency in the critical path.
Refs #196
Note
Medium Risk
Changes simulator destination/platform inference and session-default hydration/refresh behavior, which can affect build/test targeting across iOS/watchOS/tvOS/visionOS. Background refresh with revision-guards reduces stale writes, but introduces new async paths and caching that need coverage (added tests help).
Overview
Simulator tools now infer the correct simulator platform (iOS/watchOS/tvOS/visionOS) using
simctlruntime metadata first, then fallback to build-settings detection, with an optional cachedsimulatorPlatformin session defaults (inferPlatform,platform-detection).build_sim,build_run_sim, andtest_simwere updated to use this inference forxcodebuild -destinationand logging, enabling non-iOS simulator targeting.Session defaults handling is reworked to be deterministic for CLI/daemon:
bootstrapRuntimeonly hydratessessionDefaultsintosessionStorefor themcpruntime, and schedules an async simulator metadata refresh instead of blocking on selector resolution.session_set_defaultssimilarly stops synchronous name/id resolution, clears stale selector/platform fields on change, and schedules a revision-guarded background refresh that can also persist updatedsimulatorId/simulatorName/simulatorPlatform.Build execution now resolves relative
projectPath/workspacePath/derivedDataPathto absolute paths before invokingxcodebuild, andxcodemakeexecution no longer mutatesprocess.cwd()(uses child-processcwdinstead). Docs timestamps and several fixtures/tests were updated/added to cover the new inference, hydration boundaries, and path/cwd behavior.Written by Cursor Bugbot for commit a0c10d0. This will update automatically on new commits. Configure here.