@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
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import type { AgentAdapter } from '../agents/agent-adapter';
|
|
4
3
|
import { DaemonClient } from '../client/daemon-client';
|
|
5
4
|
import { startDaemon } from '../daemon/daemon';
|
|
6
5
|
import type { DaemonHandle } from '../daemon/daemon';
|
|
7
6
|
import { openMCPAuth } from '../mcp/open-mcp-auth';
|
|
8
7
|
import { ReconnectingCaller } from '../mcp/reconnecting-caller';
|
|
9
8
|
import { startMCPHTTPServer } from '../mcp/start-mcp-http-server';
|
|
9
|
+
import { buildMockAgentAdapter } from './build-mock-agent-adapter';
|
|
10
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
10
11
|
import { setupTempDir } from './setup-temp-dir';
|
|
11
12
|
|
|
12
|
-
const SLEEP_ADAPTER: AgentAdapter = {
|
|
13
|
-
id: 'claude',
|
|
14
|
-
headlessRunner: null,
|
|
15
|
-
screenDetector: null,
|
|
16
|
-
takesMessages: false,
|
|
17
|
-
planSpawn: () => ({ bin: 'sleep', args: ['30'] }),
|
|
18
|
-
normalizeHook: () => ({ kind: 'heartbeat' }),
|
|
19
|
-
loadName: () => Promise.resolve(null),
|
|
20
|
-
canResume: () => true,
|
|
21
|
-
buildResumeCommand: () => null,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
13
|
interface MCPHTTPSetupOptions {
|
|
25
|
-
// How long a rotated refresh token still answers with its successor.
|
|
26
|
-
readonly refreshReuseSeconds?: number;
|
|
27
|
-
|
|
28
14
|
// Further Host header values the server accepts.
|
|
29
15
|
readonly allowedHosts?: readonly string[];
|
|
30
16
|
}
|
|
@@ -38,11 +24,23 @@ interface MCPHTTPSetupOptions {
|
|
|
38
24
|
* `atc clients` and `atc grants` open it, and `addClient` adds a client
|
|
39
25
|
* through it and returns the client id. `restartDaemon` stops the daemon and
|
|
40
26
|
* starts a fresh one on the same socket and database, the way an operator
|
|
41
|
-
* restarts it, with the principals it is given; `countDaemonClients` reads
|
|
42
|
-
* daemon holds open.
|
|
27
|
+
* restarts it, with the principals it is given; `countDaemonClients` reads
|
|
28
|
+
* how many connections the current daemon holds open. Everything it starts
|
|
29
|
+
* stops, and the directory is removed, once the current test finishes, so it
|
|
30
|
+
* must run inside a test; disposal does so sooner, and a second disposal
|
|
31
|
+
* does nothing.
|
|
43
32
|
*/
|
|
44
33
|
export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
|
|
45
34
|
const tmp = setupTempDir('atc-mcp-http-');
|
|
35
|
+
|
|
36
|
+
// Registered after the directory, so it releases first: each part stops
|
|
37
|
+
// before the one it depends on, and the directory goes last.
|
|
38
|
+
const stack = new AsyncDisposableStack();
|
|
39
|
+
|
|
40
|
+
const dispose = registerTestCleanup(() => stack.disposeAsync());
|
|
41
|
+
|
|
42
|
+
stack.use(tmp);
|
|
43
|
+
|
|
46
44
|
const socketPath = join(tmp.dir, 'daemon.sock');
|
|
47
45
|
const stateDir = join(tmp.dir, '.local', 'state', 'atc');
|
|
48
46
|
|
|
@@ -57,7 +55,7 @@ export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
|
|
|
57
55
|
socketPath,
|
|
58
56
|
reporterSocketPath: join(tmp.dir, 'reporter.sock'),
|
|
59
57
|
build: 'atc/test-build',
|
|
60
|
-
adapter:
|
|
58
|
+
adapter: buildMockAgentAdapter(),
|
|
61
59
|
dbPath: join(tmp.dir, 'state.db'),
|
|
62
60
|
statusPath: join(tmp.dir, 'status.json'),
|
|
63
61
|
principals,
|
|
@@ -65,10 +63,14 @@ export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
|
|
|
65
63
|
|
|
66
64
|
let daemon: DaemonHandle = await startTestDaemon();
|
|
67
65
|
|
|
66
|
+
stack.defer(() => daemon.stop());
|
|
67
|
+
|
|
68
68
|
const caller = new ReconnectingCaller(socketPath, 'atc/test-build', (path) =>
|
|
69
69
|
DaemonClient.open(path),
|
|
70
70
|
);
|
|
71
71
|
|
|
72
|
+
stack.defer(() => caller.stop());
|
|
73
|
+
|
|
72
74
|
const server = await startMCPHTTPServer({
|
|
73
75
|
caller,
|
|
74
76
|
build: 'atc/test-build',
|
|
@@ -83,13 +85,14 @@ export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
|
|
|
83
85
|
printRequest: (line) => {
|
|
84
86
|
requests.push(line);
|
|
85
87
|
},
|
|
86
|
-
...(options.refreshReuseSeconds === undefined
|
|
87
|
-
? {}
|
|
88
|
-
: { refreshReuseSeconds: options.refreshReuseSeconds }),
|
|
89
88
|
});
|
|
90
89
|
|
|
90
|
+
stack.defer(() => server.stop());
|
|
91
|
+
|
|
91
92
|
const store = await openMCPAuth({ dbPath, origin: null });
|
|
92
93
|
|
|
94
|
+
stack.defer(() => store.close());
|
|
95
|
+
|
|
93
96
|
return {
|
|
94
97
|
home: tmp.dir,
|
|
95
98
|
url: server.url,
|
|
@@ -114,13 +117,6 @@ export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
|
|
|
114
117
|
|
|
115
118
|
daemon = await startTestDaemon(principals);
|
|
116
119
|
},
|
|
117
|
-
|
|
118
|
-
await server.stop();
|
|
119
|
-
await store.close();
|
|
120
|
-
await caller.stop();
|
|
121
|
-
await daemon.stop();
|
|
122
|
-
|
|
123
|
-
tmp[Symbol.dispose]();
|
|
124
|
-
},
|
|
120
|
+
[Symbol.asyncDispose]: dispose,
|
|
125
121
|
};
|
|
126
122
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
4
5
|
|
|
5
6
|
interface TempDir {
|
|
6
7
|
readonly dir: string;
|
|
@@ -9,17 +10,17 @@ interface TempDir {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Creates a
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Creates a directory under the system temp root, named by the prefix, and
|
|
14
|
+
* registers its removal to run once the current test finishes, so it must
|
|
15
|
+
* run inside a test. Disposal removes the tree sooner; whichever comes
|
|
16
|
+
* second does nothing.
|
|
16
17
|
*/
|
|
17
18
|
export function setupTempDir(prefix: string): TempDir {
|
|
18
19
|
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
19
20
|
|
|
20
|
-
const remove = () => {
|
|
21
|
+
const remove = registerTestCleanup(() => {
|
|
21
22
|
rmSync(dir, { recursive: true, force: true });
|
|
22
|
-
};
|
|
23
|
+
});
|
|
23
24
|
|
|
24
25
|
return {
|
|
25
26
|
dir,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { KEYS } from './keys';
|
|
2
|
+
import type { TUIHarness } from './start-tui-harness';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spawns a Claude session named `name` through the client's spawn flow,
|
|
6
|
+
* from the home screen or the overlay: the first agent, the first
|
|
7
|
+
* directory, the name, and no first prompt. Resolves once the fake
|
|
8
|
+
* `claude` paints, with the capture holding what the client drew since the
|
|
9
|
+
* prompt step.
|
|
10
|
+
*/
|
|
11
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- a harness is a live handle
|
|
12
|
+
export async function spawnClaudeSession(tui: TUIHarness, name: string): Promise<void> {
|
|
13
|
+
tui.write('n');
|
|
14
|
+
|
|
15
|
+
await tui.waitFor('spawn: agent');
|
|
16
|
+
|
|
17
|
+
tui.write(KEYS.enter);
|
|
18
|
+
|
|
19
|
+
await tui.waitFor('spawn: directory');
|
|
20
|
+
|
|
21
|
+
tui.write(KEYS.enter);
|
|
22
|
+
|
|
23
|
+
await tui.waitFor('spawn: name');
|
|
24
|
+
|
|
25
|
+
tui.reset();
|
|
26
|
+
tui.write(`${name}${KEYS.enter}`);
|
|
27
|
+
|
|
28
|
+
await tui.waitFor('spawn: initial prompt');
|
|
29
|
+
|
|
30
|
+
tui.reset();
|
|
31
|
+
tui.write(KEYS.enter);
|
|
32
|
+
|
|
33
|
+
await tui.waitFor('FAKE_CLAUDE_UP');
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { KEYS } from './keys';
|
|
2
|
+
import type { TUIHarness } from './start-tui-harness';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spawns a Grok session named `name` through the client's spawn flow, from
|
|
6
|
+
* the home screen or the overlay: Grok, the second agent, then the first
|
|
7
|
+
* directory, the name, and no first prompt. Resolves once the fake `grok`
|
|
8
|
+
* paints, with the capture holding what the client drew since the prompt
|
|
9
|
+
* step.
|
|
10
|
+
*/
|
|
11
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- a harness is a live handle
|
|
12
|
+
export async function spawnGrokSession(tui: TUIHarness, name: string): Promise<void> {
|
|
13
|
+
tui.write('n');
|
|
14
|
+
|
|
15
|
+
await tui.waitFor('spawn: agent');
|
|
16
|
+
|
|
17
|
+
tui.reset();
|
|
18
|
+
tui.write(KEYS.down);
|
|
19
|
+
|
|
20
|
+
await tui.waitFor('\u001B[7mGrok');
|
|
21
|
+
|
|
22
|
+
tui.write(KEYS.enter);
|
|
23
|
+
|
|
24
|
+
await tui.waitFor('spawn: directory');
|
|
25
|
+
|
|
26
|
+
tui.write(KEYS.enter);
|
|
27
|
+
|
|
28
|
+
await tui.waitFor('spawn: name');
|
|
29
|
+
|
|
30
|
+
tui.reset();
|
|
31
|
+
tui.write(`${name}${KEYS.enter}`);
|
|
32
|
+
|
|
33
|
+
await tui.waitFor('spawn: initial prompt');
|
|
34
|
+
|
|
35
|
+
tui.reset();
|
|
36
|
+
tui.write(KEYS.enter);
|
|
37
|
+
|
|
38
|
+
await tui.waitFor('FAKE_GROK_UP');
|
|
39
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Socket } from 'bun';
|
|
2
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
3
|
|
|
3
4
|
interface CutProxyOptions {
|
|
4
5
|
// The address the proxy forwards to.
|
|
@@ -20,7 +21,11 @@ export interface CutProxy {
|
|
|
20
21
|
|
|
21
22
|
// How many requests of the method the proxy has seen.
|
|
22
23
|
readonly countRequests: () => number;
|
|
24
|
+
|
|
25
|
+
// How many answers the proxy has swallowed in hold mode.
|
|
26
|
+
readonly countHeld: () => number;
|
|
23
27
|
readonly stop: () => void;
|
|
28
|
+
readonly [Symbol.dispose]: () => void;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
// One proxied connection: the target side once it is connected, what the
|
|
@@ -33,6 +38,9 @@ interface ProxyLink {
|
|
|
33
38
|
lineBuffer: string;
|
|
34
39
|
cutting: boolean;
|
|
35
40
|
held: boolean;
|
|
41
|
+
|
|
42
|
+
// Counts an answer the link swallows in hold mode.
|
|
43
|
+
readonly onHold: () => void;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/**
|
|
@@ -40,10 +48,14 @@ interface ProxyLink {
|
|
|
40
48
|
* connection byte for byte, except that for the first `cuts` requests of
|
|
41
49
|
* the method it loses the target's next answer after the request reached
|
|
42
50
|
* the target: the request runs, and the client never sees its response.
|
|
51
|
+
* It stops, ending every link, once the current test finishes, so it must
|
|
52
|
+
* run inside a test; `stop` or disposal stops it sooner, and a second stop
|
|
53
|
+
* does nothing.
|
|
43
54
|
*/
|
|
44
55
|
export function startCutProxy(options: CutProxyOptions): CutProxy {
|
|
45
56
|
let cutsLeft = options.cuts;
|
|
46
57
|
let requests = 0;
|
|
58
|
+
let held = 0;
|
|
47
59
|
|
|
48
60
|
const links = new Set<Socket<ProxyLink>>();
|
|
49
61
|
|
|
@@ -59,6 +71,9 @@ export function startCutProxy(options: CutProxyOptions): CutProxy {
|
|
|
59
71
|
lineBuffer: '',
|
|
60
72
|
cutting: false,
|
|
61
73
|
held: false,
|
|
74
|
+
onHold: () => {
|
|
75
|
+
held++;
|
|
76
|
+
},
|
|
62
77
|
};
|
|
63
78
|
|
|
64
79
|
links.add(client);
|
|
@@ -110,17 +125,21 @@ export function startCutProxy(options: CutProxyOptions): CutProxy {
|
|
|
110
125
|
},
|
|
111
126
|
});
|
|
112
127
|
|
|
128
|
+
const stop = registerTestCleanup(() => {
|
|
129
|
+
for (const client of links) {
|
|
130
|
+
client.end();
|
|
131
|
+
client.data.upstream?.end();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
server.stop(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
113
137
|
return {
|
|
114
138
|
port: server.port,
|
|
115
139
|
countRequests: () => requests,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
client.data.upstream?.end();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
server.stop(true);
|
|
123
|
-
},
|
|
140
|
+
countHeld: () => held,
|
|
141
|
+
stop,
|
|
142
|
+
[Symbol.dispose]: stop,
|
|
124
143
|
};
|
|
125
144
|
}
|
|
126
145
|
|
|
@@ -149,6 +168,8 @@ async function openUpstream(client: Socket<ProxyLink>, options: CutProxyOptions)
|
|
|
149
168
|
if (options.mode === 'hold') {
|
|
150
169
|
client.data.held = true;
|
|
151
170
|
|
|
171
|
+
client.data.onHold();
|
|
172
|
+
|
|
152
173
|
return;
|
|
153
174
|
}
|
|
154
175
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { Subprocess } from 'bun';
|
|
4
|
+
import { DaemonClient } from '../client/daemon-client';
|
|
5
|
+
import { findDaemonRecord } from '../shared/find-daemon-record';
|
|
6
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
7
|
+
import { waitFor } from './wait-for';
|
|
8
|
+
|
|
9
|
+
interface DaemonProcessConfig {
|
|
10
|
+
// The command atc runs as, such as the source entry under bun or a
|
|
11
|
+
// compiled binary.
|
|
12
|
+
readonly command: readonly string[];
|
|
13
|
+
|
|
14
|
+
// HOME and XDG_RUNTIME_DIR of the daemon, so its config, state, and
|
|
15
|
+
// sockets all sit under this directory.
|
|
16
|
+
readonly home: string;
|
|
17
|
+
|
|
18
|
+
// Variables laid over the inherited environment and the default PATH;
|
|
19
|
+
// `undefined` removes one.
|
|
20
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
21
|
+
|
|
22
|
+
// Arguments after `atc daemon`.
|
|
23
|
+
readonly args?: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One boot of the daemon: its process and the file its stderr goes to.
|
|
28
|
+
*/
|
|
29
|
+
interface DaemonBoot {
|
|
30
|
+
readonly proc: Subprocess;
|
|
31
|
+
readonly stderrPath: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Starts `atc daemon` as its own process on a home, with the test's
|
|
36
|
+
* environment, HOME and XDG_RUNTIME_DIR at the home, and a PATH of the system
|
|
37
|
+
* directories alone unless the config's variables set another. It returns at
|
|
38
|
+
* once; `openClient` waits up to 15 seconds for the socket and connects,
|
|
39
|
+
* sending no handshake, and fails at once with the daemon's stderr when the
|
|
40
|
+
* daemon exits before it listens. `restart` stops the daemon with the given
|
|
41
|
+
* signal, waits for it to exit, and starts another on the same home with the
|
|
42
|
+
* same config. `readStderr` reads what the current boot printed. Disposal
|
|
43
|
+
* closes every client it opened, kills the daemon and waits for it to exit,
|
|
44
|
+
* then kills the daemon the home's state directory records, which a restart
|
|
45
|
+
* the test asked atc for may have started. That disposal runs once the
|
|
46
|
+
* current test finishes, so it must run inside a test; disposing sooner
|
|
47
|
+
* runs it then, and a second disposal does nothing.
|
|
48
|
+
*/
|
|
49
|
+
export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
50
|
+
const socketPath = join(config.home, 'atc-daemon.sock');
|
|
51
|
+
const stateDir = join(config.home, '.local', 'state', 'atc');
|
|
52
|
+
|
|
53
|
+
const clients = new Set<DaemonClient>();
|
|
54
|
+
|
|
55
|
+
let boots = 0;
|
|
56
|
+
let disposed = false;
|
|
57
|
+
|
|
58
|
+
const boot = (): DaemonBoot => {
|
|
59
|
+
boots++;
|
|
60
|
+
|
|
61
|
+
const stderrPath = join(config.home, `daemon-${boots}.stderr`);
|
|
62
|
+
|
|
63
|
+
const proc = Bun.spawn([...config.command, 'daemon', ...(config.args ?? [])], {
|
|
64
|
+
env: Object.fromEntries(
|
|
65
|
+
Object.entries({
|
|
66
|
+
...process.env,
|
|
67
|
+
HOME: config.home,
|
|
68
|
+
XDG_RUNTIME_DIR: config.home,
|
|
69
|
+
PATH: '/usr/sbin:/usr/bin:/bin',
|
|
70
|
+
...config.env,
|
|
71
|
+
}).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
72
|
+
),
|
|
73
|
+
stdout: 'ignore',
|
|
74
|
+
stderr: Bun.file(stderrPath),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return { proc, stderrPath };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let current = boot();
|
|
81
|
+
|
|
82
|
+
const dispose = registerTestCleanup(async () => {
|
|
83
|
+
disposed = true;
|
|
84
|
+
|
|
85
|
+
for (const client of clients) {
|
|
86
|
+
client.stop();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
current.proc.kill('SIGKILL');
|
|
90
|
+
|
|
91
|
+
await current.proc.exited;
|
|
92
|
+
|
|
93
|
+
const recorded = findDaemonRecord(join(stateDir, 'daemon.json'));
|
|
94
|
+
|
|
95
|
+
if (recorded !== null && recorded.pid !== current.proc.pid) {
|
|
96
|
+
try {
|
|
97
|
+
process.kill(recorded.pid, 'SIGKILL');
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const readStderr = () => {
|
|
103
|
+
try {
|
|
104
|
+
return readFileSync(current.stderrPath, 'utf8');
|
|
105
|
+
} catch {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// A client that opens after the wait for it ended, or after disposal,
|
|
111
|
+
// closes at once, so nothing outlives the daemon it reached.
|
|
112
|
+
const openTrackedClient = async (isAbandoned: () => boolean) => {
|
|
113
|
+
const client = await DaemonClient.open(socketPath);
|
|
114
|
+
|
|
115
|
+
if (disposed || isAbandoned()) {
|
|
116
|
+
client.stop();
|
|
117
|
+
throw new Error('the wait for the daemon ended before this client opened');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
clients.add(client);
|
|
121
|
+
|
|
122
|
+
return client;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const openClient = async (): Promise<DaemonClient> => {
|
|
126
|
+
const watched = current.proc;
|
|
127
|
+
let settled = false;
|
|
128
|
+
const isSettled = () => settled;
|
|
129
|
+
|
|
130
|
+
// A daemon that has exited either refused to start or handed its socket
|
|
131
|
+
// to a replacement, so one more connect tells the two apart.
|
|
132
|
+
const openAfterExit = async () => {
|
|
133
|
+
await watched.exited;
|
|
134
|
+
|
|
135
|
+
if (settled || disposed) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
return await openTrackedClient(isSettled);
|
|
141
|
+
} catch {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`the daemon exited (${String(watched.exitCode ?? watched.signalCode)}) before it listened:\n${readStderr()}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Polls the socket while the daemon runs. Once the other wait settles or
|
|
149
|
+
// the daemon is disposed, each attempt resolves empty, which ends the
|
|
150
|
+
// polling.
|
|
151
|
+
const openWhenListening = () =>
|
|
152
|
+
waitFor(() => (settled || disposed ? null : openTrackedClient(isSettled)), {
|
|
153
|
+
timeoutMs: 15_000,
|
|
154
|
+
intervalMs: 50,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const hasExited = watched.exitCode !== null || watched.signalCode !== null;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const client = await (hasExited
|
|
161
|
+
? openAfterExit()
|
|
162
|
+
: Promise.race([openWhenListening(), openAfterExit()]));
|
|
163
|
+
|
|
164
|
+
if (client === null) {
|
|
165
|
+
throw new Error('the wait for the daemon settled without a client');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return client;
|
|
169
|
+
} finally {
|
|
170
|
+
settled = true;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
home: config.home,
|
|
176
|
+
socketPath,
|
|
177
|
+
reporterSocketPath: join(config.home, 'atc.sock'),
|
|
178
|
+
stateDir,
|
|
179
|
+
get proc(): Subprocess {
|
|
180
|
+
return current.proc;
|
|
181
|
+
},
|
|
182
|
+
readStderr,
|
|
183
|
+
openClient,
|
|
184
|
+
async restart(signal: NodeJS.Signals): Promise<void> {
|
|
185
|
+
current.proc.kill(signal);
|
|
186
|
+
|
|
187
|
+
await current.proc.exited;
|
|
188
|
+
|
|
189
|
+
current = boot();
|
|
190
|
+
},
|
|
191
|
+
[Symbol.asyncDispose]: dispose,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerTestCleanup } from './register-test-cleanup';
|
|
2
|
+
|
|
1
3
|
interface GitHTTPServer {
|
|
2
4
|
// The base URL a repository under the root is served at, ending in `/`.
|
|
3
5
|
readonly url: string;
|
|
@@ -5,6 +7,18 @@ interface GitHTTPServer {
|
|
|
5
7
|
// Every Authorization header a request carried, in arrival order.
|
|
6
8
|
readonly authorizations: string[];
|
|
7
9
|
readonly stop: () => Promise<void>;
|
|
10
|
+
readonly [Symbol.asyncDispose]: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface GitHTTPServerOptions {
|
|
14
|
+
// How long each authenticated request waits before it is served.
|
|
15
|
+
readonly delayMs?: number;
|
|
16
|
+
|
|
17
|
+
// Waits out the delay; a timer unless given.
|
|
18
|
+
readonly wait?: (ms: number) => Promise<void>;
|
|
19
|
+
|
|
20
|
+
// Runs, and is awaited, while each authenticated request is held.
|
|
21
|
+
readonly onRequest?: () => Promise<void> | void;
|
|
8
22
|
}
|
|
9
23
|
|
|
10
24
|
/**
|
|
@@ -13,13 +27,18 @@ interface GitHTTPServer {
|
|
|
13
27
|
* carries no basic auth, so a client only clones by authenticating. The
|
|
14
28
|
* backend runs with the environment it is given. With a delay, each
|
|
15
29
|
* authenticated request waits that long before it is served, which makes a
|
|
16
|
-
* clone through it slow.
|
|
30
|
+
* clone through it slow. An `onRequest` callback runs, and is awaited, while
|
|
31
|
+
* each authenticated request is held, so it sees the client that sent the
|
|
32
|
+
* request still running. It stops once the current test finishes, so it
|
|
33
|
+
* must run inside a test; `stop` or disposal stops it sooner, and a second
|
|
34
|
+
* stop does nothing.
|
|
17
35
|
*/
|
|
18
36
|
export function startGitHTTPServer(
|
|
19
37
|
root: string,
|
|
20
38
|
env: Readonly<Record<string, string | undefined>>,
|
|
21
|
-
options:
|
|
39
|
+
options: GitHTTPServerOptions = {},
|
|
22
40
|
): GitHTTPServer {
|
|
41
|
+
const wait = options.wait ?? Bun.sleep;
|
|
23
42
|
const authorizations: string[] = [];
|
|
24
43
|
|
|
25
44
|
const server = Bun.serve({
|
|
@@ -38,9 +57,11 @@ export function startGitHTTPServer(
|
|
|
38
57
|
authorizations.push(authorization);
|
|
39
58
|
|
|
40
59
|
if (options.delayMs !== undefined) {
|
|
41
|
-
await
|
|
60
|
+
await wait(options.delayMs);
|
|
42
61
|
}
|
|
43
62
|
|
|
63
|
+
await options.onRequest?.();
|
|
64
|
+
|
|
44
65
|
const url = new URL(request.url);
|
|
45
66
|
|
|
46
67
|
const body = await request.arrayBuffer();
|
|
@@ -70,10 +91,13 @@ export function startGitHTTPServer(
|
|
|
70
91
|
},
|
|
71
92
|
});
|
|
72
93
|
|
|
94
|
+
const stop = registerTestCleanup(() => server.stop(true));
|
|
95
|
+
|
|
73
96
|
return {
|
|
74
97
|
url: `http://127.0.0.1:${server.port}/`,
|
|
75
98
|
authorizations,
|
|
76
|
-
stop
|
|
99
|
+
stop,
|
|
100
|
+
[Symbol.asyncDispose]: stop,
|
|
77
101
|
};
|
|
78
102
|
}
|
|
79
103
|
|