@zgeoff/atc 3.3.1 → 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 +5 -3
- package/src/agents/agent-adapter.ts +10 -0
- package/src/agents/build-agent-adapters.ts +21 -3
- package/src/agents/build-cli-argv.ts +2 -4
- package/src/agents/claude-adapter.ts +13 -3
- package/src/agents/codex-adapter.ts +31 -2
- package/src/agents/gateway-adapter.ts +20 -8
- package/src/agents/grok-adapter.ts +16 -13
- package/src/agents/load-claude-config-bundle.ts +6 -6
- package/src/agents/print-codex-hook-file.ts +7 -3
- package/src/agents/print-grok-hook-file.ts +8 -3
- package/src/agents/render-sdk-message.ts +57 -0
- package/src/agents/resolve-agent-home.ts +7 -3
- package/src/agents/resolve-claude-global-config-path.ts +4 -4
- package/src/agents/start-claude-headless-run.ts +2 -57
- package/src/agents/update-claude-project-trust.ts +41 -30
- package/src/agents/with-claude-config-lock.ts +37 -12
- package/src/agents/write-atc-bridge.ts +10 -7
- package/src/agents/write-hook-settings.ts +13 -7
- package/src/client/boot-daemon.ts +122 -39
- package/src/client/dirs.ts +4 -2
- package/src/client/index.ts +17 -1
- package/src/client/log-client-event.ts +20 -0
- package/src/client/resolve-workspace-root.ts +10 -5
- package/src/client/spawn-picker.ts +200 -121
- package/src/client/ui.ts +7 -7
- package/src/clients.ts +41 -11
- package/src/daemon/daemon.ts +143 -33
- package/src/daemon/event-signal.ts +19 -3
- package/src/daemon/make-hook-runner.ts +82 -22
- package/src/daemon/permission-registry.ts +11 -6
- package/src/daemon/restore-fleet.ts +62 -17
- package/src/daemon/run-eject-handoff.ts +20 -2
- package/src/daemon/session-runtime.ts +0 -4
- package/src/daemon/sessions.ts +6 -0
- package/src/daemon/start-tcp-listener.ts +12 -10
- package/src/federation/daemon-caller.ts +25 -4
- package/src/federation/daemon-pool.ts +3 -1
- package/src/federation/open-gateway-caller.ts +6 -1
- package/src/gateway.ts +2 -179
- package/src/grants.ts +36 -11
- package/src/hook-report.ts +17 -3
- package/src/mcp/read-report-texts.ts +11 -7
- package/src/mcp/start-mcp-http-server.ts +5 -1
- package/src/mcp-http-server.ts +88 -32
- package/src/report.ts +21 -3
- package/src/run-bridge-tap.ts +36 -7
- package/src/run-config-migrate.ts +35 -11
- package/src/run-gateway-cli.ts +327 -0
- package/src/run-gateway.ts +51 -18
- package/src/shared/build-atc-command.ts +7 -3
- package/src/shared/bun-sqlite-driver.ts +10 -0
- package/src/shared/claim-daemon-lock.ts +10 -4
- package/src/shared/collect-dir-roots.ts +6 -6
- package/src/shared/collect-hooks.ts +7 -6
- package/src/shared/config.ts +19 -9
- package/src/shared/system-clock.ts +24 -0
- package/src/sources/github/collect-github-repos.ts +1 -1
- package/src/sources/github/read-git-protocol.ts +1 -1
- package/src/sources/github/run-gh.ts +24 -9
- package/src/store/state-store.ts +57 -7
- package/src/tap.ts +44 -13
- package/src/test-utils/build-mock-agent-adapter.ts +25 -0
- package/src/test-utils/build-mock-agent-entry.ts +32 -0
- package/src/test-utils/build-mock-auth-binding.ts +28 -0
- package/src/test-utils/build-mock-auth-profile.ts +62 -0
- package/src/test-utils/build-mock-daemon-record.ts +26 -0
- package/src/test-utils/build-mock-execution-target.ts +25 -0
- package/src/test-utils/build-mock-fleet-entry.ts +33 -0
- package/src/test-utils/build-mock-gateway-config.ts +32 -0
- package/src/test-utils/build-mock-imp-identity.ts +18 -0
- package/src/test-utils/build-mock-imp-session-request.ts +53 -0
- package/src/test-utils/build-mock-key-binding-claim.ts +34 -0
- package/src/test-utils/build-mock-message-record.ts +26 -0
- package/src/test-utils/build-mock-mirror-session.ts +42 -0
- package/src/test-utils/build-mock-registry-daemon.ts +28 -0
- package/src/test-utils/build-mock-session-descriptor.ts +66 -0
- package/src/test-utils/build-mock-session.ts +43 -0
- package/src/test-utils/build-mock-sortable-session-view.ts +20 -0
- 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-attention-adapter.ts +22 -0
- package/src/test-utils/build-stub-bridge-context.ts +20 -0
- package/src/test-utils/build-stub-brokered-agent-adapter.ts +96 -0
- package/src/test-utils/build-stub-brokered-gateway-adapter.ts +50 -0
- package/src/test-utils/build-stub-channel-opener.ts +45 -0
- package/src/test-utils/build-stub-claude-headless-run.ts +13 -0
- package/src/test-utils/build-stub-client-screens.ts +33 -0
- package/src/test-utils/build-stub-clock.ts +67 -0
- package/src/test-utils/build-stub-daemon-context.ts +81 -0
- package/src/test-utils/build-stub-daemon-requests.ts +103 -0
- package/src/test-utils/{fixture-dir-provider.ts → build-stub-dir-provider.ts} +20 -12
- package/src/test-utils/build-stub-execution-provider.ts +112 -0
- package/src/test-utils/build-stub-fleet-caller.ts +60 -0
- package/src/test-utils/build-stub-forking-gh.ts +10 -0
- package/src/test-utils/build-stub-gh.ts +50 -0
- package/src/test-utils/build-stub-handoff-daemon.ts +11 -0
- package/src/test-utils/build-stub-harness-relay.ts +50 -0
- package/src/test-utils/build-stub-headless-runner.ts +60 -0
- package/src/test-utils/build-stub-held-gh.ts +19 -0
- package/src/test-utils/build-stub-held-inbox-source.ts +36 -0
- package/src/test-utils/build-stub-held-provider.ts +58 -0
- package/src/test-utils/build-stub-held-zoxide.ts +13 -0
- package/src/test-utils/build-stub-host-hold.ts +31 -0
- package/src/test-utils/build-stub-host-provider.ts +26 -0
- package/src/test-utils/build-stub-log.ts +15 -0
- package/src/test-utils/build-stub-mcp-claude.ts +26 -0
- package/src/test-utils/build-stub-mcp-grok.ts +19 -0
- package/src/test-utils/build-stub-mcp-stdio-server.ts +20 -0
- package/src/test-utils/build-stub-peer-socket.ts +81 -0
- package/src/test-utils/build-stub-picker-host.ts +36 -0
- package/src/test-utils/build-stub-proxied-agent-adapter.ts +29 -0
- package/src/test-utils/build-stub-pty-provider.ts +53 -0
- package/src/test-utils/build-stub-recording-atc.ts +10 -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/build-stub-signed-in-gh.ts +18 -0
- package/src/test-utils/build-stub-signed-out-gh.ts +8 -0
- package/src/test-utils/build-stub-soft-kill-provider.ts +29 -0
- package/src/test-utils/build-stub-targets.ts +58 -0
- package/src/test-utils/build-stub-terminal.ts +19 -0
- package/src/test-utils/build-stub-timeout-scheduler.ts +48 -0
- package/src/test-utils/build-stub-tui-claude.ts +47 -0
- package/src/test-utils/build-stub-tui-grok.ts +53 -0
- package/src/test-utils/build-stub-wait-clock.ts +23 -0
- package/src/test-utils/build-target-options-from-config.ts +37 -0
- package/src/test-utils/collect-process-tree.ts +75 -0
- package/src/test-utils/create-git-fixture.ts +117 -0
- package/src/test-utils/create-migrated-state-db.ts +12 -10
- package/src/test-utils/create-stub-bin.ts +19 -0
- package/src/test-utils/create-stub-claude.ts +75 -0
- package/src/test-utils/create-stub-codex.ts +31 -0
- package/src/test-utils/create-stub-composer.ts +65 -0
- package/src/test-utils/create-stub-echo-claude.ts +21 -0
- package/src/test-utils/create-stub-failing-agent-adapter.ts +111 -0
- package/src/test-utils/create-stub-grok.ts +44 -0
- package/src/test-utils/create-stub-guest-clis.ts +55 -0
- package/src/test-utils/create-stub-harness-guest.ts +35 -0
- package/src/test-utils/{fixture-imp-port.ts → create-stub-imp-port.ts} +144 -63
- package/src/test-utils/create-stub-recording-claude.ts +20 -0
- 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/{setup-fake-systemd.ts → create-stub-systemd.ts} +15 -9
- package/src/test-utils/get-gateway-config.ts +3 -3
- package/src/test-utils/get-only-effect-ref.ts +21 -0
- package/src/test-utils/get-only-imp-name.ts +19 -0
- package/src/test-utils/get-records.ts +20 -0
- package/src/test-utils/get-string.ts +14 -0
- package/src/test-utils/keys.ts +26 -0
- package/src/test-utils/merge-deep.ts +36 -0
- package/src/test-utils/mock-overrides.ts +13 -0
- package/src/test-utils/open-repo-step.ts +24 -0
- package/src/test-utils/read-query-plan.ts +22 -0
- package/src/test-utils/register-test-cleanup.ts +63 -0
- package/src/test-utils/resolve-atc-command.ts +13 -0
- package/src/test-utils/resolve-gateway-command.ts +13 -0
- package/src/test-utils/run-atc.ts +56 -0
- package/src/test-utils/run-legacy-daemon.ts +27 -5
- package/src/test-utils/run-listener-daemon.ts +5 -10
- package/src/test-utils/run-mcp-authorization.ts +7 -4
- package/src/test-utils/run-source-daemon.ts +2 -2
- package/src/test-utils/seed-faker.ts +7 -0
- package/src/test-utils/send-lines-before-handshake.ts +35 -0
- package/src/test-utils/send-mcp-request.ts +25 -0
- package/src/test-utils/setup-mcp-home.ts +110 -0
- package/src/test-utils/setup-mcp-http.ts +26 -30
- package/src/test-utils/setup-temp-dir.ts +7 -6
- package/src/test-utils/spawn-claude-session.ts +34 -0
- package/src/test-utils/spawn-grok-session.ts +39 -0
- package/src/test-utils/start-cut-proxy.ts +29 -8
- package/src/test-utils/start-daemon-process.ts +193 -0
- package/src/test-utils/start-git-http-server.ts +28 -4
- package/src/test-utils/start-mcp-stdio.ts +190 -0
- package/src/test-utils/start-output-capture.ts +20 -0
- 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 +105 -0
- package/src/test-utils/start-stub-impd-info.ts +38 -0
- package/src/test-utils/start-stub-impd.ts +143 -0
- package/src/test-utils/start-stub-legacy-daemon.ts +171 -0
- package/src/test-utils/start-stub-mcp-server.ts +55 -0
- package/src/test-utils/start-stub-recording-listener.ts +47 -0
- package/src/test-utils/start-stub-refusing-listener.ts +43 -0
- package/src/test-utils/start-stub-reporter-socket.ts +79 -0
- package/src/test-utils/start-stub-restore-daemon.ts +59 -0
- package/src/test-utils/start-stub-session-bridge.ts +100 -0
- package/src/test-utils/start-stub-silent-server.ts +38 -0
- package/src/test-utils/start-stub-source-daemon.ts +41 -0
- package/src/test-utils/start-stub-stalled-client.ts +61 -0
- package/src/test-utils/start-stub-stalled-listener.ts +44 -0
- package/src/test-utils/start-stub-tap.ts +38 -0
- package/src/test-utils/start-stub-tcp-daemon.ts +75 -0
- package/src/test-utils/start-stub-telemetry-collector.ts +32 -0
- 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 +229 -0
- package/src/test-utils/start-tui-harness.ts +291 -0
- package/src/test-utils/subscribe-to-socket-lines.ts +81 -14
- package/src/test-utils/try-send-request.ts +29 -0
- package/src/test-utils/wait-for-event.ts +41 -0
- package/src/test-utils/wait-for.ts +15 -3
- package/src/verify-restored-fleet.ts +30 -17
- package/src/workspace/check-repository-access.ts +4 -0
- package/src/workspace/collect-remote-refs.ts +4 -2
- package/src/workspace/run-git.ts +6 -0
- package/src/test-utils/start-legacy-daemon.ts +0 -111
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { SocketHandler } from 'bun';
|
|
2
|
+
import type { DaemonFeature } from '../protocol/daemon-features';
|
|
3
|
+
import {
|
|
4
|
+
MAX_CHUNK,
|
|
5
|
+
MAX_LINE,
|
|
6
|
+
PROTOCOL_V,
|
|
7
|
+
decodeMessage,
|
|
8
|
+
encodeMessage,
|
|
9
|
+
} from '../protocol/protocol';
|
|
10
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
11
|
+
|
|
12
|
+
interface ReceivedRequest {
|
|
13
|
+
readonly m: string;
|
|
14
|
+
readonly p: Readonly<Record<string, unknown>> | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A TCP address to listen on in place of a unix socket; port 0 lets the
|
|
18
|
+
// kernel choose one.
|
|
19
|
+
interface StubLegacyDaemonTCPAddress {
|
|
20
|
+
readonly hostname: string;
|
|
21
|
+
readonly port: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface StubLegacyDaemonOptions {
|
|
25
|
+
readonly features?: readonly DaemonFeature[];
|
|
26
|
+
|
|
27
|
+
// The protocol version the daemon speaks; a hello on any other version is
|
|
28
|
+
// refused with `protocol_mismatch`, as a real daemon refuses it.
|
|
29
|
+
readonly protocol?: number;
|
|
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';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* An older atc daemon listening on a unix socket, or on the TCP address
|
|
41
|
+
* given in its place, and speaking the current
|
|
42
|
+
* protocol version, or the one `protocol` holds. Its `daemon.hello` announces `features` when the options
|
|
43
|
+
* hold that list, and announces none without it, as a daemon from before
|
|
44
|
+
* features did. It answers `daemon.hello`, `daemon.ping`, and each method in
|
|
45
|
+
* `replies` with that method's reply, and refuses every other method with
|
|
46
|
+
* `unknown_method`. Every request it receives is recorded in `requests`, and
|
|
47
|
+
* `connections` counts the connections it accepted and those still open.
|
|
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.
|
|
52
|
+
*/
|
|
53
|
+
export function startStubLegacyDaemon(
|
|
54
|
+
address: string | StubLegacyDaemonTCPAddress,
|
|
55
|
+
options: StubLegacyDaemonOptions = {},
|
|
56
|
+
) {
|
|
57
|
+
const answers: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
|
|
58
|
+
'daemon.hello': {
|
|
59
|
+
daemon: 'atc/legacy-build',
|
|
60
|
+
limits: { maxLine: MAX_LINE, maxChunk: MAX_CHUNK },
|
|
61
|
+
lastUsedAgent: 'claude',
|
|
62
|
+
...(options.features === undefined ? {} : { features: options.features }),
|
|
63
|
+
},
|
|
64
|
+
'daemon.ping': {},
|
|
65
|
+
...options.replies,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const protocol = options.protocol ?? PROTOCOL_V;
|
|
69
|
+
const requests: ReceivedRequest[] = [];
|
|
70
|
+
const connections = { accepted: 0, open: 0 };
|
|
71
|
+
|
|
72
|
+
const handlers: SocketHandler<{ buffer: string }> = {
|
|
73
|
+
open(socket) {
|
|
74
|
+
socket.data = { buffer: '' };
|
|
75
|
+
connections.accepted += 1;
|
|
76
|
+
connections.open += 1;
|
|
77
|
+
},
|
|
78
|
+
close() {
|
|
79
|
+
connections.open -= 1;
|
|
80
|
+
},
|
|
81
|
+
data(socket, chunk) {
|
|
82
|
+
const lines = `${socket.data.buffer}${chunk.toString()}`.split('\n');
|
|
83
|
+
|
|
84
|
+
socket.data.buffer = lines.pop() ?? '';
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
const decoded = decodeMessage(line);
|
|
88
|
+
|
|
89
|
+
if (decoded.kind !== 'request') {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const req = decoded.msg;
|
|
94
|
+
|
|
95
|
+
requests.push({ m: req.m, p: req.p });
|
|
96
|
+
|
|
97
|
+
if (req.m === 'daemon.hello' && req.v !== protocol) {
|
|
98
|
+
const client = req.p?.['client'];
|
|
99
|
+
|
|
100
|
+
socket.write(
|
|
101
|
+
encodeMessage({
|
|
102
|
+
v: protocol,
|
|
103
|
+
id: req.id,
|
|
104
|
+
err: {
|
|
105
|
+
code: 'protocol_mismatch',
|
|
106
|
+
msg: `${String(client)} speaks protocol v${req.v}, daemon atc/legacy-build speaks v${protocol}; restart the daemon so both run the same build`,
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const answer = answers[req.m];
|
|
115
|
+
|
|
116
|
+
const reply =
|
|
117
|
+
answer === undefined
|
|
118
|
+
? {
|
|
119
|
+
v: protocol,
|
|
120
|
+
id: req.id,
|
|
121
|
+
err: { code: 'unknown_method' as const, msg: `unknown method '${req.m}'` },
|
|
122
|
+
}
|
|
123
|
+
: { v: protocol, id: req.id, ok: answer };
|
|
124
|
+
|
|
125
|
+
socket.write(encodeMessage(reply));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const server = startListener(address, handlers);
|
|
131
|
+
const stop = options.owner === 'caller' ? server.stop : registerTestCleanup(server.stop);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
requests,
|
|
135
|
+
connections,
|
|
136
|
+
port: server.port,
|
|
137
|
+
stop,
|
|
138
|
+
[Symbol.dispose]: stop,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Listens on the unix socket path, or on the TCP address in its place, and
|
|
143
|
+
// returns the port bound, `null` for a unix socket.
|
|
144
|
+
function startListener(
|
|
145
|
+
address: string | StubLegacyDaemonTCPAddress,
|
|
146
|
+
handlers: Readonly<SocketHandler<{ buffer: string }>>,
|
|
147
|
+
): { readonly port: number | null; readonly stop: () => void } {
|
|
148
|
+
if (typeof address === 'string') {
|
|
149
|
+
const unix = Bun.listen<{ buffer: string }>({ unix: address, socket: handlers });
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
port: null,
|
|
153
|
+
stop: () => {
|
|
154
|
+
unix.stop(true);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const tcp = Bun.listen<{ buffer: string }>({
|
|
160
|
+
hostname: address.hostname,
|
|
161
|
+
port: address.port,
|
|
162
|
+
socket: handlers,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
port: tcp.port,
|
|
167
|
+
stop: () => {
|
|
168
|
+
tcp.stop(true);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
3
|
+
interface StubMCPRequest {
|
|
4
|
+
readonly method: string;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly authorization: string | null;
|
|
7
|
+
readonly type: string | null;
|
|
8
|
+
readonly body: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface StubMCPServer {
|
|
12
|
+
readonly url: string;
|
|
13
|
+
|
|
14
|
+
// Every request the stand-in received, in arrival order.
|
|
15
|
+
readonly requests: readonly StubMCPRequest[];
|
|
16
|
+
|
|
17
|
+
readonly [Symbol.dispose]: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A stand-in for an MCP HTTP server on a loopback port: it answers every
|
|
22
|
+
* request with the JSON body given, and records each request's method,
|
|
23
|
+
* path, authorization and content-type headers, and JSON body. `url` holds
|
|
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.
|
|
26
|
+
*/
|
|
27
|
+
export function startStubMCPServer(answer: unknown): StubMCPServer {
|
|
28
|
+
const requests: StubMCPRequest[] = [];
|
|
29
|
+
|
|
30
|
+
const server = Bun.serve({
|
|
31
|
+
hostname: '127.0.0.1',
|
|
32
|
+
port: 0,
|
|
33
|
+
async fetch(request) {
|
|
34
|
+
requests.push({
|
|
35
|
+
method: request.method,
|
|
36
|
+
path: new URL(request.url).pathname,
|
|
37
|
+
authorization: request.headers.get('authorization'),
|
|
38
|
+
type: request.headers.get('content-type'),
|
|
39
|
+
body: await request.json(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return Response.json(answer);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
url: `http://127.0.0.1:${String(server.port)}`,
|
|
50
|
+
requests,
|
|
51
|
+
[Symbol.dispose]: () => {
|
|
52
|
+
void stop();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
3
|
+
interface StubRefusingListener {
|
|
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's TCP listener refusing a connection whose first
|
|
14
|
+
* line is not a handshake: on a loopback port, it records what each read
|
|
15
|
+
* takes from a connection and ends that connection, answering nothing.
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
export function startStubRefusingListener(): StubRefusingListener {
|
|
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
|
+
socket.end();
|
|
29
|
+
},
|
|
30
|
+
error() {},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const stop = registerTestCleanup(() => {
|
|
35
|
+
server.stop(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
port: server.port,
|
|
40
|
+
received,
|
|
41
|
+
[Symbol.dispose]: stop,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
import { waitFor } from './wait-for';
|
|
3
|
+
|
|
4
|
+
interface StubReporterSocket {
|
|
5
|
+
// Every complete line received so far, newline removed, in arrival order.
|
|
6
|
+
readonly lines: readonly string[];
|
|
7
|
+
|
|
8
|
+
// How many reads the stand-in has taken from its connections, so a test
|
|
9
|
+
// can wait until one piece of a split write has arrived.
|
|
10
|
+
readonly reads: number;
|
|
11
|
+
readonly waitForLine: (timeoutMs?: number) => Promise<string>;
|
|
12
|
+
readonly [Symbol.dispose]: () => void;
|
|
13
|
+
readonly [Symbol.asyncDispose]: () => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A stand-in for the daemon's reporter socket: listens on the unix socket
|
|
18
|
+
* path and collects each newline-terminated line any connection writes,
|
|
19
|
+
* buffering a partial line per connection, as the daemon reads the lines
|
|
20
|
+
* hook and report commands send. It never writes back, and leaves each
|
|
21
|
+
* connection for the sender to close. `waitForLine` resolves with the first
|
|
22
|
+
* line once one has arrived, and rejects naming the socket when none arrives
|
|
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.
|
|
26
|
+
*/
|
|
27
|
+
export function startStubReporterSocket(path: string): StubReporterSocket {
|
|
28
|
+
const lines: string[] = [];
|
|
29
|
+
let reads = 0;
|
|
30
|
+
|
|
31
|
+
const server = Bun.listen<{ pending: string }>({
|
|
32
|
+
unix: path,
|
|
33
|
+
socket: {
|
|
34
|
+
open(socket) {
|
|
35
|
+
socket.data = { pending: '' };
|
|
36
|
+
},
|
|
37
|
+
data(socket, chunk) {
|
|
38
|
+
reads += 1;
|
|
39
|
+
|
|
40
|
+
const parts = `${socket.data.pending}${chunk.toString()}`.split('\n');
|
|
41
|
+
|
|
42
|
+
socket.data.pending = parts.pop() ?? '';
|
|
43
|
+
|
|
44
|
+
lines.push(...parts);
|
|
45
|
+
},
|
|
46
|
+
error() {},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const stop = registerTestCleanup(() => {
|
|
51
|
+
server.stop(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
lines,
|
|
56
|
+
get reads() {
|
|
57
|
+
return reads;
|
|
58
|
+
},
|
|
59
|
+
waitForLine: (timeoutMs = 5000) =>
|
|
60
|
+
waitFor(
|
|
61
|
+
() => {
|
|
62
|
+
const [first] = lines;
|
|
63
|
+
|
|
64
|
+
if (first === undefined) {
|
|
65
|
+
throw new Error(`no line has arrived at ${path}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return first;
|
|
69
|
+
},
|
|
70
|
+
{ timeoutMs },
|
|
71
|
+
),
|
|
72
|
+
[Symbol.dispose]: stop,
|
|
73
|
+
[Symbol.asyncDispose]: () => {
|
|
74
|
+
stop();
|
|
75
|
+
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { LineDecoder } from '../protocol/line-decoder';
|
|
2
|
+
import { PROTOCOL_V, decodeMessage, encodeMessage } from '../protocol/protocol';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A daemon stand-in on a unix socket path that speaks the real wire format
|
|
7
|
+
* to a fleet restore check: it answers every `fleet.restore` with an empty
|
|
8
|
+
* result, answers each other request with the next reply the test pushes
|
|
9
|
+
* onto `lists`, and withholds the answer to any request that finds `lists`
|
|
10
|
+
* empty. `methods` holds the method of each request it took, in order.
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
export function startStubRestoreDaemon(socketPath: string) {
|
|
15
|
+
const lists: Readonly<Record<string, unknown>>[] = [];
|
|
16
|
+
const methods: string[] = [];
|
|
17
|
+
|
|
18
|
+
// Each connection gets a decoder of its own, so a partial line from one
|
|
19
|
+
// client never joins a line from another.
|
|
20
|
+
const server = Bun.listen<{ lines: LineDecoder }>({
|
|
21
|
+
unix: socketPath,
|
|
22
|
+
socket: {
|
|
23
|
+
open(socket) {
|
|
24
|
+
socket.data = { lines: new LineDecoder() };
|
|
25
|
+
},
|
|
26
|
+
data(socket, buf) {
|
|
27
|
+
const requests = socket.data.lines
|
|
28
|
+
.splitChunk(buf)
|
|
29
|
+
.map((line) => decodeMessage(line))
|
|
30
|
+
.filter((decoded) => decoded.kind === 'request');
|
|
31
|
+
|
|
32
|
+
for (const request of requests) {
|
|
33
|
+
methods.push(request.msg.m);
|
|
34
|
+
|
|
35
|
+
const ok = request.msg.m === 'fleet.restore' ? {} : lists.shift();
|
|
36
|
+
|
|
37
|
+
if (ok !== undefined) {
|
|
38
|
+
socket.write(encodeMessage({ v: PROTOCOL_V, id: request.msg.id, ok }));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const stop = registerTestCleanup(() => {
|
|
46
|
+
server.stop(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
lists,
|
|
51
|
+
methods,
|
|
52
|
+
[Symbol.dispose]: stop,
|
|
53
|
+
[Symbol.asyncDispose]: () => {
|
|
54
|
+
stop();
|
|
55
|
+
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { isRecord } from '../shared/report';
|
|
2
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
3
|
+
|
|
4
|
+
type BridgeRequest = Readonly<Record<string, unknown>>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The lines the bridge writes back for one request, in order, or `null` to
|
|
8
|
+
* end the connection without answering.
|
|
9
|
+
*/
|
|
10
|
+
type BridgeResponder = (request: BridgeRequest) => readonly BridgeRequest[] | null;
|
|
11
|
+
|
|
12
|
+
interface StubSessionBridge {
|
|
13
|
+
// Every request line received so far, parsed, in arrival order.
|
|
14
|
+
readonly requests: readonly BridgeRequest[];
|
|
15
|
+
|
|
16
|
+
// How many reads the stand-in has taken from its connections, so a test
|
|
17
|
+
// can wait until one piece of a split write has arrived.
|
|
18
|
+
readonly reads: number;
|
|
19
|
+
readonly [Symbol.dispose]: () => void;
|
|
20
|
+
readonly [Symbol.asyncDispose]: () => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A stand-in for the session bridge a remote host's commands dial: listens
|
|
25
|
+
* on the unix socket path and reads newline-delimited JSON requests,
|
|
26
|
+
* buffering a partial line per connection. It opens every tap, answering a
|
|
27
|
+
* `tap.open` request with `ok` under the request's id as the real bridge
|
|
28
|
+
* does once it attaches the tap, and hands every other request to the
|
|
29
|
+
* responder, writing back each line it returns. A line that is not a JSON
|
|
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.
|
|
33
|
+
*/
|
|
34
|
+
export function startStubSessionBridge(path: string, respond: BridgeResponder): StubSessionBridge {
|
|
35
|
+
const requests: BridgeRequest[] = [];
|
|
36
|
+
let reads = 0;
|
|
37
|
+
|
|
38
|
+
const server = Bun.listen<{ pending: string }>({
|
|
39
|
+
unix: path,
|
|
40
|
+
socket: {
|
|
41
|
+
open(socket) {
|
|
42
|
+
socket.data = { pending: '' };
|
|
43
|
+
},
|
|
44
|
+
data(socket, chunk) {
|
|
45
|
+
reads += 1;
|
|
46
|
+
|
|
47
|
+
const lines = `${socket.data.pending}${chunk.toString()}`.split('\n');
|
|
48
|
+
|
|
49
|
+
socket.data.pending = lines.pop() ?? '';
|
|
50
|
+
|
|
51
|
+
for (const line of lines.filter((text) => text !== '')) {
|
|
52
|
+
const request = parseRequest(line);
|
|
53
|
+
|
|
54
|
+
requests.push(request);
|
|
55
|
+
|
|
56
|
+
const answers =
|
|
57
|
+
request['op'] === 'tap.open' ? [{ id: request['id'], ok: true }] : respond(request);
|
|
58
|
+
|
|
59
|
+
if (answers === null) {
|
|
60
|
+
socket.end();
|
|
61
|
+
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const answer of answers) {
|
|
66
|
+
socket.write(`${JSON.stringify(answer)}\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
error() {},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const stop = registerTestCleanup(() => {
|
|
75
|
+
server.stop(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
requests,
|
|
80
|
+
get reads() {
|
|
81
|
+
return reads;
|
|
82
|
+
},
|
|
83
|
+
[Symbol.dispose]: stop,
|
|
84
|
+
[Symbol.asyncDispose]: () => {
|
|
85
|
+
stop();
|
|
86
|
+
|
|
87
|
+
return Promise.resolve();
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseRequest(line: string): BridgeRequest {
|
|
93
|
+
try {
|
|
94
|
+
const parsed: unknown = JSON.parse(line);
|
|
95
|
+
|
|
96
|
+
return isRecord(parsed) && !Array.isArray(parsed) ? parsed : {};
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An HTTP stand-in for an upstream that hangs: it listens on a real port of
|
|
5
|
+
* the loopback address, takes every request, and never answers one. `paths`
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
export function startStubSilentServer() {
|
|
12
|
+
const paths: string[] = [];
|
|
13
|
+
|
|
14
|
+
const server = Bun.serve({
|
|
15
|
+
port: 0,
|
|
16
|
+
hostname: '127.0.0.1',
|
|
17
|
+
|
|
18
|
+
// A held request would otherwise end after Bun's idle timeout, and the
|
|
19
|
+
// upstream would stop being one that never answers.
|
|
20
|
+
idleTimeout: 0,
|
|
21
|
+
fetch: (request) => {
|
|
22
|
+
paths.push(new URL(request.url).pathname);
|
|
23
|
+
|
|
24
|
+
return new Promise<Response>(() => {});
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
url: `http://127.0.0.1:${String(server.port)}/`,
|
|
32
|
+
paths,
|
|
33
|
+
[Symbol.dispose]: () => {
|
|
34
|
+
void stop();
|
|
35
|
+
},
|
|
36
|
+
[Symbol.asyncDispose]: stop,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Starts a daemon composed with a fixture source, in a process of its own
|
|
6
|
+
* with the environment given, and resolves once it listens. Its home, sockets
|
|
7
|
+
* and state follow that environment as `atc daemon`'s would, so a client
|
|
8
|
+
* started with the same environment dials it instead of starting its own.
|
|
9
|
+
* `ATC_TEST_SOURCES` picks its sources, the fixture one unless it holds
|
|
10
|
+
* `none`; `ATC_TEST_FIXTURE_URL` holds the repository the fixture source
|
|
11
|
+
* lists, and `ATC_TEST_SOURCE_LOG` a file it appends each listing to.
|
|
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.
|
|
14
|
+
*/
|
|
15
|
+
export async function startStubSourceDaemon(env: Readonly<Record<string, string | undefined>>) {
|
|
16
|
+
const daemon = Bun.spawn([process.execPath, join(import.meta.dir, 'run-source-daemon.ts')], {
|
|
17
|
+
env: { ATC_TEST_SOURCES: 'fixture', ...env },
|
|
18
|
+
stdout: 'pipe',
|
|
19
|
+
stderr: 'ignore',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const stop = registerTestCleanup(async () => {
|
|
23
|
+
daemon.kill();
|
|
24
|
+
|
|
25
|
+
await daemon.exited;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const reader = daemon.stdout.getReader();
|
|
29
|
+
|
|
30
|
+
const first = await reader.read();
|
|
31
|
+
|
|
32
|
+
reader.releaseLock();
|
|
33
|
+
|
|
34
|
+
if (first.done) {
|
|
35
|
+
await stop();
|
|
36
|
+
|
|
37
|
+
throw new Error('the source daemon exited before it listened');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { pid: daemon.pid, [Symbol.asyncDispose]: stop };
|
|
41
|
+
}
|
|
@@ -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
|
+
}
|