Skip to content

Commit a9a04e9

Browse files
committed
Fix doctor and DAP tests
1 parent 290bd52 commit a9a04e9

3 files changed

Lines changed: 86 additions & 28 deletions

File tree

docs/TOOLS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,4 @@ XcodeBuildMCP provides 71 tools organized into 13 workflow groups for comprehens
122122

123123
---
124124

125-
*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2026-01-04*
125+
*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2026-01-08*

src/mcp/tools/doctor/__tests__/doctor.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import { describe, it, expect, beforeEach } from 'vitest';
88
import * as z from 'zod';
99
import doctor, { runDoctor, type DoctorDependencies } from '../doctor.ts';
10+
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
1011

1112
function createDeps(overrides?: Partial<DoctorDependencies>): DoctorDependencies {
1213
const base: DoctorDependencies = {
14+
commandExecutor: createMockExecutor({ output: 'lldb-dap' }),
1315
binaryChecker: {
1416
async checkBinaryAvailability(binary: string) {
1517
// default: all available with generic version

src/utils/debugger/dap/__tests__/transport-framing.test.ts

Lines changed: 83 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1+
import type { ChildProcess } from 'node:child_process';
2+
import { EventEmitter } from 'node:events';
3+
import { PassThrough } from 'node:stream';
4+
import type { InteractiveProcess, InteractiveSpawner } from '../../../execution/index.ts';
15
import { describe, expect, it } from 'vitest';
26

37
import { DapTransport } from '../transport.ts';
48
import type { DapEvent, DapResponse } from '../types.ts';
5-
import {
6-
createMockInteractiveSpawner,
7-
type MockInteractiveSession,
8-
} from '../../../../test-utils/mock-executors.ts';
9+
type TestSession = {
10+
stdout: PassThrough;
11+
stderr: PassThrough;
12+
stdin: PassThrough;
13+
emitExit: (code?: number | null, signal?: NodeJS.Signals | null) => void;
14+
emitError: (error: Error) => void;
15+
};
916

1017
function encodeMessage(message: Record<string, unknown>): string {
1118
const payload = JSON.stringify(message);
@@ -27,14 +34,73 @@ function buildResponse(
2734
};
2835
}
2936

37+
function createTestSpawner(): { spawner: InteractiveSpawner; session: TestSession } {
38+
const stdout = new PassThrough();
39+
const stderr = new PassThrough();
40+
const stdin = new PassThrough();
41+
const emitter = new EventEmitter();
42+
const mockProcess = emitter as unknown as ChildProcess;
43+
const mutableProcess = mockProcess as unknown as {
44+
stdout: PassThrough | null;
45+
stderr: PassThrough | null;
46+
stdin: PassThrough | null;
47+
killed: boolean;
48+
exitCode: number | null;
49+
signalCode: NodeJS.Signals | null;
50+
spawnargs: string[];
51+
spawnfile: string;
52+
pid: number;
53+
};
54+
55+
mutableProcess.stdout = stdout;
56+
mutableProcess.stderr = stderr;
57+
mutableProcess.stdin = stdin;
58+
mutableProcess.killed = false;
59+
mutableProcess.exitCode = null;
60+
mutableProcess.signalCode = null;
61+
mutableProcess.spawnargs = [];
62+
mutableProcess.spawnfile = 'mock';
63+
mutableProcess.pid = 12345;
64+
mockProcess.kill = ((signal?: NodeJS.Signals): boolean => {
65+
mutableProcess.killed = true;
66+
emitter.emit('exit', 0, signal ?? null);
67+
return true;
68+
}) as ChildProcess['kill'];
69+
70+
const session: TestSession = {
71+
stdout,
72+
stderr,
73+
stdin,
74+
emitExit: (code = 0, signal = null) => {
75+
emitter.emit('exit', code, signal);
76+
},
77+
emitError: (error) => {
78+
emitter.emit('error', error);
79+
},
80+
};
81+
82+
const spawner: InteractiveSpawner = (): InteractiveProcess => ({
83+
process: mockProcess,
84+
write(data: string): void {
85+
stdin.write(data);
86+
},
87+
kill(signal?: NodeJS.Signals): void {
88+
mockProcess.kill?.(signal);
89+
},
90+
dispose(): void {
91+
stdout.end();
92+
stderr.end();
93+
stdin.end();
94+
emitter.removeAllListeners();
95+
},
96+
});
97+
98+
return { spawner, session };
99+
}
100+
30101
describe('DapTransport framing', () => {
31102
it('parses responses across chunk boundaries', async () => {
32-
let session: MockInteractiveSession | null = null;
33-
const spawner = createMockInteractiveSpawner({
34-
onSpawn: (spawned) => {
35-
session = spawned;
36-
},
37-
});
103+
const { spawner, session } = createTestSpawner();
38104

39105
const transport = new DapTransport({ spawner, adapterCommand: ['lldb-dap'] });
40106

@@ -45,20 +111,15 @@ describe('DapTransport framing', () => {
45111
);
46112

47113
const response = encodeMessage(buildResponse(1, 'initialize', { ok: true }));
48-
session?.stdout.write(response.slice(0, 12));
49-
session?.stdout.write(response.slice(12));
114+
session.stdout.write(response.slice(0, 12));
115+
session.stdout.write(response.slice(12));
50116

51117
await expect(responsePromise).resolves.toEqual({ ok: true });
52118
transport.dispose();
53119
});
54120

55121
it('handles multiple messages in a single chunk', async () => {
56-
let session: MockInteractiveSession | null = null;
57-
const spawner = createMockInteractiveSpawner({
58-
onSpawn: (spawned) => {
59-
session = spawned;
60-
},
61-
});
122+
const { spawner, session } = createTestSpawner();
62123

63124
const transport = new DapTransport({ spawner, adapterCommand: ['lldb-dap'] });
64125
const events: DapEvent[] = [];
@@ -78,7 +139,7 @@ describe('DapTransport framing', () => {
78139
});
79140
const responseMessage = encodeMessage(buildResponse(1, 'threads', { ok: true }));
80141

81-
session?.stdout.write(`${eventMessage}${responseMessage}`);
142+
session.stdout.write(`${eventMessage}${responseMessage}`);
82143

83144
await expect(responsePromise).resolves.toEqual({ ok: true });
84145
expect(events).toHaveLength(1);
@@ -87,12 +148,7 @@ describe('DapTransport framing', () => {
87148
});
88149

89150
it('continues after invalid headers', async () => {
90-
let session: MockInteractiveSession | null = null;
91-
const spawner = createMockInteractiveSpawner({
92-
onSpawn: (spawned) => {
93-
session = spawned;
94-
},
95-
});
151+
const { spawner, session } = createTestSpawner();
96152

97153
const transport = new DapTransport({ spawner, adapterCommand: ['lldb-dap'] });
98154

@@ -102,9 +158,9 @@ describe('DapTransport framing', () => {
102158
{ timeoutMs: 1_000 },
103159
);
104160

105-
session?.stdout.write('Content-Length: nope\r\n\r\n');
161+
session.stdout.write('Content-Length: nope\r\n\r\n');
106162
const responseMessage = encodeMessage(buildResponse(1, 'stackTrace', { ok: true }));
107-
session?.stdout.write(responseMessage);
163+
session.stdout.write(responseMessage);
108164

109165
await expect(responsePromise).resolves.toEqual({ ok: true });
110166
transport.dispose();

0 commit comments

Comments
 (0)