Skip to content

refactor(4/12): extract build/test utilities, platform steps, and xcodebuild pipeline#322

Open
cameroncooke wants to merge 1 commit intorefactor/rendering-enginefrom
refactor/build-test-utility-extraction
Open

refactor(4/12): extract build/test utilities, platform steps, and xcodebuild pipeline#322
cameroncooke wants to merge 1 commit intorefactor/rendering-enginefrom
refactor/build-test-utility-extraction

Conversation

@cameroncooke
Copy link
Copy Markdown
Collaborator

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 logs
  • device-steps.ts: Prepare device, install, launch
  • macos-steps.ts: Build, launch, capture logs
  • app-path-resolver.ts: Locate built .app bundles in DerivedData (shared across all platforms)
  • device-name-resolver.ts: Resolve device UDID to display name

Xcodebuild pipeline (xcodebuild-pipeline.ts): The core build/test execution engine. Accepts an emit callback 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 to test-preflight.ts and swift-test-discovery.ts.

Stack navigation

  • PR 1-3/12: Foundation (logging removal, event types, rendering engine)
  • PR 4/12 (this PR): Build/test utility extraction, platform steps, xcodebuild pipeline
  • PR 5/12: Runtime handler contract and tool invoker
  • PR 6-9/12: Tool migrations (these use the extracted modules)
  • PR 10-12/12: Boundaries, config, tests

Test plan

  • npx vitest run passes -- tests for each extracted module
  • Build preflight correctly rejects invalid schemes/destinations
  • Pipeline streams events through emit callback during execution
  • Xcodebuild output constructs correct event sequences from pipeline state

import { tmpdir } from 'node:os';
import { join } from 'node:path';

let cachedDevices: Map<string, string> | null = null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Triggered by project rule: Bugbot Review Guide for XcodeBuildMCP

Reviewed by Cursor Bugbot for commit 344aeba. Configure here.

@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 344aeba to f7616d2 Compare April 8, 2026 21:29
@cameroncooke cameroncooke force-pushed the refactor/rendering-engine branch from d00ff56 to 40b05c5 Compare April 8, 2026 21:29
Comment on lines +158 to +166
export function startBuildPipeline(
options: PipelineOptions & { message: string },
): StartedPipeline {
const emit =
options.emit ??
(() => {
try {
return getHandlerContext().emit;
} catch {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

There are 6 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ 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;
};
}>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f7616d2. Configure here.

appLog(
'info',
`[Pipeline] ${debugCapture.count} unrecognized parser lines written to ${debugPath}`,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Triggered by project rule: Bugbot Review Guide for XcodeBuildMCP

Reviewed by Cursor Bugbot for commit f7616d2. Configure here.

...pipelineResult,
events: [...pipelineResult.events, ...fallbackEvents],
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f7616d2. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant