refactor(4/12): extract build/test utilities, platform steps, and xcodebuild pipeline#322
Conversation
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
|
|
||
| let cachedDevices: Map<string, string> | null = null; |
There was a problem hiding this comment.
Bug: The module-level cachedDevices in device-name-resolver.ts is never invalidated, which can lead to displaying stale device names in a long-running server process.
Severity: MEDIUM
Suggested Fix
The cache should be invalidated or refreshed between tool invocations. One approach is to clear the cachedDevices map at the start of each tool run. Alternatively, introduce a time-to-live (TTL) for cache entries or re-fetch the device list if a UDID is not found in the current cache.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/utils/device-name-resolver.ts#L6
Potential issue: The `resolveDeviceName` function utilizes a module-level cache,
`cachedDevices`, which is populated once and persists for the entire process lifetime.
In a long-running server environment that handles multiple tool invocations, this cache
can become stale. If a device is disconnected, renamed, or reconnected between
invocations, the function will return the old, cached device name instead of the current
one. This results in incorrect device names being displayed in logs and user-facing
output, potentially causing confusion, although it does not affect the functional
targeting of devices by their UDID.
Did we get this right? 👍 / 👎 to inform future reviews.
| return `${name} (${deviceId})`; | ||
| } | ||
| return deviceId; | ||
| } |
There was a problem hiding this comment.
device-name-resolver blocks event loop with synchronous shell call
Medium Severity
device-name-resolver.ts directly imports execSync from node:child_process and readFileSync/unlinkSync from node:fs, bypassing CommandExecutor and FileSystemExecutor injection. The execSync call (with a 10-second timeout) blocks the Node.js event loop during normal test/build flows since it's called from both test-common.ts and xcodebuild-pipeline.ts. This violates the project's DI rules and makes the module untestable with mock executors.
Triggered by project rule: Bugbot Review Guide for XcodeBuildMCP
Reviewed by Cursor Bugbot for commit 344aeba. Configure here.
| @@ -0,0 +1,90 @@ | |||
| import { execFileSync } from 'node:child_process'; | |||
| import { log } from './logger.ts'; | |||
There was a problem hiding this comment.
xcresult-test-failures uses direct execFileSync bypassing DI
Medium Severity
xcresult-test-failures.ts directly imports and uses execFileSync from node:child_process instead of accepting a CommandExecutor parameter. This violates the project's dependency injection rules requiring all shell commands to flow through CommandExecutor, and makes the module impossible to test with createMockExecutor.
Triggered by project rule: Bugbot Review Guide for XcodeBuildMCP
Reviewed by Cursor Bugbot for commit 344aeba. Configure here.
344aeba to
f7616d2
Compare
d00ff56 to
40b05c5
Compare
| export function startBuildPipeline( | ||
| options: PipelineOptions & { message: string }, | ||
| ): StartedPipeline { | ||
| const emit = | ||
| options.emit ?? | ||
| (() => { | ||
| try { | ||
| return getHandlerContext().emit; | ||
| } catch { |
There was a problem hiding this comment.
Bug: The code imports and calls getHandlerContext and handlerContextStorage, but these functions are not defined or exported in typed-tool-factory.ts, which will cause a runtime ReferenceError.
Severity: CRITICAL
Suggested Fix
Define and export the getHandlerContext and handlerContextStorage functions from the src/utils/typed-tool-factory.ts file. Ensure their implementation aligns with the intended context management logic for the new event-driven architecture.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/utils/xcodebuild-pipeline.ts#L158-L166
Potential issue: The code imports `getHandlerContext` and `handlerContextStorage` from
`./typed-tool-factory.ts` in both `xcodebuild-pipeline.ts` and `test-common.ts`.
However, these functions are not defined or exported in the `typed-tool-factory.ts`
file. Consequently, any code path that calls these functions, such as `handleTestLogic`
or the fallback logic in `startBuildPipeline`, will fail at runtime with a
`ReferenceError`. This will crash the primary test execution path for any tool that
relies on this logic.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
There are 6 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f7616d2. Configure here.
| platform?: string; | ||
| osVersion?: string; | ||
| }; | ||
| }>; |
There was a problem hiding this comment.
resolveTestProgressEnabled exported but never used in production code
Low Severity
resolveTestProgressEnabled is exported and tested but never called from any production code path. The progress parameter is accepted by handleTestLogic but resolveTestProgressEnabled is not invoked anywhere within the function or elsewhere outside its own test file. This is dead code that adds confusion about how progress resolution is intended to work.
Reviewed by Cursor Bugbot for commit f7616d2. Configure here.
| appLog( | ||
| 'info', | ||
| `[Pipeline] ${debugCapture.count} unrecognized parser lines written to ${debugPath}`, | ||
| ); |
There was a problem hiding this comment.
debugCapture.count returns zero after flush() empties lines
Low Severity
In xcodebuild-pipeline.ts, after calling debugCapture.flush(), the log message references debugCapture.count to report how many unrecognized lines were written. However, looking at createParserDebugCapture in xcodebuild-log-capture.ts, the flush() method does not clear the lines array, so count still works. But the count getter returns lines.length which is correct. Actually the real issue is that flush() writes the file and returns the path but doesn't reset state — calling flush() twice would write duplicate files. This is a minor concern given finalize is called once.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f7616d2. Configure here.
| @@ -0,0 +1,87 @@ | |||
| import * as fs from 'node:fs'; | |||
| import * as os from 'node:os'; | |||
| import * as path from 'node:path'; | |||
There was a problem hiding this comment.
Direct filesystem access bypasses FileSystemExecutor injection pattern
Medium Severity
Both xcodebuild-log-capture.ts and simulator-steps.ts import node:fs directly and use synchronous filesystem operations (mkdirSync, openSync, writeSync, closeSync, readFileSync, etc.) without accepting a FileSystemExecutor parameter. This violates the project rule requiring dependency injection for filesystem side effects and prevents testing with createMockFileSystemExecutor.
Additional Locations (1)
Triggered by project rule: Bugbot Review Guide for XcodeBuildMCP
Reviewed by Cursor Bugbot for commit f7616d2. Configure here.
| ...pipelineResult, | ||
| events: [...pipelineResult.events, ...fallbackEvents], | ||
| }; | ||
| } |
There was a problem hiding this comment.
Fallback error events emitted to wrong emit target
Medium Severity
In finalizeInlineXcodebuild, when fallback error events need to be emitted, they are sent via options.emit?.(event) (line 218). However, several call sites in test-common.ts pass emit: ctx.emit while others don't pass emit at all. When emit is not provided, fallback events are added to the returned events array but never actually emitted to the handler context, resulting in silently dropped error content when tests fail without structured diagnostics.
Reviewed by Cursor Bugbot for commit f7616d2. Configure here.


Summary
This is PR 4 of 12 in a stacked PR series that decouples the rendering pipeline from MCP transport. Depends on PR 3 (rendering engine).
Extracts shared build and test logic from monolithic utility files into focused, single-responsibility modules. This is prep work for the tool handler migrations (PRs 6-9) -- the extracted modules provide the building blocks that tool handlers will call instead of duplicating logic inline.
What was extracted and why
Build preflight (
build-preflight.ts): Validates build parameters (project path, scheme, destination) before invoking xcodebuild. Previously scattered across individual tool handlers with subtle inconsistencies.Test preflight (
test-preflight.ts): Validates test parameters, resolves test plans, and checks for common misconfiguration. Extracted from the ~500-line test tool handlers where each platform (sim/device/macOS) had its own copy.Platform step modules: Each platform's build/test/run workflow was a monolithic function. Now decomposed into composable steps:
simulator-steps.ts: Boot simulator, install app, launch app, capture logsdevice-steps.ts: Prepare device, install, launchmacos-steps.ts: Build, launch, capture logsapp-path-resolver.ts: Locate built .app bundles in DerivedData (shared across all platforms)device-name-resolver.ts: Resolve device UDID to display nameXcodebuild pipeline (
xcodebuild-pipeline.ts): The core build/test execution engine. Accepts anemitcallback from the handler context, streams parsed events during execution, and returns structured state. This replaces the old pattern where the pipeline owned renderers directly.Xcodebuild output (
xcodebuild-output.ts): Constructs the final event sequence from pipeline state (summary, detail trees, diagnostics). Separated from the pipeline itself so the same output logic works for both real builds and test replay.Supporting extractions:
derived-data-path.ts,log-paths.ts,xcodebuild-log-capture.ts,swift-test-discovery.ts,xcresult-test-failures.ts,simulator-test-execution.ts,tool-error-handling.ts.Deleted modules
capabilities.ts,validation/index.ts,test-result-content.ts,workflow-selection.ts-- functionality moved into the extracted modules or no longer needed with the new architecture.Changes to existing modules
build-utils.ts: Slimmed down significantly. Warning suppression, content consolidation, and build-specific logic moved to dedicated modules.test-common.ts: Simplified to a thin coordination layer that delegates totest-preflight.tsandswift-test-discovery.ts.Stack navigation
Test plan
npx vitest runpasses -- tests for each extracted module