@zgeoff/atc 3.4.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/agents/build-agent-adapters.ts +21 -3
- package/src/agents/codex-adapter.ts +20 -0
- package/src/agents/render-sdk-message.ts +57 -0
- package/src/agents/start-claude-headless-run.ts +2 -57
- package/src/agents/with-claude-config-lock.ts +28 -15
- package/src/client/boot-daemon.ts +75 -25
- package/src/client/resolve-workspace-root.ts +10 -5
- package/src/daemon/session-runtime.ts +0 -4
- package/src/gateway.ts +2 -179
- package/src/mcp-http-server.ts +13 -10
- package/src/run-gateway-cli.ts +327 -0
- package/src/run-gateway.ts +49 -18
- package/src/sources/github/run-gh.ts +9 -0
- package/src/test-utils/build-mock-execution-target.ts +25 -0
- package/src/test-utils/build-mock-imp-session-request.ts +53 -0
- package/src/test-utils/build-mock-message-record.ts +7 -3
- package/src/test-utils/build-mock-stored-event.ts +20 -0
- package/src/test-utils/build-mock-stored-row.ts +23 -0
- package/src/test-utils/build-stub-brokered-gateway-adapter.ts +50 -0
- package/src/test-utils/build-stub-mcp-stdio-server.ts +20 -0
- package/src/test-utils/build-stub-recording-filter.ts +9 -0
- package/src/test-utils/build-stub-recording-git.ts +8 -0
- package/src/test-utils/create-git-fixture.ts +5 -8
- package/src/test-utils/create-stub-echo-claude.ts +21 -0
- package/src/test-utils/{build-stub-imp-port.ts → create-stub-imp-port.ts} +30 -5
- package/src/test-utils/create-stub-recording-codex.ts +20 -0
- package/src/test-utils/create-stub-shared-server-codex.ts +56 -0
- package/src/test-utils/create-stub-systemd.ts +10 -4
- package/src/test-utils/get-only-imp-name.ts +2 -2
- package/src/test-utils/register-test-cleanup.ts +63 -0
- package/src/test-utils/run-legacy-daemon.ts +1 -1
- package/src/test-utils/run-mcp-authorization.ts +7 -4
- package/src/test-utils/setup-mcp-home.ts +24 -13
- package/src/test-utils/setup-mcp-http.ts +15 -7
- package/src/test-utils/setup-temp-dir.ts +7 -6
- package/src/test-utils/start-cut-proxy.ts +6 -2
- package/src/test-utils/start-daemon-process.ts +25 -24
- package/src/test-utils/start-git-http-server.ts +9 -3
- package/src/test-utils/start-mcp-stdio.ts +17 -22
- package/src/test-utils/start-stub-answering-listener.ts +65 -0
- package/src/test-utils/start-stub-closing-listener.ts +31 -0
- package/src/test-utils/start-stub-dropping-daemon.ts +10 -8
- package/src/test-utils/start-stub-impd-info.ts +10 -4
- package/src/test-utils/start-stub-impd.ts +8 -3
- package/src/test-utils/start-stub-legacy-daemon.ts +13 -8
- package/src/test-utils/start-stub-mcp-server.ts +7 -2
- package/src/test-utils/start-stub-recording-listener.ts +47 -0
- package/src/test-utils/start-stub-refusing-listener.ts +9 -4
- package/src/test-utils/start-stub-reporter-socket.ts +6 -4
- package/src/test-utils/start-stub-restore-daemon.ts +5 -4
- package/src/test-utils/start-stub-session-bridge.ts +6 -4
- package/src/test-utils/start-stub-silent-server.ts +10 -5
- package/src/test-utils/start-stub-source-daemon.ts +5 -3
- package/src/test-utils/start-stub-stalled-client.ts +61 -0
- package/src/test-utils/start-stub-stalled-listener.ts +13 -9
- package/src/test-utils/start-stub-tcp-daemon.ts +9 -4
- package/src/test-utils/start-stub-telemetry-collector.ts +9 -3
- package/src/test-utils/start-stub-unanswering-listener.ts +41 -0
- package/src/test-utils/start-stub-unanswering-unix-listener.ts +38 -0
- package/src/test-utils/start-test-daemon.ts +20 -6
- package/src/test-utils/start-tui-harness.ts +15 -8
- package/src/test-utils/subscribe-to-socket-lines.ts +25 -5
|
@@ -5,6 +5,7 @@ import { stopDaemonProcess } from '../stop-daemon-process';
|
|
|
5
5
|
import { buildStubMCPClaude } from './build-stub-mcp-claude';
|
|
6
6
|
import { buildStubMCPGrok } from './build-stub-mcp-grok';
|
|
7
7
|
import { createStubBin } from './create-stub-bin';
|
|
8
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
8
9
|
import { setupTempDir } from './setup-temp-dir';
|
|
9
10
|
import { waitFor } from './wait-for';
|
|
10
11
|
|
|
@@ -17,13 +18,33 @@ import { waitFor } from './wait-for';
|
|
|
17
18
|
* home. Disposal stops the daemon, when one wrote its pid file here, and
|
|
18
19
|
* waits for it to exit; then it kills the daemon's process group and every
|
|
19
20
|
* group `stub-pids` records, such as a reporter a stopped session left
|
|
20
|
-
* behind, waits until each group is empty, and removes the home.
|
|
21
|
-
*
|
|
21
|
+
* behind, waits until each group is empty, and removes the home. That
|
|
22
|
+
* disposal runs once the current test finishes, so it must run inside a
|
|
23
|
+
* test; disposing sooner runs it then, and a second disposal does nothing.
|
|
22
24
|
*/
|
|
23
25
|
export function setupMCPHome() {
|
|
24
26
|
const tmp = setupTempDir('atc-mcp-');
|
|
27
|
+
|
|
28
|
+
// Registered after the home, so it releases first: the daemon and the
|
|
29
|
+
// stand-ins stop before the home that records them is removed.
|
|
30
|
+
const stack = new AsyncDisposableStack();
|
|
31
|
+
|
|
32
|
+
const dispose = registerTestCleanup(() => stack.disposeAsync());
|
|
33
|
+
|
|
34
|
+
stack.use(tmp);
|
|
35
|
+
|
|
25
36
|
const home = tmp.dir;
|
|
26
37
|
|
|
38
|
+
stack.defer(async () => {
|
|
39
|
+
const pid = findPidFilePID(join(home, 'atc-daemon.pid'));
|
|
40
|
+
|
|
41
|
+
if (pid !== null) {
|
|
42
|
+
await stopDaemonProcess(pid);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await killProcessGroups([...(pid === null ? [] : [pid]), ...readStubPIDs(home)]);
|
|
46
|
+
});
|
|
47
|
+
|
|
27
48
|
mkdirSync(join(home, '.config', 'atc'), { recursive: true });
|
|
28
49
|
mkdirSync(join(home, '.local', 'state', 'atc'), { recursive: true });
|
|
29
50
|
|
|
@@ -39,17 +60,7 @@ export function setupMCPHome() {
|
|
|
39
60
|
home,
|
|
40
61
|
claudeBin,
|
|
41
62
|
grokBin,
|
|
42
|
-
|
|
43
|
-
const pid = findPidFilePID(join(home, 'atc-daemon.pid'));
|
|
44
|
-
|
|
45
|
-
if (pid !== null) {
|
|
46
|
-
await stopDaemonProcess(pid);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
await killProcessGroups([...(pid === null ? [] : [pid]), ...readStubPIDs(home)]);
|
|
50
|
-
|
|
51
|
-
tmp[Symbol.dispose]();
|
|
52
|
-
},
|
|
63
|
+
[Symbol.asyncDispose]: dispose,
|
|
53
64
|
};
|
|
54
65
|
}
|
|
55
66
|
|
|
@@ -7,6 +7,7 @@ import { openMCPAuth } from '../mcp/open-mcp-auth';
|
|
|
7
7
|
import { ReconnectingCaller } from '../mcp/reconnecting-caller';
|
|
8
8
|
import { startMCPHTTPServer } from '../mcp/start-mcp-http-server';
|
|
9
9
|
import { buildMockAgentAdapter } from './build-mock-agent-adapter';
|
|
10
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
10
11
|
import { setupTempDir } from './setup-temp-dir';
|
|
11
12
|
|
|
12
13
|
interface MCPHTTPSetupOptions {
|
|
@@ -24,13 +25,22 @@ interface MCPHTTPSetupOptions {
|
|
|
24
25
|
* through it and returns the client id. `restartDaemon` stops the daemon and
|
|
25
26
|
* starts a fresh one on the same socket and database, the way an operator
|
|
26
27
|
* restarts it, with the principals it is given; `countDaemonClients` reads
|
|
27
|
-
* how many connections the current daemon holds open.
|
|
28
|
-
*
|
|
28
|
+
* how many connections the current daemon holds open. Everything it starts
|
|
29
|
+
* stops, and the directory is removed, once the current test finishes, so it
|
|
30
|
+
* must run inside a test; disposal does so sooner, and a second disposal
|
|
31
|
+
* does nothing.
|
|
29
32
|
*/
|
|
30
33
|
export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
|
|
31
|
-
|
|
34
|
+
const tmp = setupTempDir('atc-mcp-http-');
|
|
35
|
+
|
|
36
|
+
// Registered after the directory, so it releases first: each part stops
|
|
37
|
+
// before the one it depends on, and the directory goes last.
|
|
38
|
+
const stack = new AsyncDisposableStack();
|
|
39
|
+
|
|
40
|
+
const dispose = registerTestCleanup(() => stack.disposeAsync());
|
|
41
|
+
|
|
42
|
+
stack.use(tmp);
|
|
32
43
|
|
|
33
|
-
const tmp = stack.use(setupTempDir('atc-mcp-http-'));
|
|
34
44
|
const socketPath = join(tmp.dir, 'daemon.sock');
|
|
35
45
|
const stateDir = join(tmp.dir, '.local', 'state', 'atc');
|
|
36
46
|
|
|
@@ -83,8 +93,6 @@ export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
|
|
|
83
93
|
|
|
84
94
|
stack.defer(() => store.close());
|
|
85
95
|
|
|
86
|
-
const owned = stack.move();
|
|
87
|
-
|
|
88
96
|
return {
|
|
89
97
|
home: tmp.dir,
|
|
90
98
|
url: server.url,
|
|
@@ -109,6 +117,6 @@ export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
|
|
|
109
117
|
|
|
110
118
|
daemon = await startTestDaemon(principals);
|
|
111
119
|
},
|
|
112
|
-
[Symbol.asyncDispose]:
|
|
120
|
+
[Symbol.asyncDispose]: dispose,
|
|
113
121
|
};
|
|
114
122
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
4
5
|
|
|
5
6
|
interface TempDir {
|
|
6
7
|
readonly dir: string;
|
|
@@ -9,17 +10,17 @@ interface TempDir {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Creates a
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Creates a directory under the system temp root, named by the prefix, and
|
|
14
|
+
* registers its removal to run once the current test finishes, so it must
|
|
15
|
+
* run inside a test. Disposal removes the tree sooner; whichever comes
|
|
16
|
+
* second does nothing.
|
|
16
17
|
*/
|
|
17
18
|
export function setupTempDir(prefix: string): TempDir {
|
|
18
19
|
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
19
20
|
|
|
20
|
-
const remove = () => {
|
|
21
|
+
const remove = registerTestCleanup(() => {
|
|
21
22
|
rmSync(dir, { recursive: true, force: true });
|
|
22
|
-
};
|
|
23
|
+
});
|
|
23
24
|
|
|
24
25
|
return {
|
|
25
26
|
dir,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Socket } from 'bun';
|
|
2
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
3
|
|
|
3
4
|
interface CutProxyOptions {
|
|
4
5
|
// The address the proxy forwards to.
|
|
@@ -47,6 +48,9 @@ interface ProxyLink {
|
|
|
47
48
|
* connection byte for byte, except that for the first `cuts` requests of
|
|
48
49
|
* the method it loses the target's next answer after the request reached
|
|
49
50
|
* the target: the request runs, and the client never sees its response.
|
|
51
|
+
* It stops, ending every link, once the current test finishes, so it must
|
|
52
|
+
* run inside a test; `stop` or disposal stops it sooner, and a second stop
|
|
53
|
+
* does nothing.
|
|
50
54
|
*/
|
|
51
55
|
export function startCutProxy(options: CutProxyOptions): CutProxy {
|
|
52
56
|
let cutsLeft = options.cuts;
|
|
@@ -121,14 +125,14 @@ export function startCutProxy(options: CutProxyOptions): CutProxy {
|
|
|
121
125
|
},
|
|
122
126
|
});
|
|
123
127
|
|
|
124
|
-
const stop = () => {
|
|
128
|
+
const stop = registerTestCleanup(() => {
|
|
125
129
|
for (const client of links) {
|
|
126
130
|
client.end();
|
|
127
131
|
client.data.upstream?.end();
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
server.stop(true);
|
|
131
|
-
};
|
|
135
|
+
});
|
|
132
136
|
|
|
133
137
|
return {
|
|
134
138
|
port: server.port,
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import type { Subprocess } from 'bun';
|
|
4
4
|
import { DaemonClient } from '../client/daemon-client';
|
|
5
5
|
import { findDaemonRecord } from '../shared/find-daemon-record';
|
|
6
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
6
7
|
import { waitFor } from './wait-for';
|
|
7
8
|
|
|
8
9
|
interface DaemonProcessConfig {
|
|
@@ -41,7 +42,9 @@ interface DaemonBoot {
|
|
|
41
42
|
* same config. `readStderr` reads what the current boot printed. Disposal
|
|
42
43
|
* closes every client it opened, kills the daemon and waits for it to exit,
|
|
43
44
|
* then kills the daemon the home's state directory records, which a restart
|
|
44
|
-
* the test asked atc for may have started
|
|
45
|
+
* the test asked atc for may have started. That disposal runs once the
|
|
46
|
+
* current test finishes, so it must run inside a test; disposing sooner
|
|
47
|
+
* runs it then, and a second disposal does nothing.
|
|
45
48
|
*/
|
|
46
49
|
export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
47
50
|
const socketPath = join(config.home, 'atc-daemon.sock');
|
|
@@ -76,6 +79,26 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
|
76
79
|
|
|
77
80
|
let current = boot();
|
|
78
81
|
|
|
82
|
+
const dispose = registerTestCleanup(async () => {
|
|
83
|
+
disposed = true;
|
|
84
|
+
|
|
85
|
+
for (const client of clients) {
|
|
86
|
+
client.stop();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
current.proc.kill('SIGKILL');
|
|
90
|
+
|
|
91
|
+
await current.proc.exited;
|
|
92
|
+
|
|
93
|
+
const recorded = findDaemonRecord(join(stateDir, 'daemon.json'));
|
|
94
|
+
|
|
95
|
+
if (recorded !== null && recorded.pid !== current.proc.pid) {
|
|
96
|
+
try {
|
|
97
|
+
process.kill(recorded.pid, 'SIGKILL');
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
79
102
|
const readStderr = () => {
|
|
80
103
|
try {
|
|
81
104
|
return readFileSync(current.stderrPath, 'utf8');
|
|
@@ -165,28 +188,6 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
|
165
188
|
|
|
166
189
|
current = boot();
|
|
167
190
|
},
|
|
168
|
-
|
|
169
|
-
if (disposed) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
disposed = true;
|
|
174
|
-
|
|
175
|
-
for (const client of clients) {
|
|
176
|
-
client.stop();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
current.proc.kill('SIGKILL');
|
|
180
|
-
|
|
181
|
-
await current.proc.exited;
|
|
182
|
-
|
|
183
|
-
const recorded = findDaemonRecord(join(stateDir, 'daemon.json'));
|
|
184
|
-
|
|
185
|
-
if (recorded !== null && recorded.pid !== current.proc.pid) {
|
|
186
|
-
try {
|
|
187
|
-
process.kill(recorded.pid, 'SIGKILL');
|
|
188
|
-
} catch {}
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
+
[Symbol.asyncDispose]: dispose,
|
|
191
192
|
};
|
|
192
193
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
1
3
|
interface GitHTTPServer {
|
|
2
4
|
// The base URL a repository under the root is served at, ending in `/`.
|
|
3
5
|
readonly url: string;
|
|
@@ -27,7 +29,9 @@ interface GitHTTPServerOptions {
|
|
|
27
29
|
* authenticated request waits that long before it is served, which makes a
|
|
28
30
|
* clone through it slow. An `onRequest` callback runs, and is awaited, while
|
|
29
31
|
* each authenticated request is held, so it sees the client that sent the
|
|
30
|
-
* request still running.
|
|
32
|
+
* request still running. It stops once the current test finishes, so it
|
|
33
|
+
* must run inside a test; `stop` or disposal stops it sooner, and a second
|
|
34
|
+
* stop does nothing.
|
|
31
35
|
*/
|
|
32
36
|
export function startGitHTTPServer(
|
|
33
37
|
root: string,
|
|
@@ -87,11 +91,13 @@ export function startGitHTTPServer(
|
|
|
87
91
|
},
|
|
88
92
|
});
|
|
89
93
|
|
|
94
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
95
|
+
|
|
90
96
|
return {
|
|
91
97
|
url: `http://127.0.0.1:${server.port}/`,
|
|
92
98
|
authorizations,
|
|
93
|
-
stop
|
|
94
|
-
[Symbol.asyncDispose]:
|
|
99
|
+
stop,
|
|
100
|
+
[Symbol.asyncDispose]: stop,
|
|
95
101
|
};
|
|
96
102
|
}
|
|
97
103
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { isRecord } from '../shared/report';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
3
4
|
|
|
4
5
|
const CLI_PATH = join(import.meta.dir, '..', 'cli.ts');
|
|
5
6
|
|
|
@@ -37,9 +38,10 @@ interface MCPToolResult {
|
|
|
37
38
|
* an object. `spawnSession` spawns through `atc_session_spawn` and resolves
|
|
38
39
|
* with the new session's id. A request still unanswered when the server's
|
|
39
40
|
* stdout ends rejects. When `initialize` fails, the server is stopped
|
|
40
|
-
* before the start rejects.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* before the start rejects. The server stops once the current test
|
|
42
|
+
* finishes, so it must run inside a test; disposal stops it sooner and waits
|
|
43
|
+
* for it to exit, and a second disposal waits for the same exit. The daemon
|
|
44
|
+
* stays up for the home to stop.
|
|
43
45
|
*/
|
|
44
46
|
export async function startMCPStdio(options: MCPStdioOptions) {
|
|
45
47
|
const proc = Bun.spawn([...(options.command ?? [process.execPath, CLI_PATH, 'mcp'])], {
|
|
@@ -56,6 +58,13 @@ export async function startMCPStdio(options: MCPStdioOptions) {
|
|
|
56
58
|
stderr: 'ignore',
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
const stopServer = registerTestCleanup(async (): Promise<void> => {
|
|
62
|
+
void proc.stdin.end();
|
|
63
|
+
proc.kill();
|
|
64
|
+
|
|
65
|
+
await proc.exited;
|
|
66
|
+
});
|
|
67
|
+
|
|
59
68
|
const pending = new Map<number, PromiseWithResolvers<Readonly<Record<string, unknown>>>>();
|
|
60
69
|
|
|
61
70
|
void readResponses(proc.stdout, pending);
|
|
@@ -90,23 +99,6 @@ export async function startMCPStdio(options: MCPStdioOptions) {
|
|
|
90
99
|
return toToolResult(response);
|
|
91
100
|
};
|
|
92
101
|
|
|
93
|
-
let stopped = false;
|
|
94
|
-
|
|
95
|
-
const stopServer = async (): Promise<void> => {
|
|
96
|
-
if (!stopped) {
|
|
97
|
-
stopped = true;
|
|
98
|
-
void proc.stdin.end();
|
|
99
|
-
proc.kill();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
await proc.exited;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// A failed initialize stops the server here, before the caller holds it.
|
|
106
|
-
await using stack = new AsyncDisposableStack();
|
|
107
|
-
|
|
108
|
-
stack.defer(stopServer);
|
|
109
|
-
|
|
110
102
|
const server = {
|
|
111
103
|
sendRequest,
|
|
112
104
|
sendToolCall,
|
|
@@ -124,13 +116,16 @@ export async function startMCPStdio(options: MCPStdioOptions) {
|
|
|
124
116
|
[Symbol.asyncDispose]: stopServer,
|
|
125
117
|
};
|
|
126
118
|
|
|
119
|
+
// A failed initialize stops the server here, before the start rejects.
|
|
127
120
|
await sendRequest('initialize', {
|
|
128
121
|
protocolVersion: '2025-06-18',
|
|
129
122
|
capabilities: {},
|
|
130
123
|
clientInfo: { name: 'atc-test' },
|
|
131
|
-
})
|
|
124
|
+
}).catch(async (error: unknown) => {
|
|
125
|
+
await stopServer();
|
|
132
126
|
|
|
133
|
-
|
|
127
|
+
throw error;
|
|
128
|
+
});
|
|
134
129
|
|
|
135
130
|
return server;
|
|
136
131
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
import type { Socket } from 'node:net';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
4
|
+
|
|
5
|
+
interface StubAnsweringListener {
|
|
6
|
+
// The server's side of each connection it accepted, in order.
|
|
7
|
+
readonly peers: readonly Socket[];
|
|
8
|
+
|
|
9
|
+
// The first read each connection sent, decoded, in arrival order.
|
|
10
|
+
readonly lines: readonly string[];
|
|
11
|
+
readonly [Symbol.asyncDispose]: () => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A stand-in for a daemon socket that answers a handshake: it listens on
|
|
16
|
+
* the unix socket path, records each connection it accepts and the first
|
|
17
|
+
* read each one sends, answers that read with the line `answer`, and
|
|
18
|
+
* leaves every later byte unread on the connection. Resolves once it
|
|
19
|
+
* listens. Once the current test finishes, it destroys every connection it
|
|
20
|
+
* accepted and closes, so it must run inside a test; disposal does so
|
|
21
|
+
* sooner and resolves once the server has closed, and a second stop does
|
|
22
|
+
* nothing.
|
|
23
|
+
*/
|
|
24
|
+
export async function startStubAnsweringListener(path: string): Promise<StubAnsweringListener> {
|
|
25
|
+
const peers: Socket[] = [];
|
|
26
|
+
const lines: string[] = [];
|
|
27
|
+
|
|
28
|
+
const server = createServer((peer) => {
|
|
29
|
+
peers.push(peer);
|
|
30
|
+
|
|
31
|
+
peer.once('data', (data) => {
|
|
32
|
+
peer.pause();
|
|
33
|
+
lines.push(data.toString());
|
|
34
|
+
peer.write('answer\n');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const listening = Promise.withResolvers<void>();
|
|
39
|
+
|
|
40
|
+
server.listen(path, () => {
|
|
41
|
+
listening.resolve();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await listening.promise;
|
|
45
|
+
|
|
46
|
+
const stop = registerTestCleanup(async () => {
|
|
47
|
+
for (const peer of peers) {
|
|
48
|
+
peer.destroy();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const closed = Promise.withResolvers<void>();
|
|
52
|
+
|
|
53
|
+
server.close(() => {
|
|
54
|
+
closed.resolve();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await closed.promise;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
peers,
|
|
62
|
+
lines,
|
|
63
|
+
[Symbol.asyncDispose]: stop,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
3
|
+
interface StubClosingListener {
|
|
4
|
+
readonly [Symbol.dispose]: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A stand-in for a daemon that hangs up: it listens on the unix socket path
|
|
9
|
+
* and ends every connection as soon as it opens, reading nothing. It stops
|
|
10
|
+
* once the current test finishes, so it must run inside a test; disposal
|
|
11
|
+
* stops it sooner, and a second stop does nothing.
|
|
12
|
+
*/
|
|
13
|
+
export function startStubClosingListener(path: string): StubClosingListener {
|
|
14
|
+
const server = Bun.listen({
|
|
15
|
+
unix: path,
|
|
16
|
+
socket: {
|
|
17
|
+
open(socket) {
|
|
18
|
+
socket.end();
|
|
19
|
+
},
|
|
20
|
+
data() {},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const stop = registerTestCleanup(() => {
|
|
25
|
+
server.stop(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
[Symbol.dispose]: stop,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
decodeMessage,
|
|
7
7
|
encodeMessage,
|
|
8
8
|
} from '../protocol/protocol';
|
|
9
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
9
10
|
|
|
10
11
|
interface DroppingDaemonOptions {
|
|
11
12
|
// The features the first handshake announces.
|
|
@@ -23,8 +24,9 @@ interface DroppingDaemonOptions {
|
|
|
23
24
|
* and answers every later one with a session `s-1`. `keys` records the
|
|
24
25
|
* idempotency key each of those requests carried, `undefined` for one that
|
|
25
26
|
* carried none. `reads` counts the reads it has taken from every connection,
|
|
26
|
-
* so a test can wait until one piece of a split write has arrived.
|
|
27
|
-
*
|
|
27
|
+
* so a test can wait until one piece of a split write has arrived. It stops
|
|
28
|
+
* once the current test finishes, so it must run inside a test; `stop` or
|
|
29
|
+
* disposal stops it sooner, and a second stop does nothing.
|
|
28
30
|
*/
|
|
29
31
|
export function startStubDroppingDaemon(socketPath: string, options: DroppingDaemonOptions) {
|
|
30
32
|
const keys: unknown[] = [];
|
|
@@ -88,16 +90,16 @@ export function startStubDroppingDaemon(socketPath: string, options: DroppingDae
|
|
|
88
90
|
},
|
|
89
91
|
});
|
|
90
92
|
|
|
93
|
+
const stop = registerTestCleanup(() => {
|
|
94
|
+
server.stop(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
91
97
|
return {
|
|
92
98
|
keys,
|
|
93
99
|
get reads() {
|
|
94
100
|
return reads;
|
|
95
101
|
},
|
|
96
|
-
stop
|
|
97
|
-
|
|
98
|
-
},
|
|
99
|
-
[Symbol.dispose]() {
|
|
100
|
-
server.stop(true);
|
|
101
|
-
},
|
|
102
|
+
stop,
|
|
103
|
+
[Symbol.dispose]: stop,
|
|
102
104
|
};
|
|
103
105
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* An impd stand-in on a real HTTP port of the loopback address. Every call
|
|
3
5
|
* is answered with HTTP 200 and the body impd's RPC sends for system info,
|
|
4
6
|
* `{ json: info }`, so a client reads `info.features` as impd's features;
|
|
5
7
|
* the test sets `info.features` to what impd should report. `paths` holds
|
|
6
|
-
* the request path of each call, in order.
|
|
7
|
-
*
|
|
8
|
+
* the request path of each call, in order. The server stops once the
|
|
9
|
+
* current test finishes, so it must run inside a test; disposal stops it
|
|
10
|
+
* sooner, the asynchronous form resolving once it has stopped, and a second
|
|
11
|
+
* stop does nothing.
|
|
8
12
|
*/
|
|
9
13
|
export function startStubImpdInfo() {
|
|
10
14
|
const paths: string[] = [];
|
|
@@ -20,13 +24,15 @@ export function startStubImpdInfo() {
|
|
|
20
24
|
},
|
|
21
25
|
});
|
|
22
26
|
|
|
27
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
28
|
+
|
|
23
29
|
return {
|
|
24
30
|
url: `http://127.0.0.1:${String(server.port)}`,
|
|
25
31
|
paths,
|
|
26
32
|
info,
|
|
27
33
|
[Symbol.dispose]: () => {
|
|
28
|
-
void
|
|
34
|
+
void stop();
|
|
29
35
|
},
|
|
30
|
-
[Symbol.asyncDispose]:
|
|
36
|
+
[Symbol.asyncDispose]: stop,
|
|
31
37
|
};
|
|
32
38
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ServerWebSocket } from 'bun';
|
|
2
2
|
import { isRecord } from '../shared/report';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
3
4
|
|
|
4
5
|
// What the stand-in does with an exec open: refuse it as a start whose
|
|
5
6
|
// broker is not ready, start it and count its stdin bytes as `wc -c` does,
|
|
@@ -15,8 +16,10 @@ type StubExecReply = 'refuse' | 'count' | 'exit-early';
|
|
|
15
16
|
* announce a guest connection on it. It records each exec open message in
|
|
16
17
|
* `execOpens` and answers it as `exec.reply` says, a refusal as a start
|
|
17
18
|
* whose broker is not ready by default. It takes no WebSocket message over
|
|
18
|
-
* 2 MiB, as impd refuses one over its own limit.
|
|
19
|
-
*
|
|
19
|
+
* 2 MiB, as impd refuses one over its own limit. The server stops, dropping
|
|
20
|
+
* every open connection at once, when the current test finishes, so it must
|
|
21
|
+
* run inside a test; disposal stops it sooner, and a second stop does
|
|
22
|
+
* nothing.
|
|
20
23
|
*/
|
|
21
24
|
export function startStubImpd() {
|
|
22
25
|
const authorizations: (string | null)[] = [];
|
|
@@ -123,6 +126,8 @@ export function startStubImpd() {
|
|
|
123
126
|
},
|
|
124
127
|
});
|
|
125
128
|
|
|
129
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
130
|
+
|
|
126
131
|
return {
|
|
127
132
|
url: `http://127.0.0.1:${String(server.port)}`,
|
|
128
133
|
authorizations,
|
|
@@ -132,7 +137,7 @@ export function startStubImpd() {
|
|
|
132
137
|
execOpens,
|
|
133
138
|
exec,
|
|
134
139
|
[Symbol.dispose]: () => {
|
|
135
|
-
void
|
|
140
|
+
void stop();
|
|
136
141
|
},
|
|
137
142
|
};
|
|
138
143
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
decodeMessage,
|
|
8
8
|
encodeMessage,
|
|
9
9
|
} from '../protocol/protocol';
|
|
10
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
10
11
|
|
|
11
12
|
interface ReceivedRequest {
|
|
12
13
|
readonly m: string;
|
|
@@ -28,6 +29,11 @@ interface StubLegacyDaemonOptions {
|
|
|
28
29
|
readonly protocol?: number;
|
|
29
30
|
|
|
30
31
|
readonly replies?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
32
|
+
|
|
33
|
+
// Who stops the daemon: the current test, once it finishes, or the
|
|
34
|
+
// caller alone, for a process that runs it outside any test; the test
|
|
35
|
+
// unless set.
|
|
36
|
+
readonly owner?: 'test' | 'caller';
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
@@ -39,8 +45,10 @@ interface StubLegacyDaemonOptions {
|
|
|
39
45
|
* `replies` with that method's reply, and refuses every other method with
|
|
40
46
|
* `unknown_method`. Every request it receives is recorded in `requests`, and
|
|
41
47
|
* `connections` counts the connections it accepted and those still open.
|
|
42
|
-
* `port` holds the TCP port it bound, `null` on a unix socket.
|
|
43
|
-
*
|
|
48
|
+
* `port` holds the TCP port it bound, `null` on a unix socket. It stops
|
|
49
|
+
* once the current test finishes, so it must run inside a test, unless the
|
|
50
|
+
* options make the caller its owner; `stop` or disposal stops it sooner, and
|
|
51
|
+
* a second stop does nothing.
|
|
44
52
|
*/
|
|
45
53
|
export function startStubLegacyDaemon(
|
|
46
54
|
address: string | StubLegacyDaemonTCPAddress,
|
|
@@ -120,17 +128,14 @@ export function startStubLegacyDaemon(
|
|
|
120
128
|
};
|
|
121
129
|
|
|
122
130
|
const server = startListener(address, handlers);
|
|
131
|
+
const stop = options.owner === 'caller' ? server.stop : registerTestCleanup(server.stop);
|
|
123
132
|
|
|
124
133
|
return {
|
|
125
134
|
requests,
|
|
126
135
|
connections,
|
|
127
136
|
port: server.port,
|
|
128
|
-
stop
|
|
129
|
-
|
|
130
|
-
},
|
|
131
|
-
[Symbol.dispose]() {
|
|
132
|
-
server.stop();
|
|
133
|
-
},
|
|
137
|
+
stop,
|
|
138
|
+
[Symbol.dispose]: stop,
|
|
134
139
|
};
|
|
135
140
|
}
|
|
136
141
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
1
3
|
interface StubMCPRequest {
|
|
2
4
|
readonly method: string;
|
|
3
5
|
readonly path: string;
|
|
@@ -19,7 +21,8 @@ interface StubMCPServer {
|
|
|
19
21
|
* A stand-in for an MCP HTTP server on a loopback port: it answers every
|
|
20
22
|
* request with the JSON body given, and records each request's method,
|
|
21
23
|
* path, authorization and content-type headers, and JSON body. `url` holds
|
|
22
|
-
* its origin.
|
|
24
|
+
* its origin. It stops once the current test finishes, so it must run
|
|
25
|
+
* inside a test; disposal stops it sooner, and a second stop does nothing.
|
|
23
26
|
*/
|
|
24
27
|
export function startStubMCPServer(answer: unknown): StubMCPServer {
|
|
25
28
|
const requests: StubMCPRequest[] = [];
|
|
@@ -40,11 +43,13 @@ export function startStubMCPServer(answer: unknown): StubMCPServer {
|
|
|
40
43
|
},
|
|
41
44
|
});
|
|
42
45
|
|
|
46
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
47
|
+
|
|
43
48
|
return {
|
|
44
49
|
url: `http://127.0.0.1:${String(server.port)}`,
|
|
45
50
|
requests,
|
|
46
51
|
[Symbol.dispose]: () => {
|
|
47
|
-
void
|
|
52
|
+
void stop();
|
|
48
53
|
},
|
|
49
54
|
};
|
|
50
55
|
}
|