Skip to content

Commit 6ca2dd7

Browse files
authored
Merge branch 'main' into issue_394
2 parents 0cc00a8 + 1ae1867 commit 6ca2dd7

23 files changed

Lines changed: 668 additions & 79 deletions

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ ESM TypeScript project (`type: module`). Key layers:
5656

5757

5858
## Rendering and Streaming Contract
59-
- Streaming fragments are transient output only. They MUST NOT be used as internal state, cached for final responses, or promoted into final MCP/JSON/CLI text output.
60-
- Non-streaming runtimes/output modes, including MCP final responses, MUST render only from the final structured result and next-step metadata. If final output needs data, add it to the final result type instead of reading it from fragments.
61-
- Only streaming-capable renderers may observe fragment callbacks, and only to print live progress. Their fragment handling must not affect final structured output or final rendered text.
59+
- Streaming fragments are transient live-progress output only. They may be displayed while a tool is running, but MUST NOT provide final settled MCP/JSON/CLI text.
60+
- Final settled output MUST render from the final structured/domain result and next-step metadata. If final output needs data, add it to the final result type instead of reading it from fragments.
61+
- Streaming-capable renderers may observe fragment callbacks only for live progress. Fragment handling must not affect final structured output or final settled text.
6262

6363
## Test Conventions
6464
- Vitest with colocated `__tests__/` directories using `*.test.ts`

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
- Fixed CLI test summaries showing false-positive compiler errors from xcodebuild NSError dump lines, and added compiler-error snapshot coverage for simulator, device, and macOS build-style flows ([#383](https://github.com/getsentry/XcodeBuildMCP/issues/383)).
1616
- Fixed simulator OSLog helper cleanup so server and daemon startup reconcile same-workspace orphaned log streams without stopping helpers owned by live sessions in other workspaces ([#382](https://github.com/getsentry/XcodeBuildMCP/issues/382)).
1717
- Fixed Weather example test discovery and made CLI test progress visible while tests are running instead of leaving the last build phase displayed.
18+
- Exposed xcresult bundle paths in test result structured output and text output when xcodebuild reports or is given a result bundle path, so agents can inspect test artifacts after simulator, device, and macOS test runs ([#392](https://github.com/getsentry/XcodeBuildMCP/issues/392)).
19+
- Fixed final test summaries to use xcresult top-level test declaration counts when available, avoiding overcounting dynamic-parameter test runs ([#392](https://github.com/getsentry/XcodeBuildMCP/issues/392)).
1820

1921
### Changed
2022

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ Use these sections under `## [Unreleased]`:
6666

6767

6868
## Rendering and Streaming Contract
69-
- Streaming fragments are transient output only. They MUST NOT be used as internal state, cached for final responses, or promoted into final MCP/JSON/CLI text output.
70-
- Non-streaming runtimes/output modes, including MCP final responses, MUST render only from the final structured result and next-step metadata. If final output needs data, add it to the final result type instead of reading it from fragments.
71-
- Only streaming-capable renderers may observe fragment callbacks, and only to print live progress. Their fragment handling must not affect final structured output or final rendered text.
69+
- Streaming fragments are transient live-progress output only. They may be displayed while a tool is running, but MUST NOT provide final settled MCP/JSON/CLI text.
70+
- Final settled output MUST render from the final structured/domain result and next-step metadata. If final output needs data, add it to the final result type instead of reading it from fragments.
71+
- Streaming-capable renderers may observe fragment callbacks only for live progress. Fragment handling must not affect final structured output or final settled text.
7272

7373
## Test Execution Rules
7474
- When running long test suites (snapshot tests, smoke tests), ALWAYS write full output to a log file and read it afterwards. NEVER pipe through `tail` or `grep` directly — that loses output you may need to debug failures.

schemas/structured-output/xcodebuildmcp.output.test-result/1.schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
},
7474
"packagePath": {
7575
"type": "string"
76+
},
77+
"xcresultPath": {
78+
"type": "string"
7679
}
7780
},
7881
"required": [],

src/mcp/tools/device/__tests__/test_device.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,34 @@ describe('test_device plugin', () => {
292292
expect(result.isError()).toBeFalsy();
293293
});
294294

295+
it('should expose user-provided result bundle paths in test output', async () => {
296+
const mockExecutor = createMockExecutor({
297+
success: true,
298+
output: 'Test Succeeded',
299+
});
300+
301+
const { result } = await runTestDeviceLogic(
302+
{
303+
projectPath: '/path/to/project.xcodeproj',
304+
scheme: 'MyScheme',
305+
deviceId: 'test-device-123',
306+
configuration: 'Debug',
307+
extraArgs: [
308+
'-resultBundlePath',
309+
'/tmp/Stale Device Tests.xcresult',
310+
'-resultBundlePath=/tmp/Device Tests.xcresult',
311+
],
312+
preferXcodebuild: false,
313+
platform: 'iOS',
314+
},
315+
mockExecutor,
316+
mockFs(),
317+
);
318+
319+
expectPendingBuildResponse(result);
320+
expect(result.text()).toContain('Result Bundle: /tmp/Device Tests.xcresult');
321+
});
322+
295323
it('should handle workspace testing successfully', async () => {
296324
const mockExecutor = createMockExecutor({
297325
success: true,

src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,24 @@ describe('swift_package_test plugin', () => {
147147
expect(result.isError()).toBeFalsy();
148148
});
149149

150+
it('should not expose xcresult paths from SwiftPM test output', async () => {
151+
const mockExecutor: CommandExecutor = async (_args, _name, _hideOutput, opts) => {
152+
opts?.onStdout?.('Result bundle written to: /tmp/SwiftPM.xcresult\n');
153+
return createMockCommandResponse({
154+
success: true,
155+
output: 'All tests passed.',
156+
});
157+
};
158+
159+
const { result } = await runSwiftPackageTestLogic(
160+
{ packagePath: '/test/package' },
161+
mockExecutor,
162+
);
163+
164+
expect(result.isError()).toBeFalsy();
165+
expect(result.text()).not.toContain('Result Bundle: /tmp/SwiftPM.xcresult');
166+
});
167+
150168
it('should return error response for test failure', async () => {
151169
const mockExecutor = createMockExecutor({
152170
success: false,

src/rendering/__tests__/text-render-parity.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,10 @@ describe('text render parity', () => {
249249
durationMs: 2200,
250250
counts: { passed: 1, failed: 1, skipped: 0 },
251251
},
252-
artifacts: { buildLogPath: '/tmp/Test.log' },
252+
artifacts: {
253+
buildLogPath: '/tmp/Test.log',
254+
xcresultPath: '/tmp/App Tests.xcresult',
255+
},
253256
diagnostics: {
254257
warnings: [],
255258
errors: [],
@@ -284,9 +287,59 @@ describe('text render parity', () => {
284287
expect(output.match(/Discovered 2 test\(s\):/g)).toHaveLength(1);
285288
expect(output.match(/MCPTestTests\n testTwo\(\):/g)).toHaveLength(1);
286289
expect(output.match(/1 test failed, 1 passed, 0 skipped/g)).toHaveLength(1);
290+
expect(output).toContain('Result Bundle: /tmp/App Tests.xcresult');
287291
expect(output).toContain('Build Logs: /tmp/Test.log');
288292
});
289293

294+
it('matches cli text and uses structured build summary when streamed build-summary disagrees', () => {
295+
const fixture: TranscriptFixture = {
296+
progressEvents: [
297+
{
298+
kind: 'build-result',
299+
fragment: 'invocation',
300+
operation: 'BUILD',
301+
request: {
302+
scheme: 'MyApp',
303+
projectPath: '/tmp/MyApp.xcodeproj',
304+
configuration: 'Debug',
305+
platform: 'iOS Simulator',
306+
},
307+
},
308+
{
309+
kind: 'build-result',
310+
fragment: 'build-summary',
311+
operation: 'BUILD',
312+
status: 'FAILED',
313+
durationMs: 9900,
314+
},
315+
],
316+
structuredOutput: {
317+
schema: 'xcodebuildmcp.output.build-result',
318+
schemaVersion: '1.0.0',
319+
result: {
320+
kind: 'build-result',
321+
didError: false,
322+
error: null,
323+
summary: { status: 'SUCCEEDED', durationMs: 3200 },
324+
artifacts: { scheme: 'MyApp', buildLogPath: '/tmp/build.log' },
325+
diagnostics: { warnings: [], errors: [] },
326+
},
327+
},
328+
};
329+
330+
const rendered = renderTranscript(
331+
{
332+
items: fixture.progressEvents,
333+
structuredOutput: fixture.structuredOutput,
334+
},
335+
'text',
336+
);
337+
338+
expect(rendered).toBe(captureCliText(fixture));
339+
expect(rendered).toContain('✅ Build succeeded. (⏱️ 3.2s)');
340+
expect(rendered).not.toContain('❌ Build failed. (⏱️ 9.9s)');
341+
});
342+
290343
it('renders next steps in MCP tool-call syntax for MCP runtime text transcripts', () => {
291344
const fixture: TranscriptFixture = {
292345
progressEvents: [],

src/snapshot-tests/__tests__/json-normalize.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ describe('normalizeStructuredEnvelope', () => {
3535
});
3636
});
3737

38+
it('preserves xcresult paths in test result artifacts', () => {
39+
const envelope: StructuredOutputEnvelope<unknown> = {
40+
schema: 'xcodebuildmcp.output.test-result',
41+
schemaVersion: '1',
42+
didError: false,
43+
error: null,
44+
data: {
45+
summary: { target: 'simulator' },
46+
artifacts: {
47+
buildLogPath: '/tmp/build.log',
48+
xcresultPath: '/tmp/App Tests.xcresult',
49+
},
50+
},
51+
};
52+
53+
expect(normalizeStructuredEnvelope(envelope)).toEqual(envelope);
54+
});
55+
3856
it('keeps suite-less passed test cases for non-simulator results', () => {
3957
const envelope: StructuredOutputEnvelope<unknown> = {
4058
schema: 'xcodebuildmcp.output.test-result',

src/types/domain-results.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export type TestResultArtifacts = AtLeastOne<{
168168
deviceId: string;
169169
buildLogPath: string;
170170
packagePath: string;
171+
xcresultPath: string;
171172
}>;
172173
export interface CoverageSummary extends StatusSummary {
173174
coveragePct?: number;

src/utils/__tests__/simulator-test-execution.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ describe('createSimulatorTwoPhaseExecutionPlan', () => {
6262
'/tmp/Calculator.xcresult',
6363
]);
6464
expect(plan.usesExactSelectors).toBe(false);
65+
expect(plan.resultBundlePath).toBe('/tmp/Calculator.xcresult');
6566
});
6667

6768
it('preserves user-supplied selector arguments in both simulator test phases', () => {
@@ -90,5 +91,32 @@ describe('createSimulatorTwoPhaseExecutionPlan', () => {
9091

9192
expect(plan.buildArgs).toEqual([]);
9293
expect(plan.testArgs).toEqual(['-resultBundlePath', '/tmp/UserProvided.xcresult']);
94+
expect(plan.resultBundlePath).toBe('/tmp/UserProvided.xcresult');
95+
});
96+
97+
it('supports equals-form resultBundlePath arguments', () => {
98+
const plan = createSimulatorTwoPhaseExecutionPlan({
99+
extraArgs: ['-resultBundlePath=/tmp/EqualsProvided.xcresult'],
100+
});
101+
102+
expect(plan.buildArgs).toEqual([]);
103+
expect(plan.testArgs).toEqual(['-resultBundlePath', '/tmp/EqualsProvided.xcresult']);
104+
expect(plan.resultBundlePath).toBe('/tmp/EqualsProvided.xcresult');
105+
});
106+
107+
it('uses the last valid resultBundlePath argument', () => {
108+
const plan = createSimulatorTwoPhaseExecutionPlan({
109+
extraArgs: [
110+
'-resultBundlePath',
111+
'-quiet',
112+
'-resultBundlePath',
113+
'/tmp/First.xcresult',
114+
'-resultBundlePath=/tmp/Last.xcresult',
115+
],
116+
});
117+
118+
expect(plan.buildArgs).toEqual(['-quiet']);
119+
expect(plan.testArgs).toEqual(['-quiet', '-resultBundlePath', '/tmp/Last.xcresult']);
120+
expect(plan.resultBundlePath).toBe('/tmp/Last.xcresult');
93121
});
94122
});

0 commit comments

Comments
 (0)