refactor(4/12): extract build/test utilities, platform steps, and xcodebuild pipeline#322
Conversation
344aeba to
f7616d2
Compare
d00ff56 to
40b05c5
Compare
40b05c5 to
df81928
Compare
f7616d2 to
7b1a15a
Compare
df81928 to
a0f22a7
Compare
7b1a15a to
379222d
Compare
| @@ -0,0 +1,69 @@ | |||
| import { execSync } from 'node:child_process'; | |||
| import { readFileSync, unlinkSync } from 'node:fs'; | |||
There was a problem hiding this comment.
Direct child_process imports bypass dependency injection
Medium Severity
Several new modules import execSync, execFileSync, spawn, readFileSync, and unlinkSync directly from node:child_process and node:fs instead of using the injected CommandExecutor and FileSystemExecutor. The project architecture rules flag new child_process or fs imports as critical DI violations, since all shell commands must flow through CommandExecutor and filesystem access through FileSystemExecutor.
Additional Locations (2)
Triggered by project rule: Bugbot Review Guide for XcodeBuildMCP
Reviewed by Cursor Bugbot for commit 379222d. Configure here.
There was a problem hiding this comment.
The DI guidelines have been updated in this PR (76c9848) to clarify that CommandExecutor/FileSystemExecutor DI is required for MCP tool logic orchestrating complex, long-running processes with sub-processes (e.g. xcodebuild), where standard vitest mocking produces race conditions. Standalone utility modules with simple, short-lived commands (like xcrun devicectl list here) may use direct imports and standard vitest mocking. See updated docs in BUGBOT.md, TESTING.md, CODE_QUALITY.md, CONTRIBUTING.md, and ARCHITECTURE.md.
a0f22a7 to
b7f35d4
Compare
379222d to
7ffc681
Compare
7ffc681 to
9d23583
Compare
b7f35d4 to
67d1e46
Compare
| '--level=debug', | ||
| '--predicate', | ||
| `subsystem == "${bundleId}"`, | ||
| ], |
There was a problem hiding this comment.
OSLog predicate allows bundle ID injection
Low Severity
In startOsLogStream, the bundleId is interpolated directly into a predicate string (subsystem == "${bundleId}") without sanitization. A bundleId containing a double-quote character could break the predicate syntax or alter its semantics. While bundleId typically comes from app metadata, the value flows from user-provided parameters through simctl launch output.
Triggered by project rule: Bugbot Review Guide for XcodeBuildMCP
Reviewed by Cursor Bugbot for commit 9d23583. Configure here.
There was a problem hiding this comment.
Out of scope for this PR. Bundle IDs follow Apple's reverse-DNS format and cannot contain double-quote characters. The predicate is passed as an array argument to spawn (not shell-interpolated), so there is no injection vector here.
a36e4d5 to
c0d77e5
Compare
2ac1bbb to
9b5d25f
Compare
9b5d25f to
96962ec
Compare
c0d77e5 to
7315fff
Compare
96962ec to
77c7b0b
Compare
7315fff to
80b323f
Compare
77c7b0b to
a173ad8
Compare
80b323f to
3126be4
Compare
27c8907 to
6716c5b
Compare
The module-level device name cache was never invalidated, causing stale device names in long-running server processes. Now expires after 30 seconds so it refreshes across tool invocations while avoiding repeated devicectl shell-outs within a single invocation.
3126be4 to
09a2d7d
Compare
a173ad8 to
200ac9c
Compare
Adds a shared runLogic function to test-helpers.ts that wraps tool logic execution in a mock handler context and returns a ToolResponse- shaped result. This will replace the identical helper duplicated across 49 test files in subsequent PRs.
09a2d7d to
bee0b39
Compare
| 'info', | ||
| `Starting test run for scheme ${params.scheme} on platform ${params.platform} (internal)`, | ||
| ); | ||
| const ctx = getHandlerContext(); |
There was a problem hiding this comment.
This is a Graphite stack artifact. getHandlerContext is imported from typed-tool-factory.ts where it will be defined in a later PR in this 12-PR stack. CodeQL analyzes each PR in isolation and flags it as undefined, but it will resolve when the full stack merges. Not actionable on this branch.
| try { | ||
| return await run(); | ||
| } catch (error) { | ||
| const message = toErrorMessage(error); |
There was a problem hiding this comment.
This is a Graphite stack artifact. toErrorMessage will be exported from errors.ts in a later PR in this 12-PR stack. CodeQL analyzes each PR in isolation and flags it as undefined, but it will resolve when the full stack merges. Not actionable on this branch.
| options.emit ?? | ||
| (() => { | ||
| try { | ||
| return getHandlerContext().emit; |
There was a problem hiding this comment.
This is a Graphite stack artifact. getHandlerContext will be exported from typed-tool-factory.ts in a later PR in this 12-PR stack. CodeQL analyzes each PR in isolation and flags it as undefined, but it will resolve when the full stack merges. Not actionable on this branch.
| ctx, | ||
| result: resultObj, | ||
| run: async <T>(fn: () => Promise<T>): Promise<T> => { | ||
| return handlerContextStorage.run(ctx, fn); |
There was a problem hiding this comment.
This is a Graphite stack artifact. handlerContextStorage will be exported from typed-tool-factory.ts in a later PR in this 12-PR stack. CodeQL analyzes each PR in isolation and flags it as undefined, but it will resolve when the full stack merges. Not actionable on this branch.
| try { | ||
| return getHandlerContext().emit; | ||
| } catch { | ||
| return handlerContextStorage.getStore()?.emit; |
There was a problem hiding this comment.
This is a Graphite stack artifact. handlerContextStorage will be exported from typed-tool-factory.ts in a later PR in this 12-PR stack. CodeQL analyzes each PR in isolation and flags it as undefined, but it will resolve when the full stack merges. Not actionable on this branch.
…guidelines - Add missing packageCachePath to PlatformBuildOptions interface - Remove unused vi import and result variable in pipeline test - Restore responses/index.ts barrel and processToolResponse function deleted in PR 3, which broke docs:check and downstream imports - Update DI documentation to clarify that CommandExecutor/FileSystemExecutor is required for complex process orchestration (xcodebuild) but not for simple utility modules
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 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 d279a9b. Configure here.
| const hasTextContent = response.content.some((item) => item.type === 'text'); | ||
| if (!hasTextContent && nextStepsSection) { | ||
| processedContent.push({ type: 'text', text: nextStepsSection.trim() }); | ||
| } |
There was a problem hiding this comment.
Next steps silently dropped when text isn't last content item
Medium Severity
processToolResponse only appends next steps to the last content item if it's a text item (item.type === 'text' && index === response.content.length - 1). When a response has text followed by non-text items (e.g. [text, image]), the text item at index 0 doesn't match the last-index check, and the image at the last index doesn't match the type check. The hasTextContent guard then prevents adding a new text block. Result: next steps are silently dropped whenever text content exists but isn't the final item.
Reviewed by Cursor Bugbot for commit d279a9b. Configure here.
Merge activity
|



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