Skip to content

Commit 79cb6c0

Browse files
cameroncookecodex
andcommitted
fix(snapshot-tests): Align MCP and CLI transcript fixtures
Route shared xcodebuild snapshot scenarios through canonical CLI fixtures and make MCP durable text rendering use the same non-interactive transcript formatter. This removes MCP-specific newline drift and keeps test discovery formatting consistent across success and failure paths. Also surface selective test targeting in test headers and limit discovery previews to the first six tests so the canonical fixtures stay readable while preserving parity across runtimes. Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 713a2dc commit 79cb6c0

297 files changed

Lines changed: 5284 additions & 2265 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"mcpServers": {
3+
"XcodeBuildMCP": {
4+
"type": "stdio",
5+
"command": "node",
6+
"args": [
7+
"../../build/cli.js",
8+
"mcp"
9+
],
10+
"env": {}
11+
}
12+
}
13+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
import type { PipelineEvent } from '../../types/pipeline-events.ts';
3+
import { renderEvents } from '../render.ts';
4+
import { createCliTextRenderer } from '../../utils/renderers/cli-text-renderer.ts';
5+
6+
function captureCliText(events: readonly PipelineEvent[]): string {
7+
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
8+
const renderer = createCliTextRenderer({ interactive: false });
9+
10+
for (const event of events) {
11+
renderer.onEvent(event);
12+
}
13+
renderer.finalize();
14+
15+
return stdoutWrite.mock.calls.flat().join('');
16+
}
17+
18+
describe('text render parity', () => {
19+
afterEach(() => {
20+
vi.restoreAllMocks();
21+
});
22+
23+
it('matches non-interactive cli text for discovery and summary output', () => {
24+
const events: PipelineEvent[] = [
25+
{
26+
type: 'header',
27+
timestamp: '2026-04-10T22:50:00.000Z',
28+
operation: 'Test',
29+
params: [
30+
{ label: 'Scheme', value: 'CalculatorApp' },
31+
{ label: 'Configuration', value: 'Debug' },
32+
{ label: 'Platform', value: 'iOS Simulator' },
33+
],
34+
},
35+
{
36+
type: 'test-discovery',
37+
timestamp: '2026-04-10T22:50:01.000Z',
38+
operation: 'TEST',
39+
total: 1,
40+
tests: ['CalculatorAppTests/CalculatorAppTests/testAddition'],
41+
truncated: false,
42+
},
43+
{
44+
type: 'summary',
45+
timestamp: '2026-04-10T22:50:02.000Z',
46+
operation: 'TEST',
47+
status: 'SUCCEEDED',
48+
totalTests: 1,
49+
passedTests: 1,
50+
skippedTests: 0,
51+
durationMs: 1500,
52+
},
53+
];
54+
55+
expect(renderEvents(events, 'text')).toBe(captureCliText(events));
56+
});
57+
58+
it('matches non-interactive cli text for failure diagnostics and summary spacing', () => {
59+
const events: PipelineEvent[] = [
60+
{
61+
type: 'header',
62+
timestamp: '2026-04-10T22:50:00.000Z',
63+
operation: 'Test',
64+
params: [
65+
{ label: 'Scheme', value: 'MCPTest' },
66+
{ label: 'Configuration', value: 'Debug' },
67+
{ label: 'Platform', value: 'macOS' },
68+
],
69+
},
70+
{
71+
type: 'test-discovery',
72+
timestamp: '2026-04-10T22:50:01.000Z',
73+
operation: 'TEST',
74+
total: 2,
75+
tests: [
76+
'MCPTestTests/MCPTestTests/appNameIsCorrect',
77+
'MCPTestTests/MCPTestsXCTests/testAppNameIsCorrect',
78+
],
79+
truncated: false,
80+
},
81+
{
82+
type: 'test-failure',
83+
timestamp: '2026-04-10T22:50:02.000Z',
84+
operation: 'TEST',
85+
suite: 'MCPTestsXCTests',
86+
test: 'testDeliberateFailure()',
87+
message: 'XCTAssertTrue failed',
88+
location: 'MCPTestsXCTests.swift:11',
89+
},
90+
{
91+
type: 'summary',
92+
timestamp: '2026-04-10T22:50:03.000Z',
93+
operation: 'TEST',
94+
status: 'FAILED',
95+
totalTests: 2,
96+
passedTests: 1,
97+
failedTests: 1,
98+
skippedTests: 0,
99+
durationMs: 2200,
100+
},
101+
];
102+
103+
expect(renderEvents(events, 'text')).toBe(captureCliText(events));
104+
});
105+
106+
it('renders next steps in canonical cli format for text transcripts', () => {
107+
const events: PipelineEvent[] = [
108+
{
109+
type: 'summary',
110+
timestamp: '2026-04-10T22:50:05.000Z',
111+
operation: 'BUILD',
112+
status: 'SUCCEEDED',
113+
durationMs: 7100,
114+
},
115+
{
116+
type: 'next-steps',
117+
timestamp: '2026-04-10T22:50:06.000Z',
118+
runtime: 'mcp',
119+
steps: [
120+
{
121+
label: 'Get built macOS app path',
122+
tool: 'get_mac_app_path',
123+
cliTool: 'get-app-path',
124+
workflow: 'macos',
125+
params: {
126+
scheme: 'MCPTest',
127+
},
128+
},
129+
],
130+
},
131+
];
132+
133+
const output = renderEvents(events, 'text');
134+
expect(output).toBe(captureCliText(events));
135+
expect(output).toContain('xcodebuildmcp macos get-app-path');
136+
expect(output).not.toContain('get_mac_app_path({');
137+
});
138+
});

src/rendering/render.ts

Lines changed: 7 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,9 @@
1-
import type {
2-
CompilerErrorEvent,
3-
CompilerWarningEvent,
4-
PipelineEvent,
5-
TestFailureEvent,
6-
} from '../types/pipeline-events.ts';
1+
import type { PipelineEvent } from '../types/pipeline-events.ts';
72
import { sessionStore } from '../utils/session-store.ts';
8-
import { deriveDiagnosticBaseDir } from '../utils/renderers/index.ts';
93
import {
10-
formatBuildStageEvent,
11-
formatDetailTreeEvent,
12-
formatFileRefEvent,
13-
formatGroupedCompilerErrors,
14-
formatGroupedTestFailures,
15-
formatGroupedWarnings,
16-
formatHeaderEvent,
17-
formatNextStepsEvent,
18-
formatSectionEvent,
19-
formatStatusLineEvent,
20-
formatSummaryEvent,
21-
formatTableEvent,
22-
formatTestDiscoveryEvent,
23-
} from '../utils/renderers/event-formatting.ts';
24-
import { createCliTextRenderer } from '../utils/renderers/cli-text-renderer.ts';
4+
createCliTextRenderer,
5+
renderCliTextTranscript,
6+
} from '../utils/renderers/cli-text-renderer.ts';
257
import type { RenderSession, RenderStrategy, ImageAttachment } from './types.ts';
268

279
function isErrorEvent(event: PipelineEvent): boolean {
@@ -34,124 +16,13 @@ function isErrorEvent(event: PipelineEvent): boolean {
3416
function createTextRenderSession(): RenderSession {
3517
const events: PipelineEvent[] = [];
3618
const attachments: ImageAttachment[] = [];
37-
const contentParts: string[] = [];
3819
const suppressWarnings = sessionStore.get('suppressWarnings');
39-
const groupedCompilerErrors: CompilerErrorEvent[] = [];
40-
const groupedWarnings: CompilerWarningEvent[] = [];
41-
const groupedTestFailures: TestFailureEvent[] = [];
42-
43-
let diagnosticBaseDir: string | null = null;
4420
let hasError = false;
4521

46-
const pushText = (text: string): void => {
47-
contentParts.push(text);
48-
};
49-
50-
const pushSection = (text: string): void => {
51-
pushText(`\n${text}`);
52-
};
53-
5422
return {
5523
emit(event: PipelineEvent): void {
5624
events.push(event);
5725
if (isErrorEvent(event)) hasError = true;
58-
59-
switch (event.type) {
60-
case 'header': {
61-
diagnosticBaseDir = deriveDiagnosticBaseDir(event);
62-
pushSection(formatHeaderEvent(event));
63-
break;
64-
}
65-
66-
case 'build-stage': {
67-
pushSection(formatBuildStageEvent(event));
68-
break;
69-
}
70-
71-
case 'status-line': {
72-
pushSection(formatStatusLineEvent(event));
73-
break;
74-
}
75-
76-
case 'section': {
77-
pushText(`\n\n${formatSectionEvent(event)}`);
78-
break;
79-
}
80-
81-
case 'detail-tree': {
82-
pushSection(formatDetailTreeEvent(event));
83-
break;
84-
}
85-
86-
case 'table': {
87-
pushSection(formatTableEvent(event));
88-
break;
89-
}
90-
91-
case 'file-ref': {
92-
pushSection(formatFileRefEvent(event));
93-
break;
94-
}
95-
96-
case 'compiler-warning': {
97-
if (!suppressWarnings) {
98-
groupedWarnings.push(event);
99-
}
100-
break;
101-
}
102-
103-
case 'compiler-error': {
104-
groupedCompilerErrors.push(event);
105-
break;
106-
}
107-
108-
case 'test-discovery': {
109-
pushText(formatTestDiscoveryEvent(event));
110-
break;
111-
}
112-
113-
case 'test-progress': {
114-
break;
115-
}
116-
117-
case 'test-failure': {
118-
groupedTestFailures.push(event);
119-
break;
120-
}
121-
122-
case 'summary': {
123-
const diagOpts = { baseDir: diagnosticBaseDir ?? undefined };
124-
const diagnosticSections: string[] = [];
125-
126-
if (groupedTestFailures.length > 0) {
127-
diagnosticSections.push(formatGroupedTestFailures(groupedTestFailures, diagOpts));
128-
groupedTestFailures.length = 0;
129-
}
130-
131-
if (groupedWarnings.length > 0) {
132-
diagnosticSections.push(formatGroupedWarnings(groupedWarnings, diagOpts));
133-
groupedWarnings.length = 0;
134-
}
135-
136-
if (event.status === 'FAILED' && groupedCompilerErrors.length > 0) {
137-
diagnosticSections.push(formatGroupedCompilerErrors(groupedCompilerErrors, diagOpts));
138-
groupedCompilerErrors.length = 0;
139-
}
140-
141-
if (diagnosticSections.length > 0) {
142-
pushSection(diagnosticSections.join('\n\n'));
143-
}
144-
145-
pushSection(formatSummaryEvent(event));
146-
break;
147-
}
148-
149-
case 'next-steps': {
150-
const effectiveRuntime = event.runtime === 'cli' ? 'cli' : 'mcp';
151-
pushText(`\n\n${formatNextStepsEvent(event, effectiveRuntime)}`);
152-
break;
153-
}
154-
}
15526
},
15627

15728
attach(image: ImageAttachment): void {
@@ -171,8 +42,9 @@ function createTextRenderSession(): RenderSession {
17142
},
17243

17344
finalize(): string {
174-
diagnosticBaseDir = null;
175-
return contentParts.join('');
45+
return renderCliTextTranscript(events, {
46+
suppressWarnings: suppressWarnings ?? false,
47+
});
17648
},
17749
};
17850
}

src/snapshot-tests/__fixtures__/coverage/get-coverage-report--error-invalid-bundle.txt renamed to src/snapshot-tests/__fixtures__/cli/coverage/get-coverage-report--error-invalid-bundle.txt

File renamed without changes.

src/snapshot-tests/__fixtures__/coverage/get-coverage-report--success.txt renamed to src/snapshot-tests/__fixtures__/cli/coverage/get-coverage-report--success.txt

File renamed without changes.

src/snapshot-tests/__fixtures__/coverage/get-file-coverage--error-invalid-bundle.txt renamed to src/snapshot-tests/__fixtures__/cli/coverage/get-file-coverage--error-invalid-bundle.txt

File renamed without changes.

src/snapshot-tests/__fixtures__/coverage/get-file-coverage--success.txt renamed to src/snapshot-tests/__fixtures__/cli/coverage/get-file-coverage--success.txt

File renamed without changes.

src/snapshot-tests/__fixtures__/debugging/add-breakpoint--error-no-session.txt renamed to src/snapshot-tests/__fixtures__/cli/debugging/add-breakpoint--error-no-session.txt

File renamed without changes.

src/snapshot-tests/__fixtures__/debugging/add-breakpoint--success.txt renamed to src/snapshot-tests/__fixtures__/cli/debugging/add-breakpoint--success.txt

File renamed without changes.

src/snapshot-tests/__fixtures__/debugging/attach--error-no-process.txt renamed to src/snapshot-tests/__fixtures__/cli/debugging/attach--error-no-process.txt

File renamed without changes.

0 commit comments

Comments
 (0)