@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
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
import { ImpPortError } from '../daemon/imp-port-error';
|
|
30
30
|
import { isImpNameAllowed } from '../daemon/is-imp-name-allowed';
|
|
31
31
|
import { isBrokerVariable } from '../shared/is-broker-variable';
|
|
32
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
32
33
|
|
|
33
34
|
// impd keeps exactly this many bytes of each generation's output.
|
|
34
35
|
const RING_BYTES = 262_144;
|
|
@@ -36,45 +37,33 @@ const RING_BYTES = 262_144;
|
|
|
36
37
|
// The mode bytes a fresh attach sends ahead of a ring that has wrapped.
|
|
37
38
|
const PRELUDE = '\u001B[0m';
|
|
38
39
|
|
|
39
|
-
// The PATH every
|
|
40
|
+
// The PATH every stub process runs with: a guest's own, never the
|
|
40
41
|
// daemon's.
|
|
41
42
|
const GUEST_PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
|
42
43
|
|
|
43
|
-
/**
|
|
44
|
-
* An in-process stand-in for impd behind the imp port. Each imp is a set of
|
|
45
|
-
* real `bun-pty` processes on this machine; a sleeping imp stops them with
|
|
46
|
-
* SIGSTOP and a wake continues them, so a memory wake keeps each process
|
|
47
|
-
* and its generation. Every session keeps an exact 262144-byte ring with
|
|
48
|
-
* offsets, a fresh attach skips to the next line and sends a mode prelude,
|
|
49
|
-
* and impd's refusals carry its codes and data: `LEASED`, `LEASE_NOT_HELD`,
|
|
50
|
-
* `NO_SESSION`, `INVALID_STATE`, `INVALID_RESUME`, `NOT_FOUND`, `CONFLICT`,
|
|
51
|
-
* and `FORBIDDEN`. Leases belong to principals; the port acts as
|
|
52
|
-
* `principal`, and a test adds other owners' leases, cold boots, and
|
|
53
|
-
* dropped sockets through the controls. Grants follow impd 0.27: the
|
|
54
|
-
* caller's identity must reach the imp and, under imp patterns, list the
|
|
55
|
-
* secret as grantable; a grant is idempotent, one secret per host, and a
|
|
56
|
-
* destroyed imp or a rebound or removed secret takes its grants with it. A
|
|
57
|
-
* start or an attach that requires the broker is refused with
|
|
58
|
-
* `PRECONDITION_FAILED` and reason `broker_not_ready`, and runs nothing,
|
|
59
|
-
* while the broker fails or the imp holds no grant, when the start sets a
|
|
60
|
-
* broker variable, and when it would join a process that started without
|
|
61
|
-
* the broker required.
|
|
62
|
-
*/
|
|
63
44
|
// A hold on the commands whose argv holds its text: entered resolves with
|
|
64
45
|
// the argv of the first command it holds, and stop lets every command it
|
|
65
|
-
// holds run.
|
|
66
|
-
|
|
46
|
+
// holds run. Disposing the hold stops it, so a test that holds it with
|
|
47
|
+
// `using` after its daemon lets the daemon stop even when the test fails.
|
|
48
|
+
interface StubCommandHold {
|
|
67
49
|
readonly entered: Promise<string>;
|
|
68
50
|
readonly stop: () => void;
|
|
51
|
+
readonly [Symbol.dispose]: () => void;
|
|
69
52
|
}
|
|
70
53
|
|
|
71
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The impd stand-in the factory below creates, one per call.
|
|
56
|
+
*/
|
|
57
|
+
class StubImpPort implements ImpPort {
|
|
72
58
|
// Every port call, in order, as `<call> <imp> [<detail>]`.
|
|
73
59
|
readonly calls: string[] = [];
|
|
74
60
|
|
|
75
61
|
// Every session request the port received, in order.
|
|
76
62
|
readonly sessionRequests: ImpSessionRequest[] = [];
|
|
77
63
|
|
|
64
|
+
// The spec of every imp create the port received, in order.
|
|
65
|
+
readonly createSpecs: ImpCreateSpec[] = [];
|
|
66
|
+
|
|
78
67
|
features: ImpFeatures = {
|
|
79
68
|
sessionOffsets: true,
|
|
80
69
|
leases: true,
|
|
@@ -150,6 +139,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
150
139
|
// The lease acquisitions wait for this hold to end, while one is held.
|
|
151
140
|
private leaseHold: PromiseWithResolvers<void> | null = null;
|
|
152
141
|
|
|
142
|
+
// How many lease acquisitions and releases wait on their holds.
|
|
143
|
+
private heldLeases = 0;
|
|
144
|
+
|
|
145
|
+
private heldReleases = 0;
|
|
146
|
+
|
|
153
147
|
// The directory a relative or missing working directory resolves
|
|
154
148
|
// against inside every imp, as a guest's home does; null for the test's
|
|
155
149
|
// own working directory.
|
|
@@ -163,6 +157,9 @@ export class FixtureImpPort implements ImpPort {
|
|
|
163
157
|
readonly done: PromiseWithResolvers<void>;
|
|
164
158
|
} | null = null;
|
|
165
159
|
|
|
160
|
+
// How many commands wait on a command hold.
|
|
161
|
+
private heldCommands = 0;
|
|
162
|
+
|
|
166
163
|
// Commands whose argv holds this text exit 1 without running, or null.
|
|
167
164
|
private commandFailure: string | null = null;
|
|
168
165
|
|
|
@@ -183,9 +180,12 @@ export class FixtureImpPort implements ImpPort {
|
|
|
183
180
|
// what the daemon writes still reaches the guest.
|
|
184
181
|
private droppingGuestBytes = false;
|
|
185
182
|
|
|
183
|
+
// How many guest bytes the drop has thrown away.
|
|
184
|
+
private droppedGuestBytes = 0;
|
|
185
|
+
|
|
186
186
|
private readonly principal: string;
|
|
187
187
|
|
|
188
|
-
private readonly imps = new Map<string,
|
|
188
|
+
private readonly imps = new Map<string, StubImp>();
|
|
189
189
|
|
|
190
190
|
private readonly forwards = new Set<{ stop: () => void; stopRelays: () => void }>();
|
|
191
191
|
|
|
@@ -301,6 +301,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
301
301
|
|
|
302
302
|
createImp(spec: ImpCreateSpec): Promise<ImpView> {
|
|
303
303
|
this.calls.push(`imps.create ${spec.name}`);
|
|
304
|
+
this.createSpecs.push(spec);
|
|
304
305
|
|
|
305
306
|
if (this.imps.has(spec.name)) {
|
|
306
307
|
return Promise.reject(
|
|
@@ -308,7 +309,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
308
309
|
);
|
|
309
310
|
}
|
|
310
311
|
|
|
311
|
-
const imp:
|
|
312
|
+
const imp: StubImp = {
|
|
312
313
|
id: randomUUID(),
|
|
313
314
|
name: spec.name,
|
|
314
315
|
state: 'stopped',
|
|
@@ -335,8 +336,12 @@ export class FixtureImpPort implements ImpPort {
|
|
|
335
336
|
}
|
|
336
337
|
|
|
337
338
|
private async waitForLease(name: string, label: string, ttlSeconds: number): Promise<ImpLease> {
|
|
339
|
+
this.heldLeases += 1;
|
|
340
|
+
|
|
338
341
|
await this.leaseHold?.promise;
|
|
339
342
|
|
|
343
|
+
this.heldLeases -= 1;
|
|
344
|
+
|
|
340
345
|
return this.applyLease(name, label, ttlSeconds);
|
|
341
346
|
}
|
|
342
347
|
|
|
@@ -401,8 +406,12 @@ export class FixtureImpPort implements ImpPort {
|
|
|
401
406
|
}
|
|
402
407
|
|
|
403
408
|
private async waitForRelease(name: string, label: string): Promise<boolean> {
|
|
409
|
+
this.heldReleases += 1;
|
|
410
|
+
|
|
404
411
|
await this.releaseHold?.promise;
|
|
405
412
|
|
|
413
|
+
this.heldReleases -= 1;
|
|
414
|
+
|
|
406
415
|
return this.applyRelease(name, label);
|
|
407
416
|
}
|
|
408
417
|
|
|
@@ -486,7 +495,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
486
495
|
|
|
487
496
|
const outcome = Promise.withResolvers<ImpSessionOutcome>();
|
|
488
497
|
|
|
489
|
-
const connection:
|
|
498
|
+
const connection: StubConnection = {
|
|
490
499
|
handlers,
|
|
491
500
|
sent: 0,
|
|
492
501
|
finished: false,
|
|
@@ -590,7 +599,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
590
599
|
if (hold !== null && line.includes(hold.match)) {
|
|
591
600
|
hold.entered.resolve(line);
|
|
592
601
|
|
|
602
|
+
this.heldCommands += 1;
|
|
603
|
+
|
|
593
604
|
await hold.done.promise;
|
|
605
|
+
|
|
606
|
+
this.heldCommands -= 1;
|
|
594
607
|
}
|
|
595
608
|
|
|
596
609
|
if (this.commandFailure !== null && line.includes(this.commandFailure)) {
|
|
@@ -625,11 +638,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
625
638
|
): ImpReverseForward {
|
|
626
639
|
this.calls.push(`reverse ${name} ${guestPath}`);
|
|
627
640
|
|
|
628
|
-
const server = Bun.listen<
|
|
641
|
+
const server = Bun.listen<StubRelay>({
|
|
629
642
|
unix: guestPath,
|
|
630
643
|
socket: {
|
|
631
644
|
open: (socket) => {
|
|
632
|
-
const relay:
|
|
645
|
+
const relay: StubRelay = {
|
|
633
646
|
dataListeners: [],
|
|
634
647
|
closeListeners: [],
|
|
635
648
|
unsent: new Uint8Array(0),
|
|
@@ -675,6 +688,8 @@ export class FixtureImpPort implements ImpPort {
|
|
|
675
688
|
},
|
|
676
689
|
data: (socket, buf) => {
|
|
677
690
|
if (this.droppingGuestBytes) {
|
|
691
|
+
this.droppedGuestBytes += buf.length;
|
|
692
|
+
|
|
678
693
|
return;
|
|
679
694
|
}
|
|
680
695
|
|
|
@@ -884,6 +899,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
884
899
|
this.droppingGuestBytes = false;
|
|
885
900
|
}
|
|
886
901
|
|
|
902
|
+
// How many bytes guests wrote that the drop threw away.
|
|
903
|
+
countDroppedGuestBytes(): number {
|
|
904
|
+
return this.droppedGuestBytes;
|
|
905
|
+
}
|
|
906
|
+
|
|
887
907
|
/**
|
|
888
908
|
* Holds every session answer back, as a slow network does, until the
|
|
889
909
|
* hold stops.
|
|
@@ -892,6 +912,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
892
912
|
this.held ??= [];
|
|
893
913
|
}
|
|
894
914
|
|
|
915
|
+
// How many session answers wait for the answer hold to stop.
|
|
916
|
+
countHeldAnswers(): number {
|
|
917
|
+
return this.held?.length ?? 0;
|
|
918
|
+
}
|
|
919
|
+
|
|
895
920
|
/**
|
|
896
921
|
* Sends every held answer, in order, and answers at once again.
|
|
897
922
|
*/
|
|
@@ -970,6 +995,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
970
995
|
this.leaseHold ??= Promise.withResolvers<void>();
|
|
971
996
|
}
|
|
972
997
|
|
|
998
|
+
// How many lease acquisitions wait for the lease hold to stop.
|
|
999
|
+
countHeldLeases(): number {
|
|
1000
|
+
return this.heldLeases;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
973
1003
|
/**
|
|
974
1004
|
* Lets every held lease acquisition go through, and the next at once.
|
|
975
1005
|
*/
|
|
@@ -985,6 +1015,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
985
1015
|
this.releaseHold ??= Promise.withResolvers<void>();
|
|
986
1016
|
}
|
|
987
1017
|
|
|
1018
|
+
// How many lease releases wait for the release hold to stop.
|
|
1019
|
+
countHeldReleases(): number {
|
|
1020
|
+
return this.heldReleases;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
988
1023
|
/**
|
|
989
1024
|
* Lets every held lease release go through, and the next at once.
|
|
990
1025
|
*/
|
|
@@ -1007,7 +1042,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1007
1042
|
* stops. Throws while another hold is active, so no hold replaces one a
|
|
1008
1043
|
* command still waits on.
|
|
1009
1044
|
*/
|
|
1010
|
-
startCommandHold(match: string):
|
|
1045
|
+
startCommandHold(match: string): StubCommandHold {
|
|
1011
1046
|
if (this.commandHold !== null) {
|
|
1012
1047
|
throw new Error(`a hold on ${this.commandHold.match} is still active`);
|
|
1013
1048
|
}
|
|
@@ -1020,16 +1055,20 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1020
1055
|
|
|
1021
1056
|
this.commandHold = hold;
|
|
1022
1057
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
this.commandHold = null;
|
|
1028
|
-
}
|
|
1058
|
+
const stop = () => {
|
|
1059
|
+
if (this.commandHold === hold) {
|
|
1060
|
+
this.commandHold = null;
|
|
1061
|
+
}
|
|
1029
1062
|
|
|
1030
|
-
|
|
1031
|
-
},
|
|
1063
|
+
hold.done.resolve();
|
|
1032
1064
|
};
|
|
1065
|
+
|
|
1066
|
+
return { entered: hold.entered.promise, stop, [Symbol.dispose]: stop };
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// How many commands wait for their command hold to stop.
|
|
1070
|
+
countHeldCommands(): number {
|
|
1071
|
+
return this.heldCommands;
|
|
1033
1072
|
}
|
|
1034
1073
|
|
|
1035
1074
|
// Lets every command the active hold holds run, and the next at once.
|
|
@@ -1083,7 +1122,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1083
1122
|
return this.imps.get(name)?.state ?? null;
|
|
1084
1123
|
}
|
|
1085
1124
|
|
|
1086
|
-
// The names of the imps the
|
|
1125
|
+
// The names of the imps the stub holds.
|
|
1087
1126
|
collectImpNames(): string[] {
|
|
1088
1127
|
return [...this.imps.keys()];
|
|
1089
1128
|
}
|
|
@@ -1104,6 +1143,15 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1104
1143
|
return proc.generation;
|
|
1105
1144
|
}
|
|
1106
1145
|
|
|
1146
|
+
/**
|
|
1147
|
+
* The exit a session's process left that no connection has received, or
|
|
1148
|
+
* null while the process runs, once an attach delivered the exit, and for
|
|
1149
|
+
* a session the imp does not hold.
|
|
1150
|
+
*/
|
|
1151
|
+
findUndeliveredExit(name: string, session: string): { readonly code: number | null } | null {
|
|
1152
|
+
return this.imps.get(name)?.sessions.get(session)?.exited ?? null;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1107
1155
|
// The offset after the last byte a session's running generation wrote.
|
|
1108
1156
|
getEnd(name: string, session: string): number {
|
|
1109
1157
|
const proc = this.getImp(name).sessions.get(session);
|
|
@@ -1174,7 +1222,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1174
1222
|
}
|
|
1175
1223
|
}
|
|
1176
1224
|
|
|
1177
|
-
private getImp(name: string):
|
|
1225
|
+
private getImp(name: string): StubImp {
|
|
1178
1226
|
const imp = this.imps.get(name);
|
|
1179
1227
|
|
|
1180
1228
|
if (imp === undefined) {
|
|
@@ -1184,7 +1232,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1184
1232
|
return imp;
|
|
1185
1233
|
}
|
|
1186
1234
|
|
|
1187
|
-
private buildView(imp:
|
|
1235
|
+
private buildView(imp: StubImp): ImpView {
|
|
1188
1236
|
const live = [...imp.leases.values()].filter((lease) => lease.until > Date.now());
|
|
1189
1237
|
const own = live.filter((lease) => lease.principal === this.principal);
|
|
1190
1238
|
const others = live.filter((lease) => lease.principal !== this.principal);
|
|
@@ -1200,7 +1248,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1200
1248
|
|
|
1201
1249
|
// A sleeping imp wakes from memory with every process as it was; a
|
|
1202
1250
|
// stopped one boots cold.
|
|
1203
|
-
private updateAwake(imp:
|
|
1251
|
+
private updateAwake(imp: StubImp): void {
|
|
1204
1252
|
if (imp.state === 'sleeping') {
|
|
1205
1253
|
imp.state = 'running';
|
|
1206
1254
|
|
|
@@ -1212,7 +1260,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1212
1260
|
}
|
|
1213
1261
|
}
|
|
1214
1262
|
|
|
1215
|
-
private updateAsleep(imp:
|
|
1263
|
+
private updateAsleep(imp: StubImp): void {
|
|
1216
1264
|
if (imp.state !== 'running') {
|
|
1217
1265
|
return;
|
|
1218
1266
|
}
|
|
@@ -1225,7 +1273,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1225
1273
|
}
|
|
1226
1274
|
}
|
|
1227
1275
|
|
|
1228
|
-
private bootCold(imp:
|
|
1276
|
+
private bootCold(imp: StubImp, cause: ColdBootCause): void {
|
|
1229
1277
|
for (const proc of imp.sessions.values()) {
|
|
1230
1278
|
proc.connection?.finish({ kind: 'detached', reason: 'lost', offset: proc.end });
|
|
1231
1279
|
proc.ended = true;
|
|
@@ -1246,7 +1294,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1246
1294
|
].slice(0, 4);
|
|
1247
1295
|
}
|
|
1248
1296
|
|
|
1249
|
-
private answerSession(request: ImpSessionRequest, connection:
|
|
1297
|
+
private answerSession(request: ImpSessionRequest, connection: StubConnection): void {
|
|
1250
1298
|
if (connection.finished) {
|
|
1251
1299
|
return;
|
|
1252
1300
|
}
|
|
@@ -1353,9 +1401,9 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1353
1401
|
// start sets, or a running process that started without the broker
|
|
1354
1402
|
// required, fails the requirement as impd's would.
|
|
1355
1403
|
private findBrokerProblem(
|
|
1356
|
-
imp:
|
|
1404
|
+
imp: StubImp,
|
|
1357
1405
|
request: ImpSessionRequest,
|
|
1358
|
-
running:
|
|
1406
|
+
running: StubProcess | undefined,
|
|
1359
1407
|
): string | null {
|
|
1360
1408
|
if (request.require?.includes('broker') !== true) {
|
|
1361
1409
|
return null;
|
|
@@ -1393,7 +1441,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1393
1441
|
return posix.resolve(this.homeDir, cwd ?? '.');
|
|
1394
1442
|
}
|
|
1395
1443
|
|
|
1396
|
-
private startProcess(imp:
|
|
1444
|
+
private startProcess(imp: StubImp, request: ImpSessionRequest): StubProcess | null {
|
|
1397
1445
|
if (request.kind !== 'start') {
|
|
1398
1446
|
return null;
|
|
1399
1447
|
}
|
|
@@ -1406,7 +1454,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1406
1454
|
env: { ...request.env },
|
|
1407
1455
|
});
|
|
1408
1456
|
|
|
1409
|
-
const proc:
|
|
1457
|
+
const proc: StubProcess = {
|
|
1410
1458
|
pty,
|
|
1411
1459
|
generation: randomBytes(16).toString('hex'),
|
|
1412
1460
|
ring: new Uint8Array(0),
|
|
@@ -1465,10 +1513,10 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1465
1513
|
}
|
|
1466
1514
|
|
|
1467
1515
|
private attachProcess(
|
|
1468
|
-
imp:
|
|
1469
|
-
proc:
|
|
1516
|
+
imp: StubImp,
|
|
1517
|
+
proc: StubProcess,
|
|
1470
1518
|
request: ImpSessionRequest,
|
|
1471
|
-
connection:
|
|
1519
|
+
connection: StubConnection,
|
|
1472
1520
|
created: boolean,
|
|
1473
1521
|
): void {
|
|
1474
1522
|
const bufferStart = proc.end - proc.ring.length;
|
|
@@ -1572,27 +1620,60 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1572
1620
|
}
|
|
1573
1621
|
}
|
|
1574
1622
|
|
|
1575
|
-
|
|
1623
|
+
/**
|
|
1624
|
+
* Creates an in-process stand-in for impd behind the imp port, calling impd
|
|
1625
|
+
* as the principal, `token:atc` when absent. Each imp is a set of
|
|
1626
|
+
* real `bun-pty` processes on this machine; a sleeping imp stops them with
|
|
1627
|
+
* SIGSTOP and a wake continues them, so a memory wake keeps each process
|
|
1628
|
+
* and its generation. Every session keeps an exact 262144-byte ring with
|
|
1629
|
+
* offsets, a fresh attach skips to the next line and sends a mode prelude,
|
|
1630
|
+
* and impd's refusals carry its codes and data: `LEASED`, `LEASE_NOT_HELD`,
|
|
1631
|
+
* `NO_SESSION`, `INVALID_STATE`, `INVALID_RESUME`, `NOT_FOUND`, `CONFLICT`,
|
|
1632
|
+
* and `FORBIDDEN`. Leases belong to principals; the port acts as
|
|
1633
|
+
* `principal`, and a test adds other owners' leases, cold boots, and
|
|
1634
|
+
* dropped sockets through the controls. Grants follow impd 0.27: the
|
|
1635
|
+
* caller's identity must reach the imp and, under imp patterns, list the
|
|
1636
|
+
* secret as grantable; a grant is idempotent, one secret per host, and a
|
|
1637
|
+
* destroyed imp or a rebound or removed secret takes its grants with it. A
|
|
1638
|
+
* start or an attach that requires the broker is refused with
|
|
1639
|
+
* `PRECONDITION_FAILED` and reason `broker_not_ready`, and runs nothing,
|
|
1640
|
+
* while the broker fails or the imp holds no grant, when the start sets a
|
|
1641
|
+
* broker variable, and when it would join a process that started without
|
|
1642
|
+
* the broker required. Once the current test finishes, the stand-in kills
|
|
1643
|
+
* every process and stops every forward it holds, so it must be created inside
|
|
1644
|
+
* a test; disposal does so sooner, and a second disposal does nothing.
|
|
1645
|
+
*/
|
|
1646
|
+
export function createStubImpPort(principal = 'token:atc'): StubImpPort {
|
|
1647
|
+
const port = new StubImpPort(principal);
|
|
1648
|
+
|
|
1649
|
+
registerTestCleanup(() => {
|
|
1650
|
+
port[Symbol.dispose]();
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
return port;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
interface StubLease {
|
|
1576
1657
|
readonly principal: string;
|
|
1577
1658
|
readonly label: string;
|
|
1578
1659
|
readonly until: number;
|
|
1579
1660
|
}
|
|
1580
1661
|
|
|
1581
|
-
interface
|
|
1662
|
+
interface StubImp {
|
|
1582
1663
|
readonly id: string;
|
|
1583
1664
|
readonly name: string;
|
|
1584
1665
|
state: ImpState;
|
|
1585
1666
|
bootId: string;
|
|
1586
1667
|
coldBoots: ColdBoot[];
|
|
1587
|
-
readonly leases: Map<string,
|
|
1588
|
-
readonly sessions: Map<string,
|
|
1668
|
+
readonly leases: Map<string, StubLease>;
|
|
1669
|
+
readonly sessions: Map<string, StubProcess>;
|
|
1589
1670
|
readonly previous: Map<string, PreviousGeneration>;
|
|
1590
1671
|
|
|
1591
1672
|
// The secrets granted to the imp.
|
|
1592
1673
|
readonly grants: Set<string>;
|
|
1593
1674
|
}
|
|
1594
1675
|
|
|
1595
|
-
interface
|
|
1676
|
+
interface StubProcess {
|
|
1596
1677
|
readonly pty: IPty;
|
|
1597
1678
|
readonly generation: string;
|
|
1598
1679
|
ring: Uint8Array;
|
|
@@ -1601,22 +1682,22 @@ interface FixtureProcess {
|
|
|
1601
1682
|
|
|
1602
1683
|
// Ended by a cold boot: its exit is never delivered.
|
|
1603
1684
|
ended: boolean;
|
|
1604
|
-
connection:
|
|
1685
|
+
connection: StubConnection | null;
|
|
1605
1686
|
|
|
1606
1687
|
// Whether its start required the broker, which an attach that requires
|
|
1607
1688
|
// it needs.
|
|
1608
1689
|
readonly requireBroker: boolean;
|
|
1609
1690
|
}
|
|
1610
1691
|
|
|
1611
|
-
interface
|
|
1692
|
+
interface StubConnection {
|
|
1612
1693
|
readonly handlers: ImpSessionHandlers;
|
|
1613
1694
|
sent: number;
|
|
1614
1695
|
finished: boolean;
|
|
1615
|
-
process:
|
|
1696
|
+
process: StubProcess | null;
|
|
1616
1697
|
readonly finish: (outcome: ImpSessionOutcome) => void;
|
|
1617
1698
|
}
|
|
1618
1699
|
|
|
1619
|
-
interface
|
|
1700
|
+
interface StubRelay {
|
|
1620
1701
|
// oxlint-disable-next-line prefer-readonly-parameter-types -- relayed bytes have no readonly form
|
|
1621
1702
|
readonly dataListeners: ((data: Uint8Array) => void)[];
|
|
1622
1703
|
readonly closeListeners: (() => void)[];
|
|
@@ -1647,7 +1728,7 @@ function mergeTail(ring: Uint8Array, data: Uint8Array, limit: number): Uint8Arra
|
|
|
1647
1728
|
|
|
1648
1729
|
// Calls a connection's handler as the imp client does: a handler that
|
|
1649
1730
|
// throws ends the connection with a local error and closes it.
|
|
1650
|
-
function tryEmit(connection:
|
|
1731
|
+
function tryEmit(connection: StubConnection, emit: () => void): boolean {
|
|
1651
1732
|
try {
|
|
1652
1733
|
emit();
|
|
1653
1734
|
|
|
@@ -1662,7 +1743,7 @@ function tryEmit(connection: FixtureConnection, emit: () => void): boolean {
|
|
|
1662
1743
|
}
|
|
1663
1744
|
}
|
|
1664
1745
|
|
|
1665
|
-
function findSessionName(imp:
|
|
1746
|
+
function findSessionName(imp: StubImp, proc: StubProcess): string {
|
|
1666
1747
|
for (const [name, held] of imp.sessions) {
|
|
1667
1748
|
if (held === proc) {
|
|
1668
1749
|
return name;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { createStubBin } from './create-stub-bin';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a stand-in for the Claude CLI as `fake-claude` under the
|
|
6
|
+
* directory and returns its path. Each start appends its arguments to
|
|
7
|
+
* `claude-starts.log` under the same directory, one per line, ends the
|
|
8
|
+
* start with an empty line, and then stays up without reporting anything,
|
|
9
|
+
* as a session whose agent never speaks does. The log exists only once a
|
|
10
|
+
* start has run, so a test proves that no harness started by its absence.
|
|
11
|
+
*/
|
|
12
|
+
export function createStubRecordingClaude(dir: string): string {
|
|
13
|
+
const log = join(dir, 'claude-starts.log');
|
|
14
|
+
|
|
15
|
+
return createStubBin(
|
|
16
|
+
dir,
|
|
17
|
+
'fake-claude',
|
|
18
|
+
`#!/bin/sh\n{ printf '%s\\n' "$@"; echo; } >> '${log}'\nexec sleep 30\n`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { createStubBin } from './create-stub-bin';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a stand-in for the Codex CLI as `fake-codex` under the directory
|
|
6
|
+
* and returns its path. Each start appends one line to `codex-starts.log`
|
|
7
|
+
* under the same directory: the `CODEX_HOME` it started with, then its
|
|
8
|
+
* arguments, separated by spaces. It then stays up without reporting
|
|
9
|
+
* anything. The log exists only once a start has run, so a test proves that
|
|
10
|
+
* no harness started by its absence.
|
|
11
|
+
*/
|
|
12
|
+
export function createStubRecordingCodex(dir: string): string {
|
|
13
|
+
const log = join(dir, 'codex-starts.log');
|
|
14
|
+
|
|
15
|
+
return createStubBin(
|
|
16
|
+
dir,
|
|
17
|
+
'fake-codex',
|
|
18
|
+
`#!/bin/sh\necho "$CODEX_HOME $*" >> '${log}'\nexec sleep 30\n`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { createStubBin } from './create-stub-bin';
|
|
3
|
+
|
|
4
|
+
interface StubSharedServerCodexConfig {
|
|
5
|
+
// The command atc runs as; the stub reports each hook through its
|
|
6
|
+
// `hook-report --agent codex`, as the hook entries atc prints for Codex do.
|
|
7
|
+
readonly atc: readonly string[];
|
|
8
|
+
|
|
9
|
+
// The composer script the stub finishes in.
|
|
10
|
+
readonly composer: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a stand-in for a Codex CLI that shares one background server
|
|
15
|
+
* between its terminals, as `fake-codex` under the directory, and returns
|
|
16
|
+
* its path. Without `--no-daemon`, the first start records its own
|
|
17
|
+
* `ATC_SESSION_ID` and `ATC_SOCKET` in `codex-shared-server.env` under the
|
|
18
|
+
* directory, as the server it starts keeps its environment, and every start
|
|
19
|
+
* runs its hooks with the recorded pair, as Codex runs hooks inside that
|
|
20
|
+
* server. With `--no-daemon`, a start runs its hooks with its own pair. The
|
|
21
|
+
* stub prints its arguments, reports a `SessionStart` for its thread, which
|
|
22
|
+
* is `fake-thread-<its own ATC_SESSION_ID>` or the id after `resume`, then
|
|
23
|
+
* a `Stop` whose last message is `done <thread>`, then runs the composer.
|
|
24
|
+
*/
|
|
25
|
+
export function createStubSharedServerCodex(
|
|
26
|
+
dir: string,
|
|
27
|
+
config: StubSharedServerCodexConfig,
|
|
28
|
+
): string {
|
|
29
|
+
const report = `${config.atc.map((part) => `"${part}"`).join(' ')} hook-report --agent codex`;
|
|
30
|
+
const server = join(dir, 'codex-shared-server.env');
|
|
31
|
+
|
|
32
|
+
return createStubBin(
|
|
33
|
+
dir,
|
|
34
|
+
'fake-codex',
|
|
35
|
+
`#!/usr/bin/env bash
|
|
36
|
+
echo "FAKE_CODEX_UP args: $*"
|
|
37
|
+
thread="fake-thread-$ATC_SESSION_ID"
|
|
38
|
+
source=startup
|
|
39
|
+
prev=''
|
|
40
|
+
for arg in "$@"; do
|
|
41
|
+
if [ "$prev" = resume ]; then thread="$arg"; source=resume; fi
|
|
42
|
+
prev="$arg"
|
|
43
|
+
done
|
|
44
|
+
case " $* " in
|
|
45
|
+
*' --no-daemon '*) ;;
|
|
46
|
+
*)
|
|
47
|
+
(set -o noclobber; printf 'ATC_SESSION_ID=%q\\nATC_SOCKET=%q\\n' "$ATC_SESSION_ID" "$ATC_SOCKET" > '${server}') 2>/dev/null
|
|
48
|
+
. '${server}'
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
printf '{"hook_event_name":"SessionStart","session_id":"%s","source":"%s"}' "$thread" "$source" | ${report}
|
|
52
|
+
printf '{"hook_event_name":"Stop","session_id":"%s","last_assistant_message":"done %s"}' "$thread" "$thread" | ${report}
|
|
53
|
+
exec "${process.execPath}" "${config.composer}"
|
|
54
|
+
`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
4
5
|
|
|
5
|
-
interface
|
|
6
|
-
// The directory that holds the
|
|
6
|
+
interface StubSystemd {
|
|
7
|
+
// The directory that holds the stand-in `systemctl` and `systemd-run`, to put
|
|
7
8
|
// first on the PATH of every process that could reach a real one.
|
|
8
9
|
readonly binDir: string;
|
|
9
10
|
|
|
@@ -27,17 +28,24 @@ interface FakeSystemd {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
* Writes a
|
|
31
|
+
* Writes a stand-in `systemctl` and `systemd-run` that touch no service
|
|
31
32
|
* manager. Without a MainPID, `systemctl show -p MainPID` prints a pid no
|
|
32
33
|
* process holds, so no daemon is ever a unit's main process. With
|
|
33
34
|
* `writeMainPID`, `systemctl restart` stops that pid with SIGTERM and starts
|
|
34
35
|
* `atcCommand daemon` in the background. `systemd-run` runs the command after
|
|
35
36
|
* `--` in the background with only the `--setenv` variables, as a transient
|
|
36
37
|
* unit sees them, and appends its output to the `StandardOutput=append:`
|
|
37
|
-
* file. Every call is logged.
|
|
38
|
+
* file. Every call is logged. The stand-ins' directory is removed once the
|
|
39
|
+
* current test finishes, so it must run inside a test; disposal removes it
|
|
40
|
+
* sooner, and a second removal does nothing.
|
|
38
41
|
*/
|
|
39
|
-
export function
|
|
40
|
-
const root = mkdtempSync(join(tmpdir(), 'atc-
|
|
42
|
+
export function createStubSystemd(atcCommand: readonly string[]): StubSystemd {
|
|
43
|
+
const root = mkdtempSync(join(tmpdir(), 'atc-stub-systemd-'));
|
|
44
|
+
|
|
45
|
+
const remove = registerTestCleanup(() => {
|
|
46
|
+
rmSync(root, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
41
49
|
const binDir = join(root, 'bin');
|
|
42
50
|
const procRoot = join(root, 'proc');
|
|
43
51
|
const atcLine = atcCommand.map((part) => `"${part}"`).join(' ');
|
|
@@ -118,8 +126,6 @@ exit 0
|
|
|
118
126
|
`0::/user.slice/user-${process.getuid?.() ?? 0}.slice/user@${process.getuid?.() ?? 0}.service/app.slice/${unit}\n`,
|
|
119
127
|
);
|
|
120
128
|
},
|
|
121
|
-
[Symbol.dispose]:
|
|
122
|
-
rmSync(root, { recursive: true, force: true });
|
|
123
|
-
},
|
|
129
|
+
[Symbol.dispose]: remove,
|
|
124
130
|
};
|
|
125
131
|
}
|
|
@@ -4,11 +4,11 @@ import { getAgentEntry } from './get-agent-entry';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The gateway a parsed config holds under an agent id: a Claude entry with a
|
|
7
|
-
* base URL
|
|
8
|
-
* base URL.
|
|
7
|
+
* base URL, without the registry's `kind`, which a gateway does not carry.
|
|
8
|
+
* Throws when the config holds no such agent or the entry has no base URL.
|
|
9
9
|
*/
|
|
10
10
|
export function getGatewayConfig(config: Pick<Config, 'agents'>, id: string): GatewayConfig {
|
|
11
|
-
const entry = getAgentEntry(config, id);
|
|
11
|
+
const { kind: _kind, ...entry } = getAgentEntry(config, id);
|
|
12
12
|
|
|
13
13
|
if (entry.baseURL === undefined) {
|
|
14
14
|
throw new Error(`the agent '${id}' has no baseURL`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the effect ref of the one idempotency claim a state database
|
|
5
|
+
* holds, and throws when it holds none or several, so a test that expects
|
|
6
|
+
* one claim never reads another's.
|
|
7
|
+
*/
|
|
8
|
+
export function getOnlyEffectRef(db: Database): string {
|
|
9
|
+
const refs = db
|
|
10
|
+
.query<{ effect_ref: string }, []>('SELECT effect_ref FROM idempotency')
|
|
11
|
+
.all()
|
|
12
|
+
.map((row) => row.effect_ref);
|
|
13
|
+
|
|
14
|
+
const [ref] = refs;
|
|
15
|
+
|
|
16
|
+
if (refs.length !== 1 || ref === undefined) {
|
|
17
|
+
throw new Error(`expected one idempotency claim, found ${JSON.stringify(refs)}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return ref;
|
|
21
|
+
}
|