@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,44 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
import type { Socket } from 'node:net';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
4
|
+
|
|
5
|
+
interface StubStalledListener {
|
|
6
|
+
readonly [Symbol.dispose]: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A stand-in for a peer that stops reading: it listens on the unix socket
|
|
11
|
+
* path and accepts every connection but never reads from it, so what the
|
|
12
|
+
* kernel does not buffer stays queued on the sender's side. Resolves once
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
export async function startStubStalledListener(path: string): Promise<StubStalledListener> {
|
|
18
|
+
const peers = new Set<Socket>();
|
|
19
|
+
|
|
20
|
+
const server = createServer((peer) => {
|
|
21
|
+
peer.pause();
|
|
22
|
+
peers.add(peer);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const listening = Promise.withResolvers<void>();
|
|
26
|
+
|
|
27
|
+
server.listen(path, () => {
|
|
28
|
+
listening.resolve();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await listening.promise;
|
|
32
|
+
|
|
33
|
+
const stop = registerTestCleanup(() => {
|
|
34
|
+
for (const peer of peers) {
|
|
35
|
+
peer.destroy();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
server.close();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
[Symbol.dispose]: stop,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DaemonClient } from '../client/daemon-client';
|
|
2
|
+
import type { EventMsg } from '../protocol/protocol';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Makes the client the session's tap and acks each message the tap is
|
|
6
|
+
* handed, the way an agent's inbox reader does, so the daemon drains the
|
|
7
|
+
* whole backlog one message per ack. `messages` holds each InboxMessage in
|
|
8
|
+
* the order it arrived and `closed` each InboxClosed. The client's event
|
|
9
|
+
* callback is replaced.
|
|
10
|
+
*/
|
|
11
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- a client is a live connection the tap drives
|
|
12
|
+
export async function startStubTap(client: DaemonClient, session: string) {
|
|
13
|
+
const messages: EventMsg[] = [];
|
|
14
|
+
const closed: EventMsg[] = [];
|
|
15
|
+
|
|
16
|
+
// An ack that a closing connection never answers changes nothing the
|
|
17
|
+
// caller reads.
|
|
18
|
+
const sendAck = async (message: unknown) => {
|
|
19
|
+
try {
|
|
20
|
+
await client.sendRequest('message.ack', { session, message });
|
|
21
|
+
} catch {}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
client.onEvent = (event) => {
|
|
25
|
+
if (event.ev === 'InboxMessage') {
|
|
26
|
+
messages.push(event);
|
|
27
|
+
void sendAck(event['message']);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (event.ev === 'InboxClosed') {
|
|
31
|
+
closed.push(event);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
await client.sendRequest('session.tap', { session });
|
|
36
|
+
|
|
37
|
+
return { messages, closed };
|
|
38
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PROTOCOL_V } from '../protocol/protocol';
|
|
3
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
4
|
+
|
|
5
|
+
interface StubTCPDaemon {
|
|
6
|
+
readonly port: number;
|
|
7
|
+
|
|
8
|
+
// Every request method the stand-in received, in arrival order.
|
|
9
|
+
readonly seen: string[];
|
|
10
|
+
|
|
11
|
+
// How many reads the stand-in has taken from its connections, so a test
|
|
12
|
+
// can wait until one piece of a split write has arrived before it sends
|
|
13
|
+
// the next.
|
|
14
|
+
readonly reads: number;
|
|
15
|
+
readonly [Symbol.dispose]: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The two request fields the stand-in reads; every other field passes
|
|
19
|
+
// unread.
|
|
20
|
+
const REQUEST = z.object({ id: z.number(), m: z.string() });
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A stand-in for an atc daemon's TCP listener on a loopback port: it reads
|
|
24
|
+
* newline-delimited protocol requests and answers each with an ok that holds
|
|
25
|
+
* the request's method, recording every method in `seen`. It sends no
|
|
26
|
+
* handshake of its own and checks no token. `reads` counts the reads it has
|
|
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.
|
|
30
|
+
*/
|
|
31
|
+
export function startStubTCPDaemon(): StubTCPDaemon {
|
|
32
|
+
const seen: string[] = [];
|
|
33
|
+
let reads = 0;
|
|
34
|
+
|
|
35
|
+
const server = Bun.listen<{ buffer: string }>({
|
|
36
|
+
hostname: '127.0.0.1',
|
|
37
|
+
port: 0,
|
|
38
|
+
socket: {
|
|
39
|
+
open(socket) {
|
|
40
|
+
socket.data = { buffer: '' };
|
|
41
|
+
},
|
|
42
|
+
data(socket, buf) {
|
|
43
|
+
reads += 1;
|
|
44
|
+
|
|
45
|
+
const lines = `${socket.data.buffer}${buf.toString()}`.split('\n');
|
|
46
|
+
|
|
47
|
+
socket.data.buffer = lines.pop() ?? '';
|
|
48
|
+
|
|
49
|
+
for (const line of lines.filter((part) => part !== '')) {
|
|
50
|
+
const request = REQUEST.parse(JSON.parse(line));
|
|
51
|
+
|
|
52
|
+
seen.push(request.m);
|
|
53
|
+
|
|
54
|
+
socket.write(
|
|
55
|
+
`${JSON.stringify({ v: PROTOCOL_V, id: request.id, ok: { m: request.m } })}\n`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
error() {},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const stop = registerTestCleanup(() => {
|
|
64
|
+
server.stop(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
port: server.port,
|
|
69
|
+
seen,
|
|
70
|
+
get reads() {
|
|
71
|
+
return reads;
|
|
72
|
+
},
|
|
73
|
+
[Symbol.dispose]: stop,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A stand-in for better-auth's telemetry endpoint on a real HTTP port of the
|
|
5
|
+
* loopback address. A process whose `BETTER_AUTH_TELEMETRY_ENDPOINT` holds
|
|
6
|
+
* `url` sends its telemetry here. Every request is answered with HTTP 204,
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
export function startStubTelemetryCollector() {
|
|
13
|
+
const received: string[] = [];
|
|
14
|
+
|
|
15
|
+
const server = Bun.serve({
|
|
16
|
+
hostname: '127.0.0.1',
|
|
17
|
+
port: 0,
|
|
18
|
+
fetch: (request) => {
|
|
19
|
+
received.push(request.url);
|
|
20
|
+
|
|
21
|
+
return new Response(null, { status: 204 });
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
url: `http://127.0.0.1:${String(server.port)}/`,
|
|
29
|
+
received,
|
|
30
|
+
[Symbol.asyncDispose]: stop,
|
|
31
|
+
};
|
|
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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
3
|
+
interface StubUnansweringUnixListener {
|
|
4
|
+
// What each read took from any connection, decoded, in arrival order.
|
|
5
|
+
readonly received: readonly string[];
|
|
6
|
+
|
|
7
|
+
readonly [Symbol.dispose]: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A stand-in for a daemon socket that takes a connection and never answers
|
|
12
|
+
* it: it listens on the unix socket path, records what each read takes from
|
|
13
|
+
* a connection, sends nothing back, and keeps the connection open. It
|
|
14
|
+
* stops, dropping every connection it holds, once the current test
|
|
15
|
+
* finishes, so it must run inside a test; disposal stops it sooner, and a
|
|
16
|
+
* second stop does nothing.
|
|
17
|
+
*/
|
|
18
|
+
export function startStubUnansweringUnixListener(path: string): StubUnansweringUnixListener {
|
|
19
|
+
const received: string[] = [];
|
|
20
|
+
|
|
21
|
+
const server = Bun.listen({
|
|
22
|
+
unix: path,
|
|
23
|
+
socket: {
|
|
24
|
+
data(_socket, data) {
|
|
25
|
+
received.push(data.toString());
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const stop = registerTestCleanup(() => {
|
|
31
|
+
server.stop(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
received,
|
|
36
|
+
[Symbol.dispose]: stop,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import type { Socket } from 'bun';
|
|
3
|
+
import { DaemonClient } from '../client/daemon-client';
|
|
4
|
+
import { startDaemon } from '../daemon/daemon';
|
|
5
|
+
import type { DaemonHandle, DaemonOptions } from '../daemon/daemon';
|
|
6
|
+
import type { EventMsg } from '../protocol/protocol';
|
|
7
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
8
|
+
import { setupTempDir } from './setup-temp-dir';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Where a test daemon keeps its sockets and state, all inside one temp
|
|
12
|
+
* directory, so the daemon's lock and record files land there too.
|
|
13
|
+
*/
|
|
14
|
+
interface TestDaemonPaths {
|
|
15
|
+
readonly dir: string;
|
|
16
|
+
readonly socketPath: string;
|
|
17
|
+
readonly reporterSocketPath: string;
|
|
18
|
+
readonly eventsSocketPath: string;
|
|
19
|
+
readonly dbPath: string;
|
|
20
|
+
readonly statusPath: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Every daemon option except the paths and the build string, which the
|
|
25
|
+
* harness owns.
|
|
26
|
+
*/
|
|
27
|
+
type TestDaemonOptions = Omit<
|
|
28
|
+
DaemonOptions,
|
|
29
|
+
'socketPath' | 'reporterSocketPath' | 'eventsSocketPath' | 'dbPath' | 'statusPath' | 'build'
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds the options for one boot from the daemon's paths. It runs before
|
|
34
|
+
* every boot, so it may also write what the daemon reads at start, such as
|
|
35
|
+
* a token file or a fake agent binary under the directory.
|
|
36
|
+
*/
|
|
37
|
+
type TestDaemonOptionsBuilder = (
|
|
38
|
+
paths: TestDaemonPaths,
|
|
39
|
+
) => TestDaemonOptions | Promise<TestDaemonOptions>;
|
|
40
|
+
|
|
41
|
+
interface TestDaemonConfig {
|
|
42
|
+
// The temp directory's name prefix.
|
|
43
|
+
readonly prefix?: string;
|
|
44
|
+
readonly options?: TestDaemonOptionsBuilder;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The build string the daemon and every client the harness opens send in
|
|
48
|
+
// their handshake.
|
|
49
|
+
const BUILD = 'atc/test-build';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A real daemon in a fresh temp directory, with a client that has already
|
|
53
|
+
* sent its handshake. The options builder chooses what the daemon wires;
|
|
54
|
+
* the harness sets the paths and the build, and collects the daemon's log
|
|
55
|
+
* lines unless the options set their own log. `events` collects every event
|
|
56
|
+
* the main client receives, across restarts. `openClient` opens another
|
|
57
|
+
* client over the unix socket and sends its handshake with the given
|
|
58
|
+
* params; `openTCPClient` connects to the TCP listener and sends nothing, so
|
|
59
|
+
* the test drives that handshake itself. `sendHookLines` writes reporter
|
|
60
|
+
* lines to the reporter socket and resolves once the daemon has closed the
|
|
61
|
+
* connection. `stop` closes every client and stops the daemon; `restart`
|
|
62
|
+
* does the same, then boots on the same paths and state with the options
|
|
63
|
+
* given or the last ones, and opens a new main client. Disposal stops what
|
|
64
|
+
* is running and removes the directory. That disposal runs once the current
|
|
65
|
+
* test finishes, so it must run inside a test; disposing sooner runs it
|
|
66
|
+
* then, and a second disposal does nothing. A first boot that fails runs it
|
|
67
|
+
* before the start rejects.
|
|
68
|
+
*/
|
|
69
|
+
export async function startTestDaemon(config: TestDaemonConfig = {}) {
|
|
70
|
+
const tmp = setupTempDir(config.prefix ?? 'atc-test-daemon-');
|
|
71
|
+
|
|
72
|
+
// Registered after the directory, so it releases first: the daemon stops
|
|
73
|
+
// before its directory is removed.
|
|
74
|
+
const stack = new AsyncDisposableStack();
|
|
75
|
+
|
|
76
|
+
const dispose = registerTestCleanup(() => stack.disposeAsync());
|
|
77
|
+
|
|
78
|
+
stack.use(tmp);
|
|
79
|
+
|
|
80
|
+
const paths: TestDaemonPaths = {
|
|
81
|
+
dir: tmp.dir,
|
|
82
|
+
socketPath: join(tmp.dir, 'daemon.sock'),
|
|
83
|
+
reporterSocketPath: join(tmp.dir, 'reporter.sock'),
|
|
84
|
+
eventsSocketPath: join(tmp.dir, 'events.sock'),
|
|
85
|
+
dbPath: join(tmp.dir, 'state.db'),
|
|
86
|
+
statusPath: join(tmp.dir, 'status.json'),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const logs: string[] = [];
|
|
90
|
+
const events: EventMsg[] = [];
|
|
91
|
+
|
|
92
|
+
const clients = new Set<DaemonClient>();
|
|
93
|
+
|
|
94
|
+
let buildOptions: TestDaemonOptionsBuilder = config.options ?? (() => ({}));
|
|
95
|
+
let live: DaemonHandle | null = null;
|
|
96
|
+
|
|
97
|
+
const openClient = async (hello: Readonly<Record<string, unknown>> = {}) => {
|
|
98
|
+
const client = await DaemonClient.open(paths.socketPath);
|
|
99
|
+
|
|
100
|
+
clients.add(client);
|
|
101
|
+
|
|
102
|
+
await client.sendRequest('daemon.hello', {
|
|
103
|
+
client: BUILD,
|
|
104
|
+
auth: { scheme: 'none' },
|
|
105
|
+
...hello,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return client;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const stop = async () => {
|
|
112
|
+
const stopping = live;
|
|
113
|
+
|
|
114
|
+
live = null;
|
|
115
|
+
|
|
116
|
+
for (const client of clients) {
|
|
117
|
+
client.stop();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
clients.clear();
|
|
121
|
+
|
|
122
|
+
await stopping?.stop();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
stack.defer(stop);
|
|
126
|
+
|
|
127
|
+
const boot = async () => {
|
|
128
|
+
const options = await buildOptions(paths);
|
|
129
|
+
|
|
130
|
+
const daemon = await startDaemon({
|
|
131
|
+
log: (line) => {
|
|
132
|
+
logs.push(line);
|
|
133
|
+
},
|
|
134
|
+
...options,
|
|
135
|
+
socketPath: paths.socketPath,
|
|
136
|
+
reporterSocketPath: paths.reporterSocketPath,
|
|
137
|
+
eventsSocketPath: paths.eventsSocketPath,
|
|
138
|
+
dbPath: paths.dbPath,
|
|
139
|
+
statusPath: paths.statusPath,
|
|
140
|
+
build: BUILD,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
live = daemon;
|
|
144
|
+
|
|
145
|
+
const client = await openClient();
|
|
146
|
+
|
|
147
|
+
client.onEvent = (event) => {
|
|
148
|
+
events.push(event);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return { daemon, client };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// A boot that fails stops what it started and removes the directory
|
|
155
|
+
// before the start rejects.
|
|
156
|
+
let current = await boot().catch(async (error: unknown) => {
|
|
157
|
+
await dispose();
|
|
158
|
+
|
|
159
|
+
throw error;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
...paths,
|
|
164
|
+
build: BUILD,
|
|
165
|
+
logs,
|
|
166
|
+
events,
|
|
167
|
+
get daemon(): DaemonHandle {
|
|
168
|
+
return current.daemon;
|
|
169
|
+
},
|
|
170
|
+
get client(): DaemonClient {
|
|
171
|
+
return current.client;
|
|
172
|
+
},
|
|
173
|
+
openClient,
|
|
174
|
+
async openTCPClient(): Promise<DaemonClient> {
|
|
175
|
+
const port = current.daemon.listenPort;
|
|
176
|
+
|
|
177
|
+
if (port === null) {
|
|
178
|
+
throw new Error('the test daemon started without a TCP listener');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const client = await DaemonClient.open({ hostname: '127.0.0.1', port });
|
|
182
|
+
|
|
183
|
+
clients.add(client);
|
|
184
|
+
|
|
185
|
+
return client;
|
|
186
|
+
},
|
|
187
|
+
async sendHookLines(...lines: readonly Readonly<Record<string, unknown>>[]): Promise<void> {
|
|
188
|
+
const payload = Buffer.from(lines.map((line) => `${JSON.stringify(line)}\n`).join(''));
|
|
189
|
+
const closed = Promise.withResolvers<void>();
|
|
190
|
+
let sent = 0;
|
|
191
|
+
|
|
192
|
+
// A socket write accepts only what fits its buffer, so the rest goes
|
|
193
|
+
// out as the socket drains, and the connection ends once all of it
|
|
194
|
+
// has gone.
|
|
195
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- a socket is a live handle
|
|
196
|
+
const sendRemainder = (socket: Socket) => {
|
|
197
|
+
sent += socket.write(payload.subarray(sent));
|
|
198
|
+
|
|
199
|
+
if (sent === payload.length) {
|
|
200
|
+
socket.end();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
await Bun.connect({
|
|
205
|
+
unix: paths.reporterSocketPath,
|
|
206
|
+
socket: {
|
|
207
|
+
open: sendRemainder,
|
|
208
|
+
drain: sendRemainder,
|
|
209
|
+
close() {
|
|
210
|
+
closed.resolve();
|
|
211
|
+
},
|
|
212
|
+
data() {},
|
|
213
|
+
error() {},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await closed.promise;
|
|
218
|
+
},
|
|
219
|
+
stop,
|
|
220
|
+
async restart(options: TestDaemonOptionsBuilder = buildOptions): Promise<void> {
|
|
221
|
+
await stop();
|
|
222
|
+
|
|
223
|
+
buildOptions = options;
|
|
224
|
+
|
|
225
|
+
current = await boot();
|
|
226
|
+
},
|
|
227
|
+
[Symbol.asyncDispose]: dispose,
|
|
228
|
+
};
|
|
229
|
+
}
|