@zgeoff/atc 3.4.0 → 3.4.2
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/claude-adapter.ts +4 -3
- package/src/agents/codex-adapter.ts +20 -0
- package/src/agents/gateway-adapter.ts +2 -2
- package/src/agents/plan-claude-line-input.ts +44 -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/mcp-tools.ts +1 -1
- 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-held-gh.ts +5 -7
- package/src/test-utils/build-stub-held-zoxide.ts +4 -6
- 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-claude.ts +3 -1
- 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} +39 -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-mcp-authorization.ts +7 -4
- package/src/test-utils/{run-legacy-daemon.ts → run-stub-legacy-daemon.ts} +2 -2
- 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 +6 -4
- 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
- /package/src/test-utils/{run-source-daemon.ts → run-stub-source-daemon.ts} +0 -0
|
@@ -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
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Socket } from 'bun';
|
|
2
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
3
|
+
|
|
4
|
+
interface StubRecordingListener {
|
|
5
|
+
// Resolves with the server's side of the first connection it accepts.
|
|
6
|
+
readonly accepted: Promise<Socket>;
|
|
7
|
+
|
|
8
|
+
// What each read took from any connection, decoded, in arrival order.
|
|
9
|
+
readonly received: readonly string[];
|
|
10
|
+
readonly [Symbol.dispose]: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A stand-in for a peer that a test writes through: it listens on the unix
|
|
15
|
+
* socket path, hands the test the server's side of the first connection it
|
|
16
|
+
* accepts, records what each read takes from any connection, and sends
|
|
17
|
+
* nothing of its own. It stops, dropping every connection it holds, once
|
|
18
|
+
* the current test finishes, so it must run inside a test; disposal stops it
|
|
19
|
+
* sooner, and a second stop does nothing.
|
|
20
|
+
*/
|
|
21
|
+
export function startStubRecordingListener(path: string): StubRecordingListener {
|
|
22
|
+
const accepted = Promise.withResolvers<Socket>();
|
|
23
|
+
const received: string[] = [];
|
|
24
|
+
|
|
25
|
+
const server = Bun.listen({
|
|
26
|
+
unix: path,
|
|
27
|
+
socket: {
|
|
28
|
+
open(socket) {
|
|
29
|
+
accepted.resolve(socket);
|
|
30
|
+
},
|
|
31
|
+
data(_socket, data) {
|
|
32
|
+
received.push(data.toString());
|
|
33
|
+
},
|
|
34
|
+
error() {},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const stop = registerTestCleanup(() => {
|
|
39
|
+
server.stop(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
accepted: accepted.promise,
|
|
44
|
+
received,
|
|
45
|
+
[Symbol.dispose]: stop,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
1
3
|
interface StubRefusingListener {
|
|
2
4
|
readonly port: number;
|
|
3
5
|
|
|
@@ -11,7 +13,8 @@ interface StubRefusingListener {
|
|
|
11
13
|
* A stand-in for a daemon's TCP listener refusing a connection whose first
|
|
12
14
|
* line is not a handshake: on a loopback port, it records what each read
|
|
13
15
|
* takes from a connection and ends that connection, answering nothing.
|
|
14
|
-
*
|
|
16
|
+
* It stops once the current test finishes, so it must run inside a test;
|
|
17
|
+
* disposal stops it sooner, and a second stop does nothing.
|
|
15
18
|
*/
|
|
16
19
|
export function startStubRefusingListener(): StubRefusingListener {
|
|
17
20
|
const received: string[] = [];
|
|
@@ -28,11 +31,13 @@ export function startStubRefusingListener(): StubRefusingListener {
|
|
|
28
31
|
},
|
|
29
32
|
});
|
|
30
33
|
|
|
34
|
+
const stop = registerTestCleanup(() => {
|
|
35
|
+
server.stop(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
31
38
|
return {
|
|
32
39
|
port: server.port,
|
|
33
40
|
received,
|
|
34
|
-
[Symbol.dispose]:
|
|
35
|
-
server.stop(true);
|
|
36
|
-
},
|
|
41
|
+
[Symbol.dispose]: stop,
|
|
37
42
|
};
|
|
38
43
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
1
2
|
import { waitFor } from './wait-for';
|
|
2
3
|
|
|
3
4
|
interface StubReporterSocket {
|
|
@@ -19,8 +20,9 @@ interface StubReporterSocket {
|
|
|
19
20
|
* hook and report commands send. It never writes back, and leaves each
|
|
20
21
|
* connection for the sender to close. `waitForLine` resolves with the first
|
|
21
22
|
* line once one has arrived, and rejects naming the socket when none arrives
|
|
22
|
-
* within `timeoutMs`, 5 seconds by default.
|
|
23
|
-
*
|
|
23
|
+
* within `timeoutMs`, 5 seconds by default. The listener stops once the
|
|
24
|
+
* current test finishes, so it must run inside a test; disposal stops it
|
|
25
|
+
* sooner, and a second stop does nothing.
|
|
24
26
|
*/
|
|
25
27
|
export function startStubReporterSocket(path: string): StubReporterSocket {
|
|
26
28
|
const lines: string[] = [];
|
|
@@ -45,9 +47,9 @@ export function startStubReporterSocket(path: string): StubReporterSocket {
|
|
|
45
47
|
},
|
|
46
48
|
});
|
|
47
49
|
|
|
48
|
-
const stop = () => {
|
|
50
|
+
const stop = registerTestCleanup(() => {
|
|
49
51
|
server.stop(true);
|
|
50
|
-
};
|
|
52
|
+
});
|
|
51
53
|
|
|
52
54
|
return {
|
|
53
55
|
lines,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LineDecoder } from '../protocol/line-decoder';
|
|
2
2
|
import { PROTOCOL_V, decodeMessage, encodeMessage } from '../protocol/protocol';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* A daemon stand-in on a unix socket path that speaks the real wire format
|
|
@@ -7,8 +8,8 @@ import { PROTOCOL_V, decodeMessage, encodeMessage } from '../protocol/protocol';
|
|
|
7
8
|
* result, answers each other request with the next reply the test pushes
|
|
8
9
|
* onto `lists`, and withholds the answer to any request that finds `lists`
|
|
9
10
|
* empty. `methods` holds the method of each request it took, in order.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* The listener stops once the current test finishes, so it must run inside
|
|
12
|
+
* a test; disposal stops it sooner, and a second stop does nothing.
|
|
12
13
|
*/
|
|
13
14
|
export function startStubRestoreDaemon(socketPath: string) {
|
|
14
15
|
const lists: Readonly<Record<string, unknown>>[] = [];
|
|
@@ -41,9 +42,9 @@ export function startStubRestoreDaemon(socketPath: string) {
|
|
|
41
42
|
},
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
const stop = () => {
|
|
45
|
+
const stop = registerTestCleanup(() => {
|
|
45
46
|
server.stop(true);
|
|
46
|
-
};
|
|
47
|
+
});
|
|
47
48
|
|
|
48
49
|
return {
|
|
49
50
|
lists,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRecord } from '../shared/report';
|
|
2
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
3
|
|
|
3
4
|
type BridgeRequest = Readonly<Record<string, unknown>>;
|
|
4
5
|
|
|
@@ -26,8 +27,9 @@ interface StubSessionBridge {
|
|
|
26
27
|
* `tap.open` request with `ok` under the request's id as the real bridge
|
|
27
28
|
* does once it attaches the tap, and hands every other request to the
|
|
28
29
|
* responder, writing back each line it returns. A line that is not a JSON
|
|
29
|
-
* object reaches the responder as an empty request.
|
|
30
|
-
*
|
|
30
|
+
* object reaches the responder as an empty request. The listener stops once
|
|
31
|
+
* the current test finishes, so it must run inside a test; disposal stops it
|
|
32
|
+
* sooner, and a second stop does nothing.
|
|
31
33
|
*/
|
|
32
34
|
export function startStubSessionBridge(path: string, respond: BridgeResponder): StubSessionBridge {
|
|
33
35
|
const requests: BridgeRequest[] = [];
|
|
@@ -69,9 +71,9 @@ export function startStubSessionBridge(path: string, respond: BridgeResponder):
|
|
|
69
71
|
},
|
|
70
72
|
});
|
|
71
73
|
|
|
72
|
-
const stop = () => {
|
|
74
|
+
const stop = registerTestCleanup(() => {
|
|
73
75
|
server.stop(true);
|
|
74
|
-
};
|
|
76
|
+
});
|
|
75
77
|
|
|
76
78
|
return {
|
|
77
79
|
requests,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* An HTTP stand-in for an upstream that hangs: it listens on a real port of
|
|
3
5
|
* the loopback address, takes every request, and never answers one. `paths`
|
|
4
|
-
* holds the request path of each request it took, in order.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* holds the request path of each request it took, in order. The server
|
|
7
|
+
* stops, dropping the connections it holds, once the current test finishes,
|
|
8
|
+
* so it must run inside a test; disposal stops it sooner, the asynchronous
|
|
9
|
+
* form resolving once it has stopped, and a second stop does nothing.
|
|
7
10
|
*/
|
|
8
11
|
export function startStubSilentServer() {
|
|
9
12
|
const paths: string[] = [];
|
|
@@ -22,12 +25,14 @@ export function startStubSilentServer() {
|
|
|
22
25
|
},
|
|
23
26
|
});
|
|
24
27
|
|
|
28
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
29
|
+
|
|
25
30
|
return {
|
|
26
31
|
url: `http://127.0.0.1:${String(server.port)}/`,
|
|
27
32
|
paths,
|
|
28
33
|
[Symbol.dispose]: () => {
|
|
29
|
-
void
|
|
34
|
+
void stop();
|
|
30
35
|
},
|
|
31
|
-
[Symbol.asyncDispose]:
|
|
36
|
+
[Symbol.asyncDispose]: stop,
|
|
32
37
|
};
|
|
33
38
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Starts a daemon composed with a fixture source, in a process of its own
|
|
@@ -8,20 +9,21 @@ import { join } from 'node:path';
|
|
|
8
9
|
* `ATC_TEST_SOURCES` picks its sources, the fixture one unless it holds
|
|
9
10
|
* `none`; `ATC_TEST_FIXTURE_URL` holds the repository the fixture source
|
|
10
11
|
* lists, and `ATC_TEST_SOURCE_LOG` a file it appends each listing to.
|
|
11
|
-
*
|
|
12
|
+
* The process stops once the current test finishes, so it must run inside
|
|
13
|
+
* a test; disposal stops it sooner, and a second stop does nothing.
|
|
12
14
|
*/
|
|
13
15
|
export async function startStubSourceDaemon(env: Readonly<Record<string, string | undefined>>) {
|
|
14
|
-
const daemon = Bun.spawn([process.execPath, join(import.meta.dir, 'run-source-daemon.ts')], {
|
|
16
|
+
const daemon = Bun.spawn([process.execPath, join(import.meta.dir, 'run-stub-source-daemon.ts')], {
|
|
15
17
|
env: { ATC_TEST_SOURCES: 'fixture', ...env },
|
|
16
18
|
stdout: 'pipe',
|
|
17
19
|
stderr: 'ignore',
|
|
18
20
|
});
|
|
19
21
|
|
|
20
|
-
const stop = async () => {
|
|
22
|
+
const stop = registerTestCleanup(async () => {
|
|
21
23
|
daemon.kill();
|
|
22
24
|
|
|
23
25
|
await daemon.exited;
|
|
24
|
-
};
|
|
26
|
+
});
|
|
25
27
|
|
|
26
28
|
const reader = daemon.stdout.getReader();
|
|
27
29
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createConnection } from 'node:net';
|
|
2
|
+
import { PROTOCOL_V, encodeMessage } from '../protocol/protocol';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
4
|
+
|
|
5
|
+
interface StubStalledClient {
|
|
6
|
+
// Each chunk the client has read off its connection, in order.
|
|
7
|
+
readonly chunks: readonly unknown[];
|
|
8
|
+
|
|
9
|
+
// How many bytes have arrived on the connection and wait unread.
|
|
10
|
+
readonly countUnreadBytes: () => number;
|
|
11
|
+
readonly [Symbol.dispose]: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A stand-in for a daemon client that stops reading: it connects to the
|
|
16
|
+
* daemon's unix socket, sends a `daemon.hello` with no auth as `client`,
|
|
17
|
+
* reads the first chunk the daemon sends, and then holds its next read on
|
|
18
|
+
* a promise that never resolves, so whatever the daemon sends it after
|
|
19
|
+
* that backs up. Resolves once that first chunk arrives. The connection is
|
|
20
|
+
* destroyed once the current test finishes, so it must run inside a test;
|
|
21
|
+
* disposal destroys it sooner, and a second stop does nothing.
|
|
22
|
+
*/
|
|
23
|
+
export async function startStubStalledClient(
|
|
24
|
+
socketPath: string,
|
|
25
|
+
client: string,
|
|
26
|
+
): Promise<StubStalledClient> {
|
|
27
|
+
const held = Promise.withResolvers<void>();
|
|
28
|
+
const first = Promise.withResolvers<void>();
|
|
29
|
+
const chunks: unknown[] = [];
|
|
30
|
+
const socket = createConnection(socketPath);
|
|
31
|
+
|
|
32
|
+
const stop = registerTestCleanup(() => {
|
|
33
|
+
socket.destroy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
socket.write(
|
|
37
|
+
encodeMessage({
|
|
38
|
+
v: PROTOCOL_V,
|
|
39
|
+
id: 1,
|
|
40
|
+
m: 'daemon.hello',
|
|
41
|
+
p: { client, auth: { scheme: 'none' } },
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
void (async () => {
|
|
46
|
+
for await (const chunk of socket) {
|
|
47
|
+
chunks.push(chunk);
|
|
48
|
+
first.resolve();
|
|
49
|
+
|
|
50
|
+
await held.promise;
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
|
|
54
|
+
await first.promise;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
chunks,
|
|
58
|
+
countUnreadBytes: () => socket.readableLength,
|
|
59
|
+
[Symbol.dispose]: stop,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'node:net';
|
|
2
2
|
import type { Socket } from 'node:net';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
3
4
|
|
|
4
5
|
interface StubStalledListener {
|
|
5
6
|
readonly [Symbol.dispose]: () => void;
|
|
@@ -9,8 +10,9 @@ interface StubStalledListener {
|
|
|
9
10
|
* A stand-in for a peer that stops reading: it listens on the unix socket
|
|
10
11
|
* path and accepts every connection but never reads from it, so what the
|
|
11
12
|
* kernel does not buffer stays queued on the sender's side. Resolves once
|
|
12
|
-
* it listens.
|
|
13
|
-
*
|
|
13
|
+
* it listens. Once the current test finishes, it destroys every connection
|
|
14
|
+
* it accepted and stops, so it must run inside a test; disposal does so
|
|
15
|
+
* sooner, and a second stop does nothing.
|
|
14
16
|
*/
|
|
15
17
|
export async function startStubStalledListener(path: string): Promise<StubStalledListener> {
|
|
16
18
|
const peers = new Set<Socket>();
|
|
@@ -28,13 +30,15 @@ export async function startStubStalledListener(path: string): Promise<StubStalle
|
|
|
28
30
|
|
|
29
31
|
await listening.promise;
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const stop = registerTestCleanup(() => {
|
|
34
|
+
for (const peer of peers) {
|
|
35
|
+
peer.destroy();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
server.close();
|
|
39
|
+
});
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
return {
|
|
42
|
+
[Symbol.dispose]: stop,
|
|
39
43
|
};
|
|
40
44
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { PROTOCOL_V } from '../protocol/protocol';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
3
4
|
|
|
4
5
|
interface StubTCPDaemon {
|
|
5
6
|
readonly port: number;
|
|
@@ -23,7 +24,9 @@ const REQUEST = z.object({ id: z.number(), m: z.string() });
|
|
|
23
24
|
* newline-delimited protocol requests and answers each with an ok that holds
|
|
24
25
|
* the request's method, recording every method in `seen`. It sends no
|
|
25
26
|
* handshake of its own and checks no token. `reads` counts the reads it has
|
|
26
|
-
* taken from every connection.
|
|
27
|
+
* taken from every connection. It stops once the current test finishes, so
|
|
28
|
+
* it must run inside a test; disposal stops it sooner, and a second stop
|
|
29
|
+
* does nothing.
|
|
27
30
|
*/
|
|
28
31
|
export function startStubTCPDaemon(): StubTCPDaemon {
|
|
29
32
|
const seen: string[] = [];
|
|
@@ -57,14 +60,16 @@ export function startStubTCPDaemon(): StubTCPDaemon {
|
|
|
57
60
|
},
|
|
58
61
|
});
|
|
59
62
|
|
|
63
|
+
const stop = registerTestCleanup(() => {
|
|
64
|
+
server.stop(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
60
67
|
return {
|
|
61
68
|
port: server.port,
|
|
62
69
|
seen,
|
|
63
70
|
get reads() {
|
|
64
71
|
return reads;
|
|
65
72
|
},
|
|
66
|
-
[Symbol.dispose]:
|
|
67
|
-
server.stop(true);
|
|
68
|
-
},
|
|
73
|
+
[Symbol.dispose]: stop,
|
|
69
74
|
};
|
|
70
75
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* A stand-in for better-auth's telemetry endpoint on a real HTTP port of the
|
|
3
5
|
* loopback address. A process whose `BETTER_AUTH_TELEMETRY_ENDPOINT` holds
|
|
4
6
|
* `url` sends its telemetry here. Every request is answered with HTTP 204,
|
|
5
|
-
* and `received` holds the URL of each request, in order.
|
|
6
|
-
*
|
|
7
|
+
* and `received` holds the URL of each request, in order. The server stops
|
|
8
|
+
* once the current test finishes, so it must run inside a test; disposal
|
|
9
|
+
* stops it sooner and resolves once it has stopped, and a second stop does
|
|
10
|
+
* nothing.
|
|
7
11
|
*/
|
|
8
12
|
export function startStubTelemetryCollector() {
|
|
9
13
|
const received: string[] = [];
|
|
@@ -18,9 +22,11 @@ export function startStubTelemetryCollector() {
|
|
|
18
22
|
},
|
|
19
23
|
});
|
|
20
24
|
|
|
25
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
26
|
+
|
|
21
27
|
return {
|
|
22
28
|
url: `http://127.0.0.1:${String(server.port)}/`,
|
|
23
29
|
received,
|
|
24
|
-
[Symbol.asyncDispose]:
|
|
30
|
+
[Symbol.asyncDispose]: stop,
|
|
25
31
|
};
|
|
26
32
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
3
|
+
interface StubUnansweringListener {
|
|
4
|
+
readonly port: number;
|
|
5
|
+
|
|
6
|
+
// What each read took from any connection, decoded, in arrival order.
|
|
7
|
+
readonly received: readonly string[];
|
|
8
|
+
|
|
9
|
+
readonly [Symbol.dispose]: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A stand-in for a daemon that accepts a connection and never answers it:
|
|
14
|
+
* on a loopback port, it records what each read takes from a connection,
|
|
15
|
+
* sends nothing back, and keeps the connection open. It stops, dropping
|
|
16
|
+
* every connection it holds, once the current test finishes, so it must run
|
|
17
|
+
* inside a test; disposal stops it sooner, and a second stop does nothing.
|
|
18
|
+
*/
|
|
19
|
+
export function startStubUnansweringListener(): StubUnansweringListener {
|
|
20
|
+
const received: string[] = [];
|
|
21
|
+
|
|
22
|
+
const server = Bun.listen({
|
|
23
|
+
hostname: '127.0.0.1',
|
|
24
|
+
port: 0,
|
|
25
|
+
socket: {
|
|
26
|
+
data(_socket, data) {
|
|
27
|
+
received.push(data.toString());
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const stop = registerTestCleanup(() => {
|
|
33
|
+
server.stop(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
port: server.port,
|
|
38
|
+
received,
|
|
39
|
+
[Symbol.dispose]: stop,
|
|
40
|
+
};
|
|
41
|
+
}
|