@zgeoff/atc 3.3.0 → 3.4.0
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 +4 -2
- package/src/agents/agent-adapter.ts +10 -0
- package/src/agents/build-cli-argv.ts +2 -4
- package/src/agents/claude-adapter.ts +13 -3
- package/src/agents/codex-adapter.ts +11 -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/resolve-agent-home.ts +7 -3
- package/src/agents/resolve-claude-global-config-path.ts +4 -4
- package/src/agents/update-claude-project-trust.ts +41 -30
- package/src/agents/with-claude-config-lock.ts +13 -1
- package/src/agents/write-atc-bridge.ts +10 -7
- package/src/agents/write-hook-settings.ts +13 -7
- package/src/client/boot-daemon.ts +48 -15
- 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/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/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/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 +85 -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.ts +4 -2
- 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 +15 -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-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-key-binding-claim.ts +34 -0
- package/src/test-utils/build-mock-message-record.ts +22 -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-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-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/{fixture-imp-port.ts → build-stub-imp-port.ts} +119 -63
- 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-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-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 +120 -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-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/create-stub-recording-claude.ts +20 -0
- package/src/test-utils/{setup-fake-systemd.ts → create-stub-systemd.ts} +5 -5
- 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/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-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 +99 -0
- package/src/test-utils/setup-mcp-http.ts +19 -31
- 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 +25 -8
- package/src/test-utils/start-daemon-process.ts +192 -0
- package/src/test-utils/start-git-http-server.ts +21 -3
- package/src/test-utils/start-mcp-stdio.ts +195 -0
- package/src/test-utils/start-output-capture.ts +20 -0
- package/src/test-utils/start-stub-dropping-daemon.ts +103 -0
- package/src/test-utils/start-stub-impd-info.ts +32 -0
- package/src/test-utils/start-stub-impd.ts +138 -0
- package/src/test-utils/start-stub-legacy-daemon.ts +166 -0
- package/src/test-utils/start-stub-mcp-server.ts +50 -0
- package/src/test-utils/start-stub-refusing-listener.ts +38 -0
- package/src/test-utils/start-stub-reporter-socket.ts +77 -0
- package/src/test-utils/start-stub-restore-daemon.ts +58 -0
- package/src/test-utils/start-stub-session-bridge.ts +98 -0
- package/src/test-utils/start-stub-silent-server.ts +33 -0
- package/src/test-utils/start-stub-source-daemon.ts +39 -0
- package/src/test-utils/start-stub-stalled-listener.ts +40 -0
- package/src/test-utils/start-stub-tap.ts +38 -0
- package/src/test-utils/start-stub-tcp-daemon.ts +70 -0
- package/src/test-utils/start-stub-telemetry-collector.ts +26 -0
- package/src/test-utils/start-test-daemon.ts +215 -0
- package/src/test-utils/start-tui-harness.ts +284 -0
- package/src/test-utils/subscribe-to-socket-lines.ts +59 -12
- 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 +62 -13
- 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,120 @@
|
|
|
1
|
+
import { cpSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { $ } from 'bun';
|
|
4
|
+
import { setupTempDir } from './setup-temp-dir';
|
|
5
|
+
|
|
6
|
+
interface GitFixtureConfig {
|
|
7
|
+
// The temp directory's name prefix.
|
|
8
|
+
readonly prefix?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A bare upstream at `upstream.git` on branch `main` and a work clone of it
|
|
13
|
+
* at `work`, both in a fresh temp directory, holding one pushed commit that
|
|
14
|
+
* adds `README.md`. The clone commits as `atc <atc@example.com>` without
|
|
15
|
+
* signing, and its origin is this fixture's own upstream. `sha` is that
|
|
16
|
+
* commit's id, the same in every fixture. `env` is the process environment
|
|
17
|
+
* without any `GIT_*` variable and with the host's system and global git
|
|
18
|
+
* config switched off: a git hook exports `GIT_DIR` and friends, which
|
|
19
|
+
* would point git at the repository running the hook, and a system-wide Git
|
|
20
|
+
* LFS install adds a pre-push hook that refuses the fixture. Run every git
|
|
21
|
+
* command a test adds with it.
|
|
22
|
+
*
|
|
23
|
+
* The first call in a process builds a template pair under the test home,
|
|
24
|
+
* and each call copies it, so a fixture costs a copy instead of a run of
|
|
25
|
+
* git commands. Disposal removes the directory; hold the result with
|
|
26
|
+
* `await using`.
|
|
27
|
+
*/
|
|
28
|
+
export async function createGitFixture(config: GitFixtureConfig = {}) {
|
|
29
|
+
const env = buildGitEnv(process.env);
|
|
30
|
+
|
|
31
|
+
const template = await resolveTemplate();
|
|
32
|
+
|
|
33
|
+
await using stack = new AsyncDisposableStack();
|
|
34
|
+
|
|
35
|
+
const tmp = stack.use(setupTempDir(config.prefix ?? 'atc-git-fixture-'));
|
|
36
|
+
const upstream = join(tmp.dir, 'upstream.git');
|
|
37
|
+
const work = join(tmp.dir, 'work');
|
|
38
|
+
|
|
39
|
+
cpSync(template.dir, tmp.dir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
// The copied clone's config still holds the template's upstream as its
|
|
42
|
+
// origin; it is pointed at this fixture's own upstream instead, so a push
|
|
43
|
+
// never reaches the template or another fixture.
|
|
44
|
+
const gitConfig = join(work, '.git', 'config');
|
|
45
|
+
|
|
46
|
+
writeFileSync(
|
|
47
|
+
gitConfig,
|
|
48
|
+
readFileSync(gitConfig, 'utf8').replace(
|
|
49
|
+
`url = ${join(template.dir, 'upstream.git')}`,
|
|
50
|
+
`url = ${upstream}`,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const owned = stack.move();
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
dir: tmp.dir,
|
|
58
|
+
env,
|
|
59
|
+
upstream,
|
|
60
|
+
work,
|
|
61
|
+
sha: template.sha,
|
|
62
|
+
[Symbol.asyncDispose]: () => owned.disposeAsync(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildGitEnv(
|
|
67
|
+
source: Readonly<Record<string, string | undefined>>,
|
|
68
|
+
): Record<string, string | undefined> {
|
|
69
|
+
return {
|
|
70
|
+
...Object.fromEntries(Object.entries(source).filter(([name]) => !name.startsWith('GIT_'))),
|
|
71
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
72
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface GitTemplate {
|
|
77
|
+
readonly dir: string;
|
|
78
|
+
readonly sha: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The template pair, built once per process.
|
|
82
|
+
let template: Promise<GitTemplate> | null = null;
|
|
83
|
+
|
|
84
|
+
function resolveTemplate(): Promise<GitTemplate> {
|
|
85
|
+
template ??= createTemplate();
|
|
86
|
+
|
|
87
|
+
return template;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The template lives under the test home, which the test script removes
|
|
91
|
+
// when the run ends.
|
|
92
|
+
async function createTemplate(): Promise<GitTemplate> {
|
|
93
|
+
const env = buildGitEnv(process.env);
|
|
94
|
+
const home = process.env['ATC_TEST_HOME'];
|
|
95
|
+
|
|
96
|
+
if (home === undefined) {
|
|
97
|
+
throw new Error('a git fixture needs the test home; run `bun run test`');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const dir = mkdtempSync(join(home, 'git-fixture-template-'));
|
|
101
|
+
const upstream = join(dir, 'upstream.git');
|
|
102
|
+
const work = join(dir, 'work');
|
|
103
|
+
|
|
104
|
+
await $`git init --quiet --bare --template= --initial-branch=main ${upstream}`.env(env).quiet();
|
|
105
|
+
await $`git clone --quiet --template= ${upstream} ${work}`.env(env).quiet();
|
|
106
|
+
await $`git config user.name atc`.env(env).cwd(work).quiet();
|
|
107
|
+
await $`git config user.email atc@example.com`.env(env).cwd(work).quiet();
|
|
108
|
+
await $`git config commit.gpgsign false`.env(env).cwd(work).quiet();
|
|
109
|
+
await $`git config tag.gpgsign false`.env(env).cwd(work).quiet();
|
|
110
|
+
|
|
111
|
+
writeFileSync(join(work, 'README.md'), 'hello\n');
|
|
112
|
+
|
|
113
|
+
await $`git add README.md`.env(env).cwd(work).quiet();
|
|
114
|
+
await $`git commit --quiet -m initial`.env(env).cwd(work).quiet();
|
|
115
|
+
await $`git push --quiet origin main`.env(env).cwd(work).quiet();
|
|
116
|
+
|
|
117
|
+
const head = await $`git rev-parse HEAD`.env(env).cwd(work).quiet().text();
|
|
118
|
+
|
|
119
|
+
return { dir, sha: head.trim() };
|
|
120
|
+
}
|
|
@@ -11,24 +11,26 @@ let template: Promise<string> | undefined;
|
|
|
11
11
|
* that needs a ready store skips the migration ladder. Each copy is a file of
|
|
12
12
|
* its own; every copy holds the same daemon id, minted when the template was
|
|
13
13
|
* migrated. The template sits in the test home, which the test script removes
|
|
14
|
-
* on exit
|
|
14
|
+
* on exit, under a name of its own per process, since parallel test processes
|
|
15
|
+
* share that home. It rejects when `ATC_TEST_HOME` is unset, as it is
|
|
16
|
+
* outside the test script.
|
|
15
17
|
*/
|
|
16
18
|
export async function createMigratedStateDB(dbPath: string): Promise<void> {
|
|
17
|
-
template ??= createTemplateStateDB();
|
|
18
|
-
|
|
19
|
-
const templatePath = await template;
|
|
20
|
-
|
|
21
|
-
copyFileSync(templatePath, dbPath);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function createTemplateStateDB(): Promise<string> {
|
|
25
19
|
const home = process.env['ATC_TEST_HOME'];
|
|
26
20
|
|
|
27
21
|
if (home === undefined) {
|
|
28
22
|
throw new Error('ATC_TEST_HOME is unset; run the tests through `bun run test`');
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
|
|
25
|
+
template ??= createTemplateStateDB(home);
|
|
26
|
+
|
|
27
|
+
const templatePath = await template;
|
|
28
|
+
|
|
29
|
+
copyFileSync(templatePath, dbPath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function createTemplateStateDB(home: string): Promise<string> {
|
|
33
|
+
const path = join(home, `migrated-state-${String(process.pid)}.db`);
|
|
32
34
|
|
|
33
35
|
const store = await StateStore.open(path);
|
|
34
36
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a stand-in command-line tool: the script, shebang included, as an
|
|
6
|
+
* executable file under the directory, which it creates when missing.
|
|
7
|
+
* Returns the script's path. Writing over an existing file replaces its
|
|
8
|
+
* content and makes it executable as well, so a test can swap a tool's
|
|
9
|
+
* behaviour mid-run.
|
|
10
|
+
*/
|
|
11
|
+
export function createStubBin(dir: string, name: string, script: string): string {
|
|
12
|
+
const path = join(dir, name);
|
|
13
|
+
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
writeFileSync(path, script);
|
|
16
|
+
chmodSync(path, 0o755);
|
|
17
|
+
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createStubBin } from './create-stub-bin';
|
|
2
|
+
|
|
3
|
+
interface StubClaudeConfig {
|
|
4
|
+
// The command atc runs as, which the stub's tap and restart go through.
|
|
5
|
+
readonly atc: readonly string[];
|
|
6
|
+
|
|
7
|
+
// The composer script the stub finishes in when the home asks for it.
|
|
8
|
+
readonly composer: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a stand-in for the Claude CLI as `fake-claude` under the
|
|
13
|
+
* directory and returns its path. Like the real CLI, it runs the
|
|
14
|
+
* `SessionStart` hook command from the `--settings` file atc passed it
|
|
15
|
+
* through a shell, feeding each hook its JSON payload on stdin, so a session
|
|
16
|
+
* reports with the command line atc wrote for its agent. It prints its
|
|
17
|
+
* arguments, its TERM, and whether it inherited a parent-session variable,
|
|
18
|
+
* appends its atc session id to `$HOME/fake-claude-starts.log`, reports a
|
|
19
|
+
* `SessionStart` as `fake-1` and a `Notification` asking for permission, then
|
|
20
|
+
* echoes each input line as `GOT:<line>`.
|
|
21
|
+
*
|
|
22
|
+
* Files in `$HOME` steer a scenario: `fake-claude-dies-<id>` exits at once
|
|
23
|
+
* when the stub resumes that agent session; `fake-claude-hold-start` reports
|
|
24
|
+
* nothing and only echoes input; `fake-claude-composer` runs the composer
|
|
25
|
+
* instead; `fake-claude-gate` waits for one input line before it reports,
|
|
26
|
+
* and the first stub to find it removes it; `fake-claude-own-id` reports the
|
|
27
|
+
* atc session id as the agent session; `fake-claude-restart` runs
|
|
28
|
+
* `atc daemon restart` into `$HOME/restart.out` after `SessionStart`, and the
|
|
29
|
+
* first stub to find it removes it; `fake-claude-tap` runs `atc tap` into
|
|
30
|
+
* `$HOME/tap.jsonl`; `fake-claude-events.jsonl` holds more hook payloads to
|
|
31
|
+
* report, one per line, after the `Notification`; `fake-claude-exit` exits
|
|
32
|
+
* once they are reported.
|
|
33
|
+
*/
|
|
34
|
+
export function createStubClaude(dir: string, config: StubClaudeConfig): string {
|
|
35
|
+
const atc = config.atc.map((part) => `"${part}"`).join(' ');
|
|
36
|
+
|
|
37
|
+
return createStubBin(
|
|
38
|
+
dir,
|
|
39
|
+
'fake-claude',
|
|
40
|
+
`#!/usr/bin/env bash
|
|
41
|
+
echo "FAKE_CLAUDE_UP args: $@"
|
|
42
|
+
echo "FAKE_CLAUDE_TERM:[\${TERM-unset}]"
|
|
43
|
+
echo "FAKE_CLAUDE_PARENT:[\${CLAUDE_CODE_ATC_TEST-unset}]"
|
|
44
|
+
echo "$ATC_SESSION_ID" >> "$HOME/fake-claude-starts.log"
|
|
45
|
+
settings=""
|
|
46
|
+
resume=""
|
|
47
|
+
prev=""
|
|
48
|
+
for arg in "$@"; do
|
|
49
|
+
if [ "$prev" = "--settings" ]; then settings="$arg"; fi
|
|
50
|
+
if [ "$prev" = "--resume" ]; then resume="$arg"; fi
|
|
51
|
+
prev="$arg"
|
|
52
|
+
done
|
|
53
|
+
if [ -n "$resume" ] && [ -f "$HOME/fake-claude-dies-$resume" ]; then exit 0; fi
|
|
54
|
+
if [ -f "$HOME/fake-claude-hold-start" ]; then while read -r line; do echo "GOT:$line"; done; exit 0; fi
|
|
55
|
+
if [ -f "$HOME/fake-claude-composer" ]; then exec "${process.execPath}" "${config.composer}"; fi
|
|
56
|
+
hookCommand="$("${process.execPath}" -e 'const s = JSON.parse(require("fs").readFileSync(process.argv.at(-1), "utf8")); console.log(s.hooks.SessionStart[0].hooks[0].command)' "$settings" < /dev/null)"
|
|
57
|
+
hookReport() { sh -c "$hookCommand"; }
|
|
58
|
+
sessionID="fake-1"
|
|
59
|
+
if [ -f "$HOME/fake-claude-own-id" ]; then sessionID="$ATC_SESSION_ID"; fi
|
|
60
|
+
if [ -f "$HOME/fake-claude-gate" ]; then rm "$HOME/fake-claude-gate"; read -r _; fi
|
|
61
|
+
printf '{"hook_event_name":"SessionStart","session_id":"%s","transcript_path":"%s/fake-transcript.jsonl"}' "$sessionID" "$HOME" | hookReport
|
|
62
|
+
if [ -f "$HOME/fake-claude-restart" ]; then rm "$HOME/fake-claude-restart"; ${atc} daemon restart > "$HOME/restart.out" 2>&1; fi
|
|
63
|
+
if [ -f "$HOME/fake-claude-tap" ]; then ${atc} tap --session "$ATC_SESSION_ID" >> "$HOME/tap.jsonl" & fi
|
|
64
|
+
printf '{"hook_event_name":"Notification","session_id":"%s","message":"needs permission"}' "$sessionID" | hookReport
|
|
65
|
+
if [ -f "$HOME/fake-claude-events.jsonl" ]; then
|
|
66
|
+
while IFS= read -r ev; do
|
|
67
|
+
[ -n "$ev" ] || continue
|
|
68
|
+
printf '%s' "$ev" | hookReport
|
|
69
|
+
done < "$HOME/fake-claude-events.jsonl"
|
|
70
|
+
fi
|
|
71
|
+
if [ -f "$HOME/fake-claude-exit" ]; then exit 0; fi
|
|
72
|
+
while read -r line; do echo "GOT:$line"; done
|
|
73
|
+
`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createStubBin } from './create-stub-bin';
|
|
2
|
+
|
|
3
|
+
interface StubCodexConfig {
|
|
4
|
+
// The command atc runs as; the stub reports each hook through its
|
|
5
|
+
// `hook-report --agent codex`, as the hook entries atc prints for Codex do.
|
|
6
|
+
readonly atc: readonly string[];
|
|
7
|
+
|
|
8
|
+
// The composer script the stub finishes in.
|
|
9
|
+
readonly composer: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a stand-in for the Codex CLI as `fake-codex` under the directory
|
|
14
|
+
* and returns its path. It prints its arguments, reports a `SessionStart`
|
|
15
|
+
* as `fake-codex-1` with its working directory and a rollout under `$HOME`,
|
|
16
|
+
* then a `Stop` whose last message is `pong`, then runs the composer.
|
|
17
|
+
*/
|
|
18
|
+
export function createStubCodex(dir: string, config: StubCodexConfig): string {
|
|
19
|
+
const report = `${config.atc.map((part) => `"${part}"`).join(' ')} hook-report --agent codex`;
|
|
20
|
+
|
|
21
|
+
return createStubBin(
|
|
22
|
+
dir,
|
|
23
|
+
'fake-codex',
|
|
24
|
+
`#!/usr/bin/env bash
|
|
25
|
+
echo "FAKE_CODEX_UP args: $@"
|
|
26
|
+
printf '{"hook_event_name":"SessionStart","session_id":"fake-codex-1","transcript_path":"%s/fake-rollout.jsonl","cwd":"%s","source":"startup"}' "$HOME" "$PWD" | ${report}
|
|
27
|
+
printf '{"hook_event_name":"Stop","session_id":"fake-codex-1","transcript_path":"%s/fake-rollout.jsonl","last_assistant_message":"pong"}' "$HOME" | ${report}
|
|
28
|
+
exec "${process.execPath}" "${config.composer}"
|
|
29
|
+
`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createStubBin } from './create-stub-bin';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates the composer the stub agents finish in, as `fake-composer.js`
|
|
5
|
+
* under the directory, and returns its path; run it with bun. It is modelled
|
|
6
|
+
* on the TUIs Codex and Grok draw: raw mode with bracketed paste on. Each
|
|
7
|
+
* read is one input event batch. A bracketed paste lands in the composer
|
|
8
|
+
* whole, newlines kept; a lone CR outside a paste submits; a lone LF is
|
|
9
|
+
* Ctrl-J and adds a newline; any other read of more than one byte is a paste
|
|
10
|
+
* burst whose line breaks stay in the composer. A read that ends partway
|
|
11
|
+
* into a paste marker holds that part for the next read. It prints
|
|
12
|
+
* `FAKE_COMPOSER_READY` once paste mode is on, every byte received so far as
|
|
13
|
+
* `RECEIVED:<json>` after each read, and each submission as `SUBMIT:<json>`.
|
|
14
|
+
*/
|
|
15
|
+
export function createStubComposer(dir: string): string {
|
|
16
|
+
return createStubBin(
|
|
17
|
+
dir,
|
|
18
|
+
'fake-composer.js',
|
|
19
|
+
String.raw`const OPEN = '\u001B[200~';
|
|
20
|
+
const CLOSE = '\u001B[201~';
|
|
21
|
+
let composer = '';
|
|
22
|
+
let pasting = false;
|
|
23
|
+
let held = '';
|
|
24
|
+
let received = '';
|
|
25
|
+
process.stdin.setRawMode(true);
|
|
26
|
+
process.stdout.write('\u001B[?2004hFAKE_COMPOSER_READY\r\n');
|
|
27
|
+
process.stdin.on('data', (buf) => {
|
|
28
|
+
const chunk = buf.toString('utf8');
|
|
29
|
+
received += chunk;
|
|
30
|
+
process.stdout.write('RECEIVED:' + JSON.stringify(received) + '\r\n');
|
|
31
|
+
let rest = held + chunk;
|
|
32
|
+
held = '';
|
|
33
|
+
const cut = rest.lastIndexOf('\u001B');
|
|
34
|
+
const tail = cut === -1 ? '' : rest.slice(cut);
|
|
35
|
+
if (tail !== '' && tail.length < CLOSE.length && (OPEN.startsWith(tail) || CLOSE.startsWith(tail))) {
|
|
36
|
+
held = tail;
|
|
37
|
+
rest = rest.slice(0, cut);
|
|
38
|
+
}
|
|
39
|
+
while (rest !== '') {
|
|
40
|
+
if (pasting) {
|
|
41
|
+
const end = rest.indexOf(CLOSE);
|
|
42
|
+
composer += (end === -1 ? rest : rest.slice(0, end)).replaceAll('\r', '\n');
|
|
43
|
+
pasting = end === -1;
|
|
44
|
+
rest = end === -1 ? '' : rest.slice(end + CLOSE.length);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (rest.startsWith(OPEN)) {
|
|
48
|
+
pasting = true;
|
|
49
|
+
rest = rest.slice(OPEN.length);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const next = rest.indexOf(OPEN);
|
|
53
|
+
const plain = next === -1 ? rest : rest.slice(0, next);
|
|
54
|
+
rest = next === -1 ? '' : rest.slice(next);
|
|
55
|
+
if (plain === '\r') {
|
|
56
|
+
process.stdout.write('SUBMIT:' + JSON.stringify(composer) + '\r\n');
|
|
57
|
+
composer = '';
|
|
58
|
+
} else {
|
|
59
|
+
composer += plain.replaceAll('\r', '\n');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { AgentAdapter } from '../agents/agent-adapter';
|
|
2
|
+
import { buildMockAgentAdapter } from './build-mock-agent-adapter';
|
|
3
|
+
|
|
4
|
+
interface Plan {
|
|
5
|
+
readonly bin: string;
|
|
6
|
+
readonly args: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// A named pipe the first spawn's process writes its pid to once it is ready
|
|
10
|
+
// to be killed, and how long the first failing read waits for that pid.
|
|
11
|
+
interface ReadyPipe {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly timeoutMs: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface FailingAgentAdapterConfig {
|
|
17
|
+
// What the first spawn runs, and what every later spawn runs.
|
|
18
|
+
readonly firstPlan: Plan;
|
|
19
|
+
readonly laterPlan: Plan;
|
|
20
|
+
|
|
21
|
+
// How many reads of the headless runner throw once the first spawn is
|
|
22
|
+
// planned: one fails the start after its process started, and a second
|
|
23
|
+
// fails the kill that takes the process back.
|
|
24
|
+
readonly failedReads: number;
|
|
25
|
+
|
|
26
|
+
// The pipe the first failing read waits on. Null fails at once.
|
|
27
|
+
readonly ready: ReadyPipe | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An agent adapter whose first spawn fails after its process has started:
|
|
32
|
+
* the daemon reads the adapter's headless runner once the process runs, and
|
|
33
|
+
* that read throws, as many times as the config holds, while every other
|
|
34
|
+
* read finds no headless runner. Every member but the spawn plan and the
|
|
35
|
+
* headless runner is the mock adapter's.
|
|
36
|
+
*
|
|
37
|
+
* With a ready pipe, the stub makes the pipe at that path, and the first
|
|
38
|
+
* failing read blocks until a process writes its pid there, which the
|
|
39
|
+
* headless runner read is synchronous for. A read that finds no writer
|
|
40
|
+
* within the timeout throws instead of failing the start. `getReadyPID`
|
|
41
|
+
* returns the pid that read took. `countPlans` reads how many spawns the
|
|
42
|
+
* adapter has planned.
|
|
43
|
+
*/
|
|
44
|
+
export function createStubFailingAgentAdapter(config: FailingAgentAdapterConfig) {
|
|
45
|
+
let planned = 0;
|
|
46
|
+
let readsToFail = 0;
|
|
47
|
+
let readyPID: number | null = null;
|
|
48
|
+
|
|
49
|
+
if (config.ready !== null) {
|
|
50
|
+
createPipe(config.ready.path);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const adapter: AgentAdapter = {
|
|
54
|
+
...buildMockAgentAdapter(),
|
|
55
|
+
planSpawn: () => {
|
|
56
|
+
planned += 1;
|
|
57
|
+
|
|
58
|
+
if (planned > 1) {
|
|
59
|
+
return { bin: config.laterPlan.bin, args: [...config.laterPlan.args] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
readsToFail = config.failedReads;
|
|
63
|
+
|
|
64
|
+
return { bin: config.firstPlan.bin, args: [...config.firstPlan.args] };
|
|
65
|
+
},
|
|
66
|
+
get headlessRunner() {
|
|
67
|
+
if (readsToFail === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (readsToFail === config.failedReads && config.ready !== null) {
|
|
72
|
+
readyPID = readPID(config.ready);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
readsToFail -= 1;
|
|
76
|
+
throw new Error('adapter failed after the process started');
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
adapter,
|
|
82
|
+
countPlans: () => planned,
|
|
83
|
+
getReadyPID: (): number => {
|
|
84
|
+
if (readyPID === null) {
|
|
85
|
+
throw new Error('no process has written its pid to the ready pipe');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return readyPID;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createPipe(path: string): void {
|
|
94
|
+
const made = Bun.spawnSync(['mkfifo', path]);
|
|
95
|
+
|
|
96
|
+
if (made.exitCode !== 0) {
|
|
97
|
+
throw new Error(`mkfifo could not make ${path} (${made.stderr.toString().trim()})`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Reading the pipe blocks until a writer opens it and closes it, so the
|
|
102
|
+
// read returns once the process has written its pid.
|
|
103
|
+
function readPID(ready: ReadyPipe): number {
|
|
104
|
+
const read = Bun.spawnSync({ cmd: ['cat', ready.path], timeout: ready.timeoutMs });
|
|
105
|
+
|
|
106
|
+
if (read.exitedDueToTimeout === true) {
|
|
107
|
+
throw new Error(`no process wrote ${ready.path} within ${String(ready.timeoutMs)}ms`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return Number(read.stdout.toString().trim());
|
|
111
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createStubBin } from './create-stub-bin';
|
|
2
|
+
|
|
3
|
+
interface StubGrokConfig {
|
|
4
|
+
// The command atc runs as; the stub reports each hook through its
|
|
5
|
+
// `hook-report --agent grok`, as the hook file atc prints for Grok does.
|
|
6
|
+
readonly atc: readonly string[];
|
|
7
|
+
|
|
8
|
+
// The composer script the stub finishes in.
|
|
9
|
+
readonly composer: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a stand-in for the Grok CLI as `fake-grok` under the directory and
|
|
14
|
+
* returns its path. It prints its arguments, reports a `session_start` as
|
|
15
|
+
* `fake-grok-1` with its working directory and a `notification` asking
|
|
16
|
+
* permission to edit, prints `FAKE_GROK_HOOKS_DONE` once its reports are
|
|
17
|
+
* sent, then runs the composer. Files in `$HOME` steer a scenario:
|
|
18
|
+
* `fake-grok-hold-start` reports nothing and only echoes input lines as
|
|
19
|
+
* `GOT:<line>`; `fake-grok-events.jsonl` holds the hook payloads to report
|
|
20
|
+
* after `session_start` in place of the notification, one per line.
|
|
21
|
+
*/
|
|
22
|
+
export function createStubGrok(dir: string, config: StubGrokConfig): string {
|
|
23
|
+
const report = `${config.atc.map((part) => `"${part}"`).join(' ')} hook-report --agent grok`;
|
|
24
|
+
|
|
25
|
+
return createStubBin(
|
|
26
|
+
dir,
|
|
27
|
+
'fake-grok',
|
|
28
|
+
`#!/usr/bin/env bash
|
|
29
|
+
echo "FAKE_GROK_UP args: $@"
|
|
30
|
+
if [ -f "$HOME/fake-grok-hold-start" ]; then while read -r line; do echo "GOT:$line"; done; exit 0; fi
|
|
31
|
+
printf '{"hookEventName":"session_start","sessionId":"fake-grok-1","cwd":"%s"}' "$PWD" | ${report}
|
|
32
|
+
if [ -f "$HOME/fake-grok-events.jsonl" ]; then
|
|
33
|
+
while IFS= read -r ev; do
|
|
34
|
+
[ -n "$ev" ] || continue
|
|
35
|
+
printf '%s' "$ev" | ${report}
|
|
36
|
+
done < "$HOME/fake-grok-events.jsonl"
|
|
37
|
+
else
|
|
38
|
+
printf '{"hookEventName":"notification","sessionId":"fake-grok-1","notificationType":"permission_prompt","message":"allow edit?"}' | ${report}
|
|
39
|
+
fi
|
|
40
|
+
echo "FAKE_GROK_HOOKS_DONE"
|
|
41
|
+
exec "${process.execPath}" "${config.composer}"
|
|
42
|
+
`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { createStubBin } from './create-stub-bin';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates the command-line tools a remote host runs for a session, as
|
|
6
|
+
* scripts under the directory: `atc` runs the atc CLI from this source tree,
|
|
7
|
+
* and `claude` is a stand-in agent that prints `UP:<pid>`, then reads one
|
|
8
|
+
* command per line and prints `GOT:<line>` after each, reporting through
|
|
9
|
+
* that atc:
|
|
10
|
+
*
|
|
11
|
+
* - `start <id>`: a SessionStart for agent session `<id>`, with a
|
|
12
|
+
* transcript at a path only the host holds;
|
|
13
|
+
* - `notify <text>`: a Notification carrying the text;
|
|
14
|
+
* - `nested <id>`: a SessionStart for agent session `<id>` from a Codex
|
|
15
|
+
* harness nested inside the session;
|
|
16
|
+
* - `forge <session>`: a Notification as atc session `<session>` instead
|
|
17
|
+
* of its own;
|
|
18
|
+
* - `tap <file>`: the session's tap, run in the background and printing
|
|
19
|
+
* into the file;
|
|
20
|
+
* - `answer <message> <text>`: the message answered with the text;
|
|
21
|
+
* - `note <text>`: a note labelled `progress`.
|
|
22
|
+
*
|
|
23
|
+
* Returns each tool's path.
|
|
24
|
+
*/
|
|
25
|
+
export function createStubGuestCLIs(dir: string) {
|
|
26
|
+
const cliPath = join(import.meta.dir, '..', 'cli.ts');
|
|
27
|
+
|
|
28
|
+
const atc = createStubBin(
|
|
29
|
+
dir,
|
|
30
|
+
'atc',
|
|
31
|
+
`#!/bin/sh\nexec "${process.execPath}" "${cliPath}" "$@"\n`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const claude = createStubBin(
|
|
35
|
+
dir,
|
|
36
|
+
'claude',
|
|
37
|
+
`#!/usr/bin/env bash
|
|
38
|
+
echo "UP:$$"
|
|
39
|
+
while read -r line; do
|
|
40
|
+
case "$line" in
|
|
41
|
+
start\\ *) printf '{"hook_event_name":"SessionStart","session_id":"%s","transcript_path":"/guest/only/transcript.jsonl"}' "\${line#start }" | "${atc}" hook-report --agent claude ;;
|
|
42
|
+
notify\\ *) printf '{"hook_event_name":"Notification","message":"%s"}' "\${line#notify }" | "${atc}" hook-report --agent claude ;;
|
|
43
|
+
nested\\ *) printf '{"hook_event_name":"SessionStart","session_id":"%s","source":"startup"}' "\${line#nested }" | "${atc}" hook-report --agent codex ;;
|
|
44
|
+
forge\\ *) echo '{"hook_event_name":"Notification","message":"forged"}' | ATC_SESSION_ID="\${line#forge }" "${atc}" hook-report --agent claude ;;
|
|
45
|
+
tap\\ *) "${atc}" tap --session "$ATC_SESSION_ID" >> "\${line#tap }" 2>&1 & ;;
|
|
46
|
+
answer\\ *) rest="\${line#answer }"; printf '%s' "\${rest#* }" | "${atc}" report answered --messages "\${rest%% *}" ;;
|
|
47
|
+
note\\ *) printf '%s' "\${line#note }" | "${atc}" report note --label progress ;;
|
|
48
|
+
esac
|
|
49
|
+
echo "GOT:$line"
|
|
50
|
+
done
|
|
51
|
+
`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return { atc, claude };
|
|
55
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { createStubBin } from './create-stub-bin';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a stand-in guest program for a harness to run, under the
|
|
6
|
+
* directory: a script that prints its pid and the terminal size it started
|
|
7
|
+
* at as `UP:<pid> START:<rows> <cols>`, echoes each line it reads as
|
|
8
|
+
* `GOT:<line>`, prints `SIZE:<rows> <cols>` on `size`, and exits 3 on
|
|
9
|
+
* `quit`. On `later` it waits for a line on the named pipe at `burstPath`,
|
|
10
|
+
* then prints 300000 `x` bytes, more than impd's ring keeps, a newline, and
|
|
11
|
+
* `BURST_DONE`. Returns the script's path and the pipe's.
|
|
12
|
+
*/
|
|
13
|
+
export function createStubHarnessGuest(dir: string) {
|
|
14
|
+
const burstPath = join(dir, 'burst');
|
|
15
|
+
|
|
16
|
+
Bun.spawnSync(['mkfifo', burstPath]);
|
|
17
|
+
|
|
18
|
+
const path = createStubBin(
|
|
19
|
+
dir,
|
|
20
|
+
'harness',
|
|
21
|
+
`#!/usr/bin/env bash
|
|
22
|
+
echo "UP:$$ START:$(stty size)"
|
|
23
|
+
while read -r line; do
|
|
24
|
+
if [ "$line" = "later" ]; then
|
|
25
|
+
(cat "${burstPath}" > /dev/null; head -c 300000 /dev/zero | tr '\\0' 'x'; echo; echo "BURST_DONE") &
|
|
26
|
+
fi
|
|
27
|
+
if [ "$line" = "size" ]; then echo "SIZE:$(stty size)"; fi
|
|
28
|
+
if [ "$line" = "quit" ]; then exit 3; fi
|
|
29
|
+
echo "GOT:$line"
|
|
30
|
+
done
|
|
31
|
+
`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return { path, burstPath };
|
|
35
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -2,8 +2,8 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
|
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
|
|
5
|
-
interface
|
|
6
|
-
// The directory that holds the
|
|
5
|
+
interface StubSystemd {
|
|
6
|
+
// The directory that holds the stand-in `systemctl` and `systemd-run`, to put
|
|
7
7
|
// first on the PATH of every process that could reach a real one.
|
|
8
8
|
readonly binDir: string;
|
|
9
9
|
|
|
@@ -27,7 +27,7 @@ interface FakeSystemd {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Writes a
|
|
30
|
+
* Writes a stand-in `systemctl` and `systemd-run` that touch no service
|
|
31
31
|
* manager. Without a MainPID, `systemctl show -p MainPID` prints a pid no
|
|
32
32
|
* process holds, so no daemon is ever a unit's main process. With
|
|
33
33
|
* `writeMainPID`, `systemctl restart` stops that pid with SIGTERM and starts
|
|
@@ -36,8 +36,8 @@ interface FakeSystemd {
|
|
|
36
36
|
* unit sees them, and appends its output to the `StandardOutput=append:`
|
|
37
37
|
* file. Every call is logged.
|
|
38
38
|
*/
|
|
39
|
-
export function
|
|
40
|
-
const root = mkdtempSync(join(tmpdir(), 'atc-
|
|
39
|
+
export function createStubSystemd(atcCommand: readonly string[]): StubSystemd {
|
|
40
|
+
const root = mkdtempSync(join(tmpdir(), 'atc-stub-systemd-'));
|
|
41
41
|
const binDir = join(root, 'bin');
|
|
42
42
|
const procRoot = join(root, 'proc');
|
|
43
43
|
const atcLine = atcCommand.map((part) => `"${part}"`).join(' ');
|
|
@@ -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`);
|