Skip to content

Commit 76c9848

Browse files
committed
fix: address PR review feedback, restore responses barrel, update DI 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
1 parent bee0b39 commit 76c9848

9 files changed

Lines changed: 79 additions & 32 deletions

File tree

.cursor/BUGBOT.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ For full details see [README.md](README.md) and [docs/ARCHITECTURE.md](docs/ARCH
1212
## 1. Security Checklist — Critical
1313

1414
* No hard-coded secrets, tokens or DSNs.
15-
* All shell commands must flow through `CommandExecutor` with validated arguments (no direct `child_process` calls).
15+
* MCP tool logic functions that orchestrate long-running processes with sub-processes (e.g., `xcodebuild`) must flow through `CommandExecutor` with validated arguments. Standalone utility modules that invoke simple, short-lived commands may use direct `child_process`/`fs` imports and standard vitest mocking.
1616
* Paths must be sanitised via helpers in `src/utils/validation.ts`.
1717
* Sentry breadcrumbs / logs must **NOT** include user PII.
1818

@@ -22,7 +22,7 @@ For full details see [README.md](README.md) and [docs/ARCHITECTURE.md](docs/ARCH
2222

2323
| Rule | Quick diff heuristic |
2424
|------|----------------------|
25-
| Dependency injection only | New `child_process` \| `fs` import ⇒ **critical** |
25+
| Dependency injection for tool logic | New `child_process` \| `fs` import in MCP tool logic **warning** (check if process is complex/long-running) |
2626
| Handler / Logic split | `handler` > 20 LOC or contains branching ⇒ **critical** |
2727
| Plugin auto-registration | Manual `registerTool(...)` / `registerResource(...)`**critical** |
2828

@@ -45,7 +45,8 @@ export const handler = (p: FooBarParams) => fooBarLogic(p);
4545

4646
## 3. Testing Checklist
4747

48-
* **External-boundary rule**: Use `createMockExecutor` / `createMockFileSystemExecutor` for command execution and filesystem side effects.
48+
* **External-boundary rule for tool logic**: Use `createMockExecutor` / `createMockFileSystemExecutor` for complex process orchestration (xcodebuild, multi-step pipelines) in MCP tool logic functions.
49+
* **Simple utilities**: Standalone utility modules with simple command calls can use direct imports and standard vitest mocking.
4950
* **Internal mocking is allowed**: `vi.mock`, `vi.fn`, `vi.spyOn`, and `.mock*` are acceptable for internal modules/collaborators.
5051
* Each tool must have tests covering happy-path **and** at least one failure path.
5152
* Avoid the `any` type unless justified with an inline comment.
@@ -66,15 +67,15 @@ export const handler = (p: FooBarParams) => fooBarLogic(p);
6667
|--------------|--------------------|
6768
| Complex logic in `handler` | Move to `*Logic` function |
6869
| Re-implementing logging | Use `src/utils/logger.ts` |
69-
| Direct `fs` / `child_process` usage | Inject `FileSystemExecutor` / `CommandExecutor` |
70+
| Direct `fs` / `child_process` in tool logic orchestrating complex processes | Inject `FileSystemExecutor` / `CommandExecutor` |
7071
| Chained re-exports | Export directly from source |
7172

7273
---
7374

7475
### How Bugbot Can Verify Rules
7576

7677
1. **External-boundary violations**: confirm tests use injected executors/filesystem for external side effects.
77-
2. **DI compliance**: search for direct `child_process` / `fs` imports outside approved patterns.
78+
2. **DI compliance**: check direct `child_process` / `fs` imports in MCP tool logic; standalone utilities with simple commands are acceptable.
7879
3. **Docs accuracy**: compare `docs/TOOLS.md` against `src/mcp/tools/**`.
7980
4. **Style**: ensure ESLint and Prettier pass (`npm run lint`, `npm run format:check`).
8081

docs/dev/ARCHITECTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,13 @@ Not all parts are required for every tool. For example, `swift_package_build` ha
418418

419419
### Testing Principles
420420

421-
XcodeBuildMCP uses a **Dependency Injection (DI)** pattern for testing external boundaries (command execution, filesystem, and other side effects). Vitest mocking libraries (`vi.mock`, `vi.fn`, etc.) are acceptable for internal collaborators when needed. This keeps tests robust while preserving deterministic behavior at external boundaries.
421+
XcodeBuildMCP uses a **Dependency Injection (DI)** pattern for MCP tool logic functions that orchestrate complex, long-running processes with sub-processes (e.g., `xcodebuild`), where standard vitest mocking produces race conditions. Standalone utility modules with simple commands may use direct `child_process`/`fs` imports and standard vitest mocking. Vitest mocking libraries (`vi.mock`, `vi.fn`, etc.) are also acceptable for internal collaborators.
422422

423423
For detailed guidelines, see the [Testing Guide](TESTING.md).
424424

425425
### Test Structure Example
426426

427-
Tests inject mock "executors" for external interactions like command-line execution or file system access. This allows for deterministic testing of tool logic without mocking the implementation itself. The project provides helper functions like `createMockExecutor` and `createMockFileSystemExecutor` in `src/test-utils/mock-executors.ts` to facilitate this pattern.
427+
Tests for MCP tool logic inject mock "executors" for complex process orchestration (e.g., xcodebuild). This allows for deterministic testing without race conditions from non-deterministic sub-process ordering. The project provides helper functions like `createMockExecutor` and `createMockFileSystemExecutor` in `src/test-utils/mock-executors.ts`. Standalone utility modules with simple commands use standard vitest mocking.
428428

429429
```typescript
430430
import { describe, it, expect } from 'vitest';

docs/dev/CODE_QUALITY.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,18 @@ XcodeBuildMCP enforces several architectural patterns that cannot be expressed t
5353

5454
### 1. Dependency Injection Pattern
5555

56-
**Rule**: All tools must use dependency injection for external interactions.
56+
**Rule**: MCP tool logic functions that orchestrate complex, long-running processes with sub-processes (e.g., `xcodebuild`) must use dependency injection for external interactions. This is because standard vitest mocking produces race conditions when sub-process ordering is non-deterministic.
57+
58+
Standalone utility modules that invoke simple, short-lived commands (e.g., `xcrun devicectl list`, `xcrun xcresulttool get`) may use direct `child_process`/`fs` imports and be tested with standard vitest mocking.
5759

5860
**Allowed**:
59-
- `createMockExecutor()` for command execution mocking
60-
- `createMockFileSystemExecutor()` for file system mocking
61-
- Logic functions accepting `executor?: CommandExecutor` parameter
61+
- `createMockExecutor()` / `createMockFileSystemExecutor()` for complex process orchestration in tool logic
62+
- Logic functions accepting `executor?: CommandExecutor` parameter for xcodebuild and similar pipelines
63+
- Direct `child_process`/`fs` imports in standalone utility modules with simple commands, tested via vitest mocking
6264

6365
**Forbidden**:
64-
- Direct calls to `execSync`, `spawn`, or `exec` in production code
6566
- Testing handler functions directly
66-
- Real external side effects in unit tests (xcodebuild/xcrun/filesystem writes outside mocks)
67+
- Real external side effects in unit tests (real `xcodebuild`, `xcrun`, AXe, filesystem writes/reads outside test harness)
6768

6869
### 2. Handler Signature Compliance
6970

docs/dev/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ Before making changes, please familiarize yourself with:
270270
All contributions must adhere to the testing standards outlined in the [**XcodeBuildMCP Plugin Testing Guidelines (TESTING.md)**](TESTING.md). This is the canonical source of truth for all testing practices.
271271

272272
**Key Principles (Summary):**
273-
- **Dependency Injection for External Boundaries**: All external dependencies (command execution, file system access) must be injected into tool logic functions using the `CommandExecutor` and `FileSystemExecutor` patterns.
273+
- **Dependency Injection for Complex Processes**: MCP tool logic functions that orchestrate complex, long-running processes with sub-processes (e.g., `xcodebuild`) must use injected `CommandExecutor` and `FileSystemExecutor` patterns. Standalone utility modules with simple commands may use direct imports and standard vitest mocking.
274274
- **Internal Mocking Is Allowed**: Vitest mocking (`vi.mock`, `vi.fn`, `vi.spyOn`, etc.) is acceptable for internal modules/collaborators.
275275
- **Test Production Code**: Tests must import and execute the actual tool logic, not mock implementations.
276276
- **Comprehensive Coverage**: Tests must cover input validation, command generation, and output processing.

docs/dev/TESTING.md

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,32 @@ This document provides comprehensive testing guidelines for XcodeBuildMCP plugin
2020

2121
### 🚨 CRITICAL: External Dependency Mocking Rules
2222

23-
### ABSOLUTE RULE: External side effects must use dependency injection utilities
23+
### When to use dependency-injection executors
2424

25-
### Use dependency-injection mocks for EXTERNAL dependencies:
26-
- `createMockExecutor()` / `createNoopExecutor()` for command execution (`xcrun`, `xcodebuild`, AXe, etc.)
27-
- `createMockFileSystemExecutor()` / `createNoopFileSystemExecutor()` for file system interactions
25+
`CommandExecutor` / `FileSystemExecutor` DI is required for **MCP tool logic functions** that orchestrate complex, long-running processes with sub-processes (e.g., `xcodebuild`, multi-step build pipelines). Standard vitest mocking produces race conditions with these because sub-process ordering is non-deterministic.
26+
27+
- `createMockExecutor()` / `createNoopExecutor()` for command execution in tool logic
28+
- `createMockFileSystemExecutor()` / `createNoopFileSystemExecutor()` for file system interactions in tool logic
29+
30+
### When standard vitest mocking is fine
31+
32+
Standalone utility modules that invoke simple, short-lived commands (e.g., `xcrun devicectl list`, `xcrun xcresulttool get`) may use direct `child_process`/`fs` imports and be tested with standard vitest mocking (`vi.fn`, `vi.mock`, `vi.spyOn`, etc.). This is simpler and perfectly adequate for deterministic, single-command utilities.
2833

2934
### Internal mocking guidance:
3035
- Vitest mocking (`vi.fn`, `vi.mock`, `vi.spyOn`, `.mockResolvedValue`, etc.) is allowed for internal modules and in-memory collaborators
3136
- Prefer straightforward, readable test doubles over over-engineered mocks
3237

3338
### Still forbidden:
3439
- Hitting real external systems in unit tests (real `xcodebuild`, `xcrun`, AXe, filesystem writes/reads outside test harness)
35-
- Bypassing dependency injection for external effects
3640

3741
### OUR CORE PRINCIPLE
3842

39-
**Simple Rule**: Use dependency-injection mock executors for external boundaries; use Vitest mocking only for internal behavior.
43+
**Simple Rule**: Use dependency-injection mock executors for complex process orchestration in tool logic; use standard vitest mocking for simple utility modules and internal behavior.
4044

4145
**Why This Rule Exists**:
42-
1. **Reliability**: External side effects stay deterministic and hermetic
43-
2. **Clarity**: Internal collaboration assertions remain concise and readable
44-
3. **Architectural Enforcement**: External boundaries are explicit in tool logic signatures
46+
1. **Reliability**: Complex multi-process orchestration stays deterministic and hermetic via DI executors
47+
2. **Simplicity**: Simple utilities use standard vitest mocking without unnecessary abstraction
48+
3. **Architectural Enforcement**: External boundaries for complex processes are explicit in tool logic signatures
4549
4. **Maintainability**: Tests fail for behavior regressions, not incidental environment differences
4650

4751
### Integration Testing with Dependency Injection
@@ -111,7 +115,7 @@ Test → Plugin Handler → utilities → [DEPENDENCY INJECTION] createMockExecu
111115

112116
### Handler Requirements
113117

114-
All plugin handlers must support dependency injection:
118+
MCP tool logic functions that orchestrate complex processes must support dependency injection:
115119

116120
```typescript
117121
export function tool_nameLogic(
@@ -134,12 +138,9 @@ export default {
134138
};
135139
```
136140

137-
**Important**: The dependency injection pattern applies to ALL handlers, including:
138-
- Tool handlers
139-
- Resource handlers
140-
- Any future handler types (prompts, etc.)
141+
**Important**: The dependency injection pattern applies to tool and resource handler logic that orchestrates complex, long-running processes (e.g., `xcodebuild`). Standalone utility modules with simple commands may use direct imports and standard vitest mocking.
141142

142-
Always use default parameter values (e.g., `= getDefaultCommandExecutor()`) to ensure production code works without explicit executor injection, while tests can override with mock executors.
143+
Always use default parameter values (e.g., `= getDefaultCommandExecutor()`) in tool logic to ensure production code works without explicit executor injection, while tests can override with mock executors.
143144

144145
### Test Requirements
145146

src/types/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export interface PlatformBuildOptions {
137137
simulatorId?: string;
138138
deviceId?: string;
139139
useLatestOS?: boolean;
140+
packageCachePath?: string;
140141
arch?: string;
141142
logPrefix: string;
142143
}

src/utils/__tests__/xcodebuild-pipeline.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
1+
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
22
import { createXcodebuildPipeline } from '../xcodebuild-pipeline.ts';
33
import { STAGE_RANK } from '../../types/pipeline-events.ts';
44
import type { PipelineEvent } from '../../types/pipeline-events.ts';
@@ -146,7 +146,7 @@ describe('xcodebuild-pipeline', () => {
146146
truncated: false,
147147
});
148148

149-
const result = pipeline.finalize(true, 100);
149+
pipeline.finalize(true, 100);
150150

151151
const discoveryEvents = emittedEvents.filter((e) => e.type === 'test-discovery');
152152
expect(discoveryEvents).toHaveLength(1);

src/utils/responses/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export { createTextResponse } from '../validation.ts';
2+
export {
3+
createErrorResponse,
4+
DependencyError,
5+
AxeError,
6+
SystemError,
7+
ValidationError,
8+
} from '../errors.ts';
9+
export {
10+
processToolResponse,
11+
renderNextStep,
12+
renderNextStepsSection,
13+
} from './next-steps-renderer.ts';
14+
15+
export type { ToolResponse, NextStep, OutputStyle } from '../../types/common.ts';

src/utils/responses/next-steps-renderer.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { RuntimeKind } from '../../runtime/types.ts';
2-
import type { NextStep } from '../../types/common.ts';
2+
import type { NextStep, OutputStyle, ToolResponse } from '../../types/common.ts';
33
import { toKebabCase } from '../../runtime/naming.ts';
44

55
function resolveLabel(step: NextStep): string {
@@ -90,3 +90,31 @@ export function renderNextStepsSection(steps: NextStep[], runtime: RuntimeKind):
9090

9191
return `Next steps:\n${lines.join('\n')}`;
9292
}
93+
94+
export function processToolResponse(
95+
response: ToolResponse,
96+
runtime: RuntimeKind,
97+
style: OutputStyle = 'normal',
98+
): ToolResponse {
99+
const { nextSteps, ...rest } = response;
100+
101+
if (!nextSteps || nextSteps.length === 0 || style === 'minimal') {
102+
return { ...rest };
103+
}
104+
105+
const nextStepsSection = renderNextStepsSection(nextSteps, runtime);
106+
107+
const processedContent = response.content.map((item, index) => {
108+
if (item.type === 'text' && index === response.content.length - 1) {
109+
return { ...item, text: item.text + nextStepsSection };
110+
}
111+
return item;
112+
});
113+
114+
const hasTextContent = response.content.some((item) => item.type === 'text');
115+
if (!hasTextContent && nextStepsSection) {
116+
processedContent.push({ type: 'text', text: nextStepsSection.trim() });
117+
}
118+
119+
return { ...rest, content: processedContent };
120+
}

0 commit comments

Comments
 (0)