@zgeoff/atc 3.3.1 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/agents/agent-adapter.ts +10 -0
- package/src/agents/build-agent-adapters.ts +21 -3
- package/src/agents/build-cli-argv.ts +2 -4
- package/src/agents/claude-adapter.ts +13 -3
- package/src/agents/codex-adapter.ts +31 -2
- package/src/agents/gateway-adapter.ts +20 -8
- package/src/agents/grok-adapter.ts +16 -13
- package/src/agents/load-claude-config-bundle.ts +6 -6
- package/src/agents/print-codex-hook-file.ts +7 -3
- package/src/agents/print-grok-hook-file.ts +8 -3
- package/src/agents/render-sdk-message.ts +57 -0
- package/src/agents/resolve-agent-home.ts +7 -3
- package/src/agents/resolve-claude-global-config-path.ts +4 -4
- package/src/agents/start-claude-headless-run.ts +2 -57
- package/src/agents/update-claude-project-trust.ts +41 -30
- package/src/agents/with-claude-config-lock.ts +37 -12
- package/src/agents/write-atc-bridge.ts +10 -7
- package/src/agents/write-hook-settings.ts +13 -7
- package/src/client/boot-daemon.ts +122 -39
- package/src/client/dirs.ts +4 -2
- package/src/client/index.ts +17 -1
- package/src/client/log-client-event.ts +20 -0
- package/src/client/resolve-workspace-root.ts +10 -5
- package/src/client/spawn-picker.ts +200 -121
- package/src/client/ui.ts +7 -7
- package/src/clients.ts +41 -11
- package/src/daemon/daemon.ts +143 -33
- package/src/daemon/event-signal.ts +19 -3
- package/src/daemon/make-hook-runner.ts +82 -22
- package/src/daemon/permission-registry.ts +11 -6
- package/src/daemon/restore-fleet.ts +62 -17
- package/src/daemon/run-eject-handoff.ts +20 -2
- package/src/daemon/session-runtime.ts +0 -4
- package/src/daemon/sessions.ts +6 -0
- package/src/daemon/start-tcp-listener.ts +12 -10
- package/src/federation/daemon-caller.ts +25 -4
- package/src/federation/daemon-pool.ts +3 -1
- package/src/federation/open-gateway-caller.ts +6 -1
- package/src/gateway.ts +2 -179
- package/src/grants.ts +36 -11
- package/src/hook-report.ts +17 -3
- package/src/mcp/read-report-texts.ts +11 -7
- package/src/mcp/start-mcp-http-server.ts +5 -1
- package/src/mcp-http-server.ts +88 -32
- package/src/report.ts +21 -3
- package/src/run-bridge-tap.ts +36 -7
- package/src/run-config-migrate.ts +35 -11
- package/src/run-gateway-cli.ts +327 -0
- package/src/run-gateway.ts +51 -18
- package/src/shared/build-atc-command.ts +7 -3
- package/src/shared/bun-sqlite-driver.ts +10 -0
- package/src/shared/claim-daemon-lock.ts +10 -4
- package/src/shared/collect-dir-roots.ts +6 -6
- package/src/shared/collect-hooks.ts +7 -6
- package/src/shared/config.ts +19 -9
- package/src/shared/system-clock.ts +24 -0
- package/src/sources/github/collect-github-repos.ts +1 -1
- package/src/sources/github/read-git-protocol.ts +1 -1
- package/src/sources/github/run-gh.ts +24 -9
- package/src/store/state-store.ts +57 -7
- package/src/tap.ts +44 -13
- package/src/test-utils/build-mock-agent-adapter.ts +25 -0
- package/src/test-utils/build-mock-agent-entry.ts +32 -0
- package/src/test-utils/build-mock-auth-binding.ts +28 -0
- package/src/test-utils/build-mock-auth-profile.ts +62 -0
- package/src/test-utils/build-mock-daemon-record.ts +26 -0
- package/src/test-utils/build-mock-execution-target.ts +25 -0
- package/src/test-utils/build-mock-fleet-entry.ts +33 -0
- package/src/test-utils/build-mock-gateway-config.ts +32 -0
- package/src/test-utils/build-mock-imp-identity.ts +18 -0
- package/src/test-utils/build-mock-imp-session-request.ts +53 -0
- package/src/test-utils/build-mock-key-binding-claim.ts +34 -0
- package/src/test-utils/build-mock-message-record.ts +26 -0
- package/src/test-utils/build-mock-mirror-session.ts +42 -0
- package/src/test-utils/build-mock-registry-daemon.ts +28 -0
- package/src/test-utils/build-mock-session-descriptor.ts +66 -0
- package/src/test-utils/build-mock-session.ts +43 -0
- package/src/test-utils/build-mock-sortable-session-view.ts +20 -0
- package/src/test-utils/build-mock-stored-event.ts +20 -0
- package/src/test-utils/build-mock-stored-row.ts +23 -0
- package/src/test-utils/build-stub-attention-adapter.ts +22 -0
- package/src/test-utils/build-stub-bridge-context.ts +20 -0
- package/src/test-utils/build-stub-brokered-agent-adapter.ts +96 -0
- package/src/test-utils/build-stub-brokered-gateway-adapter.ts +50 -0
- package/src/test-utils/build-stub-channel-opener.ts +45 -0
- package/src/test-utils/build-stub-claude-headless-run.ts +13 -0
- package/src/test-utils/build-stub-client-screens.ts +33 -0
- package/src/test-utils/build-stub-clock.ts +67 -0
- package/src/test-utils/build-stub-daemon-context.ts +81 -0
- package/src/test-utils/build-stub-daemon-requests.ts +103 -0
- package/src/test-utils/{fixture-dir-provider.ts → build-stub-dir-provider.ts} +20 -12
- package/src/test-utils/build-stub-execution-provider.ts +112 -0
- package/src/test-utils/build-stub-fleet-caller.ts +60 -0
- package/src/test-utils/build-stub-forking-gh.ts +10 -0
- package/src/test-utils/build-stub-gh.ts +50 -0
- package/src/test-utils/build-stub-handoff-daemon.ts +11 -0
- package/src/test-utils/build-stub-harness-relay.ts +50 -0
- package/src/test-utils/build-stub-headless-runner.ts +60 -0
- package/src/test-utils/build-stub-held-gh.ts +19 -0
- package/src/test-utils/build-stub-held-inbox-source.ts +36 -0
- package/src/test-utils/build-stub-held-provider.ts +58 -0
- package/src/test-utils/build-stub-held-zoxide.ts +13 -0
- package/src/test-utils/build-stub-host-hold.ts +31 -0
- package/src/test-utils/build-stub-host-provider.ts +26 -0
- package/src/test-utils/build-stub-log.ts +15 -0
- package/src/test-utils/build-stub-mcp-claude.ts +26 -0
- package/src/test-utils/build-stub-mcp-grok.ts +19 -0
- package/src/test-utils/build-stub-mcp-stdio-server.ts +20 -0
- package/src/test-utils/build-stub-peer-socket.ts +81 -0
- package/src/test-utils/build-stub-picker-host.ts +36 -0
- package/src/test-utils/build-stub-proxied-agent-adapter.ts +29 -0
- package/src/test-utils/build-stub-pty-provider.ts +53 -0
- package/src/test-utils/build-stub-recording-atc.ts +10 -0
- package/src/test-utils/build-stub-recording-filter.ts +9 -0
- package/src/test-utils/build-stub-recording-git.ts +8 -0
- package/src/test-utils/build-stub-signed-in-gh.ts +18 -0
- package/src/test-utils/build-stub-signed-out-gh.ts +8 -0
- package/src/test-utils/build-stub-soft-kill-provider.ts +29 -0
- package/src/test-utils/build-stub-targets.ts +58 -0
- package/src/test-utils/build-stub-terminal.ts +19 -0
- package/src/test-utils/build-stub-timeout-scheduler.ts +48 -0
- package/src/test-utils/build-stub-tui-claude.ts +47 -0
- package/src/test-utils/build-stub-tui-grok.ts +53 -0
- package/src/test-utils/build-stub-wait-clock.ts +23 -0
- package/src/test-utils/build-target-options-from-config.ts +37 -0
- package/src/test-utils/collect-process-tree.ts +75 -0
- package/src/test-utils/create-git-fixture.ts +117 -0
- package/src/test-utils/create-migrated-state-db.ts +12 -10
- package/src/test-utils/create-stub-bin.ts +19 -0
- package/src/test-utils/create-stub-claude.ts +75 -0
- package/src/test-utils/create-stub-codex.ts +31 -0
- package/src/test-utils/create-stub-composer.ts +65 -0
- package/src/test-utils/create-stub-echo-claude.ts +21 -0
- package/src/test-utils/create-stub-failing-agent-adapter.ts +111 -0
- package/src/test-utils/create-stub-grok.ts +44 -0
- package/src/test-utils/create-stub-guest-clis.ts +55 -0
- package/src/test-utils/create-stub-harness-guest.ts +35 -0
- package/src/test-utils/{fixture-imp-port.ts → create-stub-imp-port.ts} +144 -63
- package/src/test-utils/create-stub-recording-claude.ts +20 -0
- package/src/test-utils/create-stub-recording-codex.ts +20 -0
- package/src/test-utils/create-stub-shared-server-codex.ts +56 -0
- package/src/test-utils/{setup-fake-systemd.ts → create-stub-systemd.ts} +15 -9
- package/src/test-utils/get-gateway-config.ts +3 -3
- package/src/test-utils/get-only-effect-ref.ts +21 -0
- package/src/test-utils/get-only-imp-name.ts +19 -0
- package/src/test-utils/get-records.ts +20 -0
- package/src/test-utils/get-string.ts +14 -0
- package/src/test-utils/keys.ts +26 -0
- package/src/test-utils/merge-deep.ts +36 -0
- package/src/test-utils/mock-overrides.ts +13 -0
- package/src/test-utils/open-repo-step.ts +24 -0
- package/src/test-utils/read-query-plan.ts +22 -0
- package/src/test-utils/register-test-cleanup.ts +63 -0
- package/src/test-utils/resolve-atc-command.ts +13 -0
- package/src/test-utils/resolve-gateway-command.ts +13 -0
- package/src/test-utils/run-atc.ts +56 -0
- package/src/test-utils/run-legacy-daemon.ts +27 -5
- package/src/test-utils/run-listener-daemon.ts +5 -10
- package/src/test-utils/run-mcp-authorization.ts +7 -4
- package/src/test-utils/run-source-daemon.ts +2 -2
- package/src/test-utils/seed-faker.ts +7 -0
- package/src/test-utils/send-lines-before-handshake.ts +35 -0
- package/src/test-utils/send-mcp-request.ts +25 -0
- package/src/test-utils/setup-mcp-home.ts +110 -0
- package/src/test-utils/setup-mcp-http.ts +26 -30
- package/src/test-utils/setup-temp-dir.ts +7 -6
- package/src/test-utils/spawn-claude-session.ts +34 -0
- package/src/test-utils/spawn-grok-session.ts +39 -0
- package/src/test-utils/start-cut-proxy.ts +29 -8
- package/src/test-utils/start-daemon-process.ts +193 -0
- package/src/test-utils/start-git-http-server.ts +28 -4
- package/src/test-utils/start-mcp-stdio.ts +190 -0
- package/src/test-utils/start-output-capture.ts +20 -0
- package/src/test-utils/start-stub-answering-listener.ts +65 -0
- package/src/test-utils/start-stub-closing-listener.ts +31 -0
- package/src/test-utils/start-stub-dropping-daemon.ts +105 -0
- package/src/test-utils/start-stub-impd-info.ts +38 -0
- package/src/test-utils/start-stub-impd.ts +143 -0
- package/src/test-utils/start-stub-legacy-daemon.ts +171 -0
- package/src/test-utils/start-stub-mcp-server.ts +55 -0
- package/src/test-utils/start-stub-recording-listener.ts +47 -0
- package/src/test-utils/start-stub-refusing-listener.ts +43 -0
- package/src/test-utils/start-stub-reporter-socket.ts +79 -0
- package/src/test-utils/start-stub-restore-daemon.ts +59 -0
- package/src/test-utils/start-stub-session-bridge.ts +100 -0
- package/src/test-utils/start-stub-silent-server.ts +38 -0
- package/src/test-utils/start-stub-source-daemon.ts +41 -0
- package/src/test-utils/start-stub-stalled-client.ts +61 -0
- package/src/test-utils/start-stub-stalled-listener.ts +44 -0
- package/src/test-utils/start-stub-tap.ts +38 -0
- package/src/test-utils/start-stub-tcp-daemon.ts +75 -0
- package/src/test-utils/start-stub-telemetry-collector.ts +32 -0
- package/src/test-utils/start-stub-unanswering-listener.ts +41 -0
- package/src/test-utils/start-stub-unanswering-unix-listener.ts +38 -0
- package/src/test-utils/start-test-daemon.ts +229 -0
- package/src/test-utils/start-tui-harness.ts +291 -0
- package/src/test-utils/subscribe-to-socket-lines.ts +81 -14
- package/src/test-utils/try-send-request.ts +29 -0
- package/src/test-utils/wait-for-event.ts +41 -0
- package/src/test-utils/wait-for.ts +15 -3
- package/src/verify-restored-fleet.ts +30 -17
- package/src/workspace/check-repository-access.ts +4 -0
- package/src/workspace/collect-remote-refs.ts +4 -2
- package/src/workspace/run-git.ts +6 -0
- package/src/test-utils/start-legacy-daemon.ts +0 -111
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExecutionCapabilities,
|
|
3
|
+
ExecutionProvider,
|
|
4
|
+
HarnessSpec,
|
|
5
|
+
} from '../daemon/execution-provider';
|
|
6
|
+
import { LocalPTYProvider } from '../daemon/local-pty-provider';
|
|
7
|
+
|
|
8
|
+
interface StubPTYProviderConfig {
|
|
9
|
+
// The provider kind the stand-in reports, such as the kind a target's
|
|
10
|
+
// config selects; the local provider's own kind when absent.
|
|
11
|
+
readonly kind?: string;
|
|
12
|
+
|
|
13
|
+
// Capabilities that differ from the local provider's.
|
|
14
|
+
readonly capabilities?: Partial<ExecutionCapabilities>;
|
|
15
|
+
|
|
16
|
+
// Called with each harness spec before the harness starts.
|
|
17
|
+
readonly onSpawn?: (spec: HarnessSpec) => void;
|
|
18
|
+
readonly prepareHost?: ExecutionProvider['prepareHost'];
|
|
19
|
+
readonly suspendHost?: ExecutionProvider['suspendHost'];
|
|
20
|
+
readonly destroyHost?: ExecutionProvider['destroyHost'];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An execution provider for daemon tests that runs every harness on a real
|
|
25
|
+
* local pseudo-terminal, as the `local-pty` provider does, under whatever
|
|
26
|
+
* kind and capabilities the test gives it, so a target of any kind starts
|
|
27
|
+
* a real process. Each spawn is reported to `onSpawn` first. Host
|
|
28
|
+
* readying, sleeping, and destroying take the test's own operations when
|
|
29
|
+
* given, and the local provider's otherwise: readying always succeeds, and
|
|
30
|
+
* a sleep or a destroy rejects.
|
|
31
|
+
*/
|
|
32
|
+
export function buildStubPTYProvider(config: StubPTYProviderConfig = {}): ExecutionProvider {
|
|
33
|
+
const local = new LocalPTYProvider();
|
|
34
|
+
|
|
35
|
+
const onSpawn = config.onSpawn ?? (() => {});
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
kind: config.kind ?? local.kind,
|
|
39
|
+
remote: false,
|
|
40
|
+
capabilities: { ...local.capabilities, ...config.capabilities },
|
|
41
|
+
prepareHost: config.prepareHost ?? local.prepareHost,
|
|
42
|
+
spawnHarness: (spec) => {
|
|
43
|
+
onSpawn(spec);
|
|
44
|
+
|
|
45
|
+
return local.spawnHarness(spec);
|
|
46
|
+
},
|
|
47
|
+
transferArchive: local.transferArchive,
|
|
48
|
+
runCommand: local.runCommand,
|
|
49
|
+
suspendHost: config.suspendHost ?? local.suspendHost,
|
|
50
|
+
destroyHost: config.destroyHost ?? local.destroyHost,
|
|
51
|
+
dispose: local.dispose,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The script of a stand-in `atc` that records each run: it appends
|
|
3
|
+
* `args:<arguments>`, `session:<$ATC_SESSION_ID>`, and `stdin:<what it
|
|
4
|
+
* read>` to the log, each on its own line, and exits 0.
|
|
5
|
+
*/
|
|
6
|
+
export function buildStubRecordingATC(log: string): string {
|
|
7
|
+
return `#!/bin/sh
|
|
8
|
+
{ printf 'args:%s\\n' "$*"; printf 'session:%s\\n' "$ATC_SESSION_ID"; printf 'stdin:'; cat; printf '\\n'; } >> '${log}'
|
|
9
|
+
`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The script of a stand-in git filter driver that records each run by
|
|
3
|
+
* appending the line `ran` to the record file and passes its input through
|
|
4
|
+
* unchanged, as a smudge filter that changes nothing does, so a test can
|
|
5
|
+
* tell whether git ran a filter the host configured.
|
|
6
|
+
*/
|
|
7
|
+
export function buildStubRecordingFilter(record: string): string {
|
|
8
|
+
return `#!/bin/sh\necho ran >> '${record}'\ncat\n`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The script of a stand-in `git` that records each run by appending the
|
|
3
|
+
* line `ran` to the record file, then exits 0 without doing anything else,
|
|
4
|
+
* so a test can tell whether the code under test ran git at all.
|
|
5
|
+
*/
|
|
6
|
+
export function buildStubRecordingGit(record: string): string {
|
|
7
|
+
return `#!/bin/sh\necho ran >> '${record}'\n`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The script of a stand-in `gh` that is signed in. It appends each command
|
|
3
|
+
* line it runs to `$HOME/gh-argv`. `gh config …` prints `https`. A
|
|
4
|
+
* repository listing of the signed-in account, `gh repo list --limit …`,
|
|
5
|
+
* prints one public repository, `me/dots`, and a listing of an owner,
|
|
6
|
+
* `gh repo list <owner> --limit …`, prints one private repository,
|
|
7
|
+
* `acme/app`, each in the JSON shape `gh repo list` prints.
|
|
8
|
+
*/
|
|
9
|
+
export function buildStubSignedInGH(): string {
|
|
10
|
+
return `#!/bin/sh
|
|
11
|
+
printf '%s\\n' "$*" >> "$HOME/gh-argv"
|
|
12
|
+
case "$1 $3" in
|
|
13
|
+
"config "*) echo https ;;
|
|
14
|
+
"repo --limit") echo '[{"nameWithOwner":"me/dots","description":"dotfiles","isPrivate":false,"url":"https://github.com/me/dots","sshUrl":"git@github.com:me/dots.git"}]' ;;
|
|
15
|
+
*) echo '[{"nameWithOwner":"acme/app","description":"","isPrivate":true,"url":"https://github.com/acme/app","sshUrl":"git@github.com:acme/app.git"}]' ;;
|
|
16
|
+
esac
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The script of a stand-in `gh` that is signed out: every command prints
|
|
3
|
+
* gh's own sign-in hint to stderr and exits 4, as the real one does before
|
|
4
|
+
* `gh auth login`. The hint is gh 2.99.0's text, byte for byte.
|
|
5
|
+
*/
|
|
6
|
+
export function buildStubSignedOutGH(): string {
|
|
7
|
+
return "#!/bin/sh\nprintf '%s\\n%s\\n' 'To get started with GitHub CLI, please run: gh auth login' 'Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.' >&2\nexit 4\n";
|
|
8
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExecutionProvider, HarnessHandle } from '../daemon/execution-provider';
|
|
2
|
+
import { LocalPTYProvider } from '../daemon/local-pty-provider';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An execution provider of kind `no-forced-kill` that runs every harness on
|
|
6
|
+
* this machine through the local terminal provider, but whose harnesses
|
|
7
|
+
* have no forced kill to send, as a remote provider's have none: a harness
|
|
8
|
+
* that ignores its kill keeps running.
|
|
9
|
+
*/
|
|
10
|
+
export function buildStubSoftKillProvider(): ExecutionProvider {
|
|
11
|
+
const local = new LocalPTYProvider();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
kind: 'no-forced-kill',
|
|
15
|
+
remote: false,
|
|
16
|
+
capabilities: local.capabilities,
|
|
17
|
+
prepareHost: local.prepareHost,
|
|
18
|
+
spawnHarness: (spec): HarnessHandle => {
|
|
19
|
+
const { killForced: _killForced, ...handle } = local.spawnHarness(spec);
|
|
20
|
+
|
|
21
|
+
return handle;
|
|
22
|
+
},
|
|
23
|
+
transferArchive: local.transferArchive,
|
|
24
|
+
runCommand: local.runCommand,
|
|
25
|
+
suspendHost: local.suspendHost,
|
|
26
|
+
destroyHost: local.destroyHost,
|
|
27
|
+
dispose: local.dispose,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ExecutionTarget } from '../daemon/build-execution-targets';
|
|
2
|
+
import { buildTargetIdentity } from '../daemon/build-target-identity';
|
|
3
|
+
import type { TargetConfig } from '../shared/collect-targets';
|
|
4
|
+
import { buildStubPTYProvider } from './build-stub-pty-provider';
|
|
5
|
+
|
|
6
|
+
// What a target's provider does in place of the local one's: the
|
|
7
|
+
// capabilities it adds and the host preparation, sleep, and destroy it runs.
|
|
8
|
+
type HostOverride = Omit<
|
|
9
|
+
NonNullable<Parameters<typeof buildStubPTYProvider>[0]>,
|
|
10
|
+
'kind' | 'onSpawn'
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
interface StubTargetsConfig {
|
|
14
|
+
// Each spawn appends its target's id here, in order.
|
|
15
|
+
readonly spawned: string[];
|
|
16
|
+
|
|
17
|
+
// Host operations of their own for the targets they name.
|
|
18
|
+
readonly hosts?: Readonly<Record<string, HostOverride>>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The daemon's execution targets for configured target entries, as the
|
|
23
|
+
* config load or the targets section parse gives them, in their order:
|
|
24
|
+
* each `local-pty` entry runs its harnesses on a real local
|
|
25
|
+
* pseudo-terminal through a stand-in provider that appends the entry's id
|
|
26
|
+
* to `spawned` on every spawn and takes the host operations `hosts` gives
|
|
27
|
+
* it, and an entry of any other kind has no provider, as a provider this
|
|
28
|
+
* daemon lacks leaves it. Each target's identity is the real one for its
|
|
29
|
+
* kind and options.
|
|
30
|
+
*/
|
|
31
|
+
export function buildStubTargets(
|
|
32
|
+
targets: readonly TargetConfig[],
|
|
33
|
+
|
|
34
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- the stand-in appends to the caller's spawn list
|
|
35
|
+
config: StubTargetsConfig,
|
|
36
|
+
): ExecutionTarget[] {
|
|
37
|
+
const hosts: Readonly<Record<string, HostOverride>> = { ...config.hosts };
|
|
38
|
+
|
|
39
|
+
const providers = new Map(
|
|
40
|
+
targets.map((target) => [
|
|
41
|
+
target.id,
|
|
42
|
+
buildStubPTYProvider({
|
|
43
|
+
onSpawn: () => {
|
|
44
|
+
config.spawned.push(target.id);
|
|
45
|
+
},
|
|
46
|
+
...hosts[target.id],
|
|
47
|
+
}),
|
|
48
|
+
]),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return targets.map((target) => ({
|
|
52
|
+
id: target.id,
|
|
53
|
+
kind: target.provider,
|
|
54
|
+
options: target.options,
|
|
55
|
+
identity: buildTargetIdentity(target.provider, target.options),
|
|
56
|
+
provider: target.provider === 'local-pty' ? (providers.get(target.id) ?? null) : null,
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A terminal that keeps every chunk written to it, in order, and draws
|
|
3
|
+
* nothing. `getText` returns everything written. `mark` returns a position
|
|
4
|
+
* at the end of what is written so far, and `getTextSince` returns only
|
|
5
|
+
* what was written after that position, so a test reads only the output of
|
|
6
|
+
* the action it checks.
|
|
7
|
+
*/
|
|
8
|
+
export function buildStubTerminal() {
|
|
9
|
+
const chunks: string[] = [];
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
write: (chunk: string): void => {
|
|
13
|
+
chunks.push(chunk);
|
|
14
|
+
},
|
|
15
|
+
getText: (): string => chunks.join(''),
|
|
16
|
+
mark: (): number => chunks.length,
|
|
17
|
+
getTextSince: (mark: number): string => chunks.slice(mark).join(''),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// One timer the scheduler started: its delay, what it runs, and whether it
|
|
2
|
+
// is still pending, has run, or was cancelled.
|
|
3
|
+
interface StubTimer {
|
|
4
|
+
readonly ms: number;
|
|
5
|
+
readonly onTimeout: () => void;
|
|
6
|
+
state: 'pending' | 'ran' | 'cancelled';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A timeout scheduler for code that takes one in place of `setTimeout`: no
|
|
11
|
+
* timer ever runs on its own. `schedule` records the timer and returns what
|
|
12
|
+
* cancels it, as a real scheduler does; `runTimer` runs the oldest pending
|
|
13
|
+
* timer of the given delay and throws when none is pending, so a test both
|
|
14
|
+
* proves the code asked for that delay and moves time past it.
|
|
15
|
+
* `collectDelays` returns the delay of every timer in the order the code
|
|
16
|
+
* started them, and `collectPendingDelays` those neither run nor cancelled.
|
|
17
|
+
*/
|
|
18
|
+
export function buildStubTimeoutScheduler() {
|
|
19
|
+
const timers: StubTimer[] = [];
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
schedule: (onTimeout: () => void, ms: number): (() => void) => {
|
|
23
|
+
const timer: StubTimer = { ms, onTimeout, state: 'pending' };
|
|
24
|
+
|
|
25
|
+
timers.push(timer);
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
timer.state = timer.state === 'pending' ? 'cancelled' : timer.state;
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
runTimer: (ms: number): void => {
|
|
32
|
+
const timer = timers.find(
|
|
33
|
+
(candidate) => candidate.state === 'pending' && candidate.ms === ms,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (timer === undefined) {
|
|
37
|
+
throw new Error(`no pending timer of ${ms} ms`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
timer.state = 'ran';
|
|
41
|
+
|
|
42
|
+
timer.onTimeout();
|
|
43
|
+
},
|
|
44
|
+
collectDelays: (): number[] => timers.map((timer) => timer.ms),
|
|
45
|
+
collectPendingDelays: (): number[] =>
|
|
46
|
+
timers.filter((timer) => timer.state === 'pending').map((timer) => timer.ms),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
const CLI_PATH = join(import.meta.dir, '..', 'cli.ts');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The script of a stand-in `claude` for the TUI suites. It prints
|
|
7
|
+
* `FAKE_CLAUDE_UP args: ` and its arguments at start and again on every
|
|
8
|
+
* SIGWINCH, as the real one repaints on a resize. It reports `SessionStart`
|
|
9
|
+
* through the real reporter with session id `fake-1` and transcript
|
|
10
|
+
* `$HOME/fake-transcript.jsonl`, then a permission `Notification`, and
|
|
11
|
+
* echoes each line it reads as `GOT:<line>` for five minutes. Scenario files
|
|
12
|
+
* in `$HOME` change it: `fake-claude-events.jsonl` replaces the
|
|
13
|
+
* notification with its hook lines, and a run with `--resume` among its
|
|
14
|
+
* arguments touches `fake-claude-resume-held` and waits, before it prints
|
|
15
|
+
* or reports anything, while `fake-claude-hold-resume` exists.
|
|
16
|
+
*/
|
|
17
|
+
export function buildStubTUIClaude(): string {
|
|
18
|
+
const report = `"${process.execPath}" "${CLI_PATH}" hook-report`;
|
|
19
|
+
|
|
20
|
+
return `#!/usr/bin/env bash
|
|
21
|
+
ARGS="$*"
|
|
22
|
+
case "$ARGS" in *--resume*)
|
|
23
|
+
if [ -f "$HOME/fake-claude-hold-resume" ]; then
|
|
24
|
+
touch "$HOME/fake-claude-resume-held"
|
|
25
|
+
while [ -f "$HOME/fake-claude-hold-resume" ]; do sleep 0.05; done
|
|
26
|
+
fi ;;
|
|
27
|
+
esac
|
|
28
|
+
paint() { echo "FAKE_CLAUDE_UP args: $ARGS"; }
|
|
29
|
+
trap paint WINCH
|
|
30
|
+
paint
|
|
31
|
+
printf '{"hook_event_name":"SessionStart","session_id":"fake-1","transcript_path":"'"$HOME"'/fake-transcript.jsonl"}' | ${report}
|
|
32
|
+
if [ -f "$HOME/fake-claude-events.jsonl" ]; then
|
|
33
|
+
while IFS= read -r ev; do
|
|
34
|
+
[ -n "$ev" ] || continue
|
|
35
|
+
sleep 0.3
|
|
36
|
+
printf '%s' "$ev" | ${report}
|
|
37
|
+
done < "$HOME/fake-claude-events.jsonl"
|
|
38
|
+
else
|
|
39
|
+
sleep 0.3
|
|
40
|
+
printf '{"hook_event_name":"Notification","session_id":"fake-1","message":"needs permission"}' | ${report}
|
|
41
|
+
fi
|
|
42
|
+
# bash 3.2 read -t takes whole seconds; a fraction times out immediately
|
|
43
|
+
for _ in $(seq 1 300); do
|
|
44
|
+
if read -t 1 -r line; then echo "GOT:$line"; fi
|
|
45
|
+
done
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
const CLI_PATH = join(import.meta.dir, '..', 'cli.ts');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The script of a stand-in `grok` for the TUI suites. It prints
|
|
7
|
+
* `FAKE_GROK_UP args: ` and its arguments at start and again on every
|
|
8
|
+
* SIGWINCH, reports `session_start` through the real reporter with session
|
|
9
|
+
* id `fake-grok-1` and its working directory, then a `permission_prompt`
|
|
10
|
+
* notification, prints `FAKE_GROK_HOOKS_DONE`, and echoes each line it
|
|
11
|
+
* reads as `GOT:<line>` for five minutes. Scenario files in `$HOME` change
|
|
12
|
+
* it: `fake-grok-events.jsonl` replaces the notification with its hook
|
|
13
|
+
* lines, `fake-grok-hold-start` skips every report, and while
|
|
14
|
+
* `fake-grok-defer-start` exists it touches `fake-grok-start-deferred` and
|
|
15
|
+
* waits before it reports.
|
|
16
|
+
*/
|
|
17
|
+
export function buildStubTUIGrok(): string {
|
|
18
|
+
const report = `"${process.execPath}" "${CLI_PATH}" hook-report`;
|
|
19
|
+
|
|
20
|
+
return `#!/usr/bin/env bash
|
|
21
|
+
ARGS="$*"
|
|
22
|
+
paint() { echo "FAKE_GROK_UP args: $ARGS"; }
|
|
23
|
+
trap paint WINCH
|
|
24
|
+
paint
|
|
25
|
+
idle() {
|
|
26
|
+
for _ in $(seq 1 300); do
|
|
27
|
+
if read -t 1 -r line; then echo "GOT:$line"; fi
|
|
28
|
+
done
|
|
29
|
+
}
|
|
30
|
+
if [ -f "$HOME/fake-grok-hold-start" ]; then
|
|
31
|
+
idle
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
if [ -f "$HOME/fake-grok-defer-start" ]; then
|
|
35
|
+
touch "$HOME/fake-grok-start-deferred"
|
|
36
|
+
while [ -f "$HOME/fake-grok-defer-start" ]; do sleep 0.05; done
|
|
37
|
+
fi
|
|
38
|
+
printf '{"hookEventName":"session_start","sessionId":"fake-grok-1","cwd":"%s"}' "$PWD" | ${report}
|
|
39
|
+
if [ -f "$HOME/fake-grok-events.jsonl" ]; then
|
|
40
|
+
sleep 0.3
|
|
41
|
+
while IFS= read -r ev; do
|
|
42
|
+
[ -n "$ev" ] || continue
|
|
43
|
+
printf '%s' "$ev" | ${report}
|
|
44
|
+
sleep 0.2
|
|
45
|
+
done < "$HOME/fake-grok-events.jsonl"
|
|
46
|
+
else
|
|
47
|
+
sleep 0.3
|
|
48
|
+
printf '{"hookEventName":"notification","sessionId":"fake-grok-1","notificationType":"permission_prompt","message":"allow edit?"}' | ${report}
|
|
49
|
+
fi
|
|
50
|
+
echo "FAKE_GROK_HOOKS_DONE"
|
|
51
|
+
idle
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A clock for a polling wait that moves only when the wait waits it out:
|
|
3
|
+
* `now` starts at zero, and `wait` records the milliseconds it was asked to
|
|
4
|
+
* wait in `waits`, moves `now` forward by them, and resolves at once. A
|
|
5
|
+
* deadline then passes after a known number of attempts, with no real time
|
|
6
|
+
* spent.
|
|
7
|
+
*/
|
|
8
|
+
export function buildStubWaitClock() {
|
|
9
|
+
const waits: number[] = [];
|
|
10
|
+
let elapsed = 0;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
waits,
|
|
14
|
+
now: () => elapsed,
|
|
15
|
+
wait: (ms: number): Promise<void> => {
|
|
16
|
+
waits.push(ms);
|
|
17
|
+
|
|
18
|
+
elapsed += ms;
|
|
19
|
+
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { buildTargetIdentity } from '../daemon/build-target-identity';
|
|
2
|
+
import type { DaemonOptions } from '../daemon/daemon';
|
|
3
|
+
import type { ExecutionProvider } from '../daemon/execution-provider';
|
|
4
|
+
import { collectTargets } from '../shared/collect-targets';
|
|
5
|
+
|
|
6
|
+
interface RawTargetConfig {
|
|
7
|
+
// The raw `targets` key of a config file.
|
|
8
|
+
readonly targets?: unknown;
|
|
9
|
+
|
|
10
|
+
// The raw `defaultTarget` key of a config file.
|
|
11
|
+
readonly defaultTarget?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Turns the raw target keys of a config file into daemon options through
|
|
16
|
+
* the real parse. Each parsed target takes the provider that the factory
|
|
17
|
+
* for its kind builds for its id, or no provider when no factory serves its
|
|
18
|
+
* kind, and every parse error passes through as a target config error.
|
|
19
|
+
*/
|
|
20
|
+
export function buildTargetOptionsFromConfig(
|
|
21
|
+
raw: RawTargetConfig,
|
|
22
|
+
factories: ReadonlyMap<string, (id: string) => ExecutionProvider>,
|
|
23
|
+
): Required<Pick<DaemonOptions, 'targets' | 'defaultTarget' | 'targetErrors'>> {
|
|
24
|
+
const parsed = collectTargets(raw.targets, raw.defaultTarget);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
targets: parsed.targets.map((target) => ({
|
|
28
|
+
id: target.id,
|
|
29
|
+
kind: target.provider,
|
|
30
|
+
options: target.options,
|
|
31
|
+
identity: buildTargetIdentity(target.provider, target.options),
|
|
32
|
+
provider: factories.get(target.provider)?.(target.id) ?? null,
|
|
33
|
+
})),
|
|
34
|
+
defaultTarget: parsed.defaultTarget,
|
|
35
|
+
targetErrors: parsed.errors,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface TreeProcess {
|
|
5
|
+
readonly pid: number;
|
|
6
|
+
readonly argv: readonly string[];
|
|
7
|
+
readonly env: Readonly<Record<string, string>>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Collects the argv and environment of every live process descended from
|
|
12
|
+
* the root process, read from `/proc`, so a test can check what a child
|
|
13
|
+
* was started with while it still runs. The root itself is left out. A
|
|
14
|
+
* process that exits during the scan is left out too. Linux only, since
|
|
15
|
+
* only Linux has `/proc`.
|
|
16
|
+
*/
|
|
17
|
+
export async function collectProcessTree(rootPID: number): Promise<TreeProcess[]> {
|
|
18
|
+
const entries = await readdir('/proc');
|
|
19
|
+
|
|
20
|
+
const pids = entries.filter((entry) => /^\d+$/u.test(entry));
|
|
21
|
+
|
|
22
|
+
const parents = new Map<string, string>();
|
|
23
|
+
|
|
24
|
+
for (const pid of pids) {
|
|
25
|
+
const stat = await readFile(join('/proc', pid, 'stat'), 'utf8').catch(() => '');
|
|
26
|
+
|
|
27
|
+
// The fields after the parenthesised command name start with the state,
|
|
28
|
+
// then the parent pid.
|
|
29
|
+
parents.set(pid, stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1] ?? '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const root = String(rootPID);
|
|
33
|
+
const tree: TreeProcess[] = [];
|
|
34
|
+
|
|
35
|
+
for (const pid of pids) {
|
|
36
|
+
if (!isDescendant(parents, pid, root)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cmdline = await readFile(join('/proc', pid, 'cmdline'), 'utf8').catch(() => null);
|
|
41
|
+
const environ = await readFile(join('/proc', pid, 'environ'), 'utf8').catch(() => null);
|
|
42
|
+
|
|
43
|
+
if (cmdline === null || environ === null) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
tree.push({
|
|
48
|
+
pid: Number(pid),
|
|
49
|
+
argv: splitNullTerminated(cmdline),
|
|
50
|
+
env: Object.fromEntries(
|
|
51
|
+
splitNullTerminated(environ).map((entry) => {
|
|
52
|
+
const equals = entry.indexOf('=');
|
|
53
|
+
|
|
54
|
+
return [entry.slice(0, equals), entry.slice(equals + 1)];
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return tree;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isDescendant(parents: ReadonlyMap<string, string>, pid: string, root: string): boolean {
|
|
64
|
+
let ancestor = parents.get(pid);
|
|
65
|
+
|
|
66
|
+
while (ancestor !== undefined && ancestor !== root) {
|
|
67
|
+
ancestor = parents.get(ancestor);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return ancestor === root;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function splitNullTerminated(text: string): string[] {
|
|
74
|
+
return text.split('\0').filter((entry) => entry !== '');
|
|
75
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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. The directory is removed once the current test finishes,
|
|
26
|
+
* so it must run inside a test; disposal removes it sooner, and a second
|
|
27
|
+
* removal does nothing.
|
|
28
|
+
*/
|
|
29
|
+
export async function createGitFixture(config: GitFixtureConfig = {}) {
|
|
30
|
+
const env = buildGitEnv(process.env);
|
|
31
|
+
|
|
32
|
+
const template = await resolveTemplate();
|
|
33
|
+
|
|
34
|
+
const tmp = setupTempDir(config.prefix ?? 'atc-git-fixture-');
|
|
35
|
+
const upstream = join(tmp.dir, 'upstream.git');
|
|
36
|
+
const work = join(tmp.dir, 'work');
|
|
37
|
+
|
|
38
|
+
cpSync(template.dir, tmp.dir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
// The copied clone's config still holds the template's upstream as its
|
|
41
|
+
// origin; it is pointed at this fixture's own upstream instead, so a push
|
|
42
|
+
// never reaches the template or another fixture.
|
|
43
|
+
const gitConfig = join(work, '.git', 'config');
|
|
44
|
+
|
|
45
|
+
writeFileSync(
|
|
46
|
+
gitConfig,
|
|
47
|
+
readFileSync(gitConfig, 'utf8').replace(
|
|
48
|
+
`url = ${join(template.dir, 'upstream.git')}`,
|
|
49
|
+
`url = ${upstream}`,
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
dir: tmp.dir,
|
|
55
|
+
env,
|
|
56
|
+
upstream,
|
|
57
|
+
work,
|
|
58
|
+
sha: template.sha,
|
|
59
|
+
[Symbol.asyncDispose]: tmp[Symbol.asyncDispose],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildGitEnv(
|
|
64
|
+
source: Readonly<Record<string, string | undefined>>,
|
|
65
|
+
): Record<string, string | undefined> {
|
|
66
|
+
return {
|
|
67
|
+
...Object.fromEntries(Object.entries(source).filter(([name]) => !name.startsWith('GIT_'))),
|
|
68
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
69
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface GitTemplate {
|
|
74
|
+
readonly dir: string;
|
|
75
|
+
readonly sha: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The template pair, built once per process.
|
|
79
|
+
let template: Promise<GitTemplate> | null = null;
|
|
80
|
+
|
|
81
|
+
function resolveTemplate(): Promise<GitTemplate> {
|
|
82
|
+
template ??= createTemplate();
|
|
83
|
+
|
|
84
|
+
return template;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The template lives under the test home, which the test script removes
|
|
88
|
+
// when the run ends.
|
|
89
|
+
async function createTemplate(): Promise<GitTemplate> {
|
|
90
|
+
const env = buildGitEnv(process.env);
|
|
91
|
+
const home = process.env['ATC_TEST_HOME'];
|
|
92
|
+
|
|
93
|
+
if (home === undefined) {
|
|
94
|
+
throw new Error('a git fixture needs the test home; run `bun run test`');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const dir = mkdtempSync(join(home, 'git-fixture-template-'));
|
|
98
|
+
const upstream = join(dir, 'upstream.git');
|
|
99
|
+
const work = join(dir, 'work');
|
|
100
|
+
|
|
101
|
+
await $`git init --quiet --bare --template= --initial-branch=main ${upstream}`.env(env).quiet();
|
|
102
|
+
await $`git clone --quiet --template= ${upstream} ${work}`.env(env).quiet();
|
|
103
|
+
await $`git config user.name atc`.env(env).cwd(work).quiet();
|
|
104
|
+
await $`git config user.email atc@example.com`.env(env).cwd(work).quiet();
|
|
105
|
+
await $`git config commit.gpgsign false`.env(env).cwd(work).quiet();
|
|
106
|
+
await $`git config tag.gpgsign false`.env(env).cwd(work).quiet();
|
|
107
|
+
|
|
108
|
+
writeFileSync(join(work, 'README.md'), 'hello\n');
|
|
109
|
+
|
|
110
|
+
await $`git add README.md`.env(env).cwd(work).quiet();
|
|
111
|
+
await $`git commit --quiet -m initial`.env(env).cwd(work).quiet();
|
|
112
|
+
await $`git push --quiet origin main`.env(env).cwd(work).quiet();
|
|
113
|
+
|
|
114
|
+
const head = await $`git rev-parse HEAD`.env(env).cwd(work).quiet().text();
|
|
115
|
+
|
|
116
|
+
return { dir, sha: head.trim() };
|
|
117
|
+
}
|
|
@@ -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
|
|