@zgeoff/atc 3.3.1 → 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 +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,26 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { DaemonRecord } from '../shared/find-daemon-record';
|
|
3
|
+
import { mergeDeep } from './merge-deep';
|
|
4
|
+
import type { MockOverrides } from './mock-overrides';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The record a daemon writes beside its state directory, for a daemon with
|
|
8
|
+
* no events socket and no TCP listener. The pid and the socket paths are
|
|
9
|
+
* arbitrary. Overrides replace the defaults field by field.
|
|
10
|
+
*/
|
|
11
|
+
export function buildMockDaemonRecord(
|
|
12
|
+
overrides: MockOverrides<DaemonRecord, keyof DaemonRecord> = {},
|
|
13
|
+
): DaemonRecord {
|
|
14
|
+
const dir = faker.system.directoryPath();
|
|
15
|
+
|
|
16
|
+
return mergeDeep<DaemonRecord>(
|
|
17
|
+
{
|
|
18
|
+
pid: faker.number.int({ min: 2, max: 4_194_304 }),
|
|
19
|
+
socketPath: `${dir}/atc-daemon.sock`,
|
|
20
|
+
reporterSocketPath: `${dir}/atc.sock`,
|
|
21
|
+
eventsSocketPath: null,
|
|
22
|
+
listenPort: null,
|
|
23
|
+
},
|
|
24
|
+
overrides,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
3
|
+
import { toSessionID } from '../shared/to-session-id';
|
|
4
|
+
import type { FleetEntry } from '../store/fleet-entry';
|
|
5
|
+
import { mergeDeep } from './merge-deep';
|
|
6
|
+
import type { MockOverrides } from './mock-overrides';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The fields the defaults set; every other field is absent until an
|
|
10
|
+
* override gives it whole.
|
|
11
|
+
*/
|
|
12
|
+
type DefaultedKey = 'sessionID' | 'name' | 'cwd' | 'agentSessionID' | 'agent';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A fleet row for a live Claude session on the local target: a fresh
|
|
16
|
+
* session id and a fresh agent session id, so it restores as resumable,
|
|
17
|
+
* with an arbitrary name and directory and every optional field absent.
|
|
18
|
+
* Overrides merge into fresh defaults at every depth.
|
|
19
|
+
*/
|
|
20
|
+
export function buildMockFleetEntry(
|
|
21
|
+
overrides: MockOverrides<FleetEntry, DefaultedKey> = {},
|
|
22
|
+
): FleetEntry {
|
|
23
|
+
return mergeDeep<FleetEntry, DefaultedKey>(
|
|
24
|
+
{
|
|
25
|
+
sessionID: toSessionID(faker.string.uuid()),
|
|
26
|
+
name: faker.word.noun(),
|
|
27
|
+
cwd: faker.system.directoryPath(),
|
|
28
|
+
agentSessionID: toAgentSessionID(faker.string.uuid()),
|
|
29
|
+
agent: 'claude',
|
|
30
|
+
},
|
|
31
|
+
overrides,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { GatewayConfig } from '../shared/collect-gateways';
|
|
3
|
+
import { mergeDeep } from './merge-deep';
|
|
4
|
+
import type { MockOverrides } from './mock-overrides';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The fields the defaults set; every other field is absent until an
|
|
8
|
+
* override gives it whole.
|
|
9
|
+
*/
|
|
10
|
+
type DefaultedKey = 'id' | 'label' | 'mark' | 'bin' | 'args' | 'baseURL' | 'env';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A Claude-compatible backend with no arguments, no environment, no helper
|
|
14
|
+
* command, no settings, and no auth. The id, label, mark, binary, and base
|
|
15
|
+
* URL are arbitrary. Overrides merge into fresh defaults at every depth.
|
|
16
|
+
*/
|
|
17
|
+
export function buildMockGatewayConfig(
|
|
18
|
+
overrides: MockOverrides<GatewayConfig, DefaultedKey> = {},
|
|
19
|
+
): GatewayConfig {
|
|
20
|
+
return mergeDeep<GatewayConfig, DefaultedKey>(
|
|
21
|
+
{
|
|
22
|
+
id: faker.string.alpha({ length: 8, casing: 'lower' }),
|
|
23
|
+
label: faker.company.name(),
|
|
24
|
+
mark: faker.string.alpha({ length: 1, casing: 'lower' }),
|
|
25
|
+
bin: faker.system.filePath(),
|
|
26
|
+
args: [],
|
|
27
|
+
baseURL: faker.internet.url(),
|
|
28
|
+
env: {},
|
|
29
|
+
},
|
|
30
|
+
overrides,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { ImpIdentity } from '../daemon/imp-port';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An impd token identity with an arbitrary name that manages the imps
|
|
6
|
+
* matching `atc-*` and may grant no secret. An override replaces the field
|
|
7
|
+
* it names.
|
|
8
|
+
*/
|
|
9
|
+
export function buildMockImpIdentity(overrides: Partial<ImpIdentity> = {}): ImpIdentity {
|
|
10
|
+
return {
|
|
11
|
+
kind: 'token',
|
|
12
|
+
name: faker.word.noun(),
|
|
13
|
+
scope: 'manage',
|
|
14
|
+
imps: ['atc-*'],
|
|
15
|
+
grantable: [],
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { GatewayStore } from '../federation/gateway-store';
|
|
3
|
+
import { mergeDeep } from './merge-deep';
|
|
4
|
+
import type { MockOverrides } from './mock-overrides';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The binding a keyed request claims in the gateway store, before any
|
|
8
|
+
* outcome or send is recorded for it.
|
|
9
|
+
*/
|
|
10
|
+
type KeyBindingClaim = Parameters<GatewayStore['claimBinding']>[0];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A claim of a keyed spawn by an arbitrary principal under an arbitrary
|
|
14
|
+
* key, bound to an arbitrary daemon that announced an arbitrary retention,
|
|
15
|
+
* with an arbitrary payload hash and claim id. Overrides replace the
|
|
16
|
+
* defaults field by field.
|
|
17
|
+
*/
|
|
18
|
+
export function buildMockKeyBindingClaim(
|
|
19
|
+
overrides: MockOverrides<KeyBindingClaim, keyof KeyBindingClaim> = {},
|
|
20
|
+
): KeyBindingClaim {
|
|
21
|
+
return mergeDeep<KeyBindingClaim>(
|
|
22
|
+
{
|
|
23
|
+
principal: faker.string.alphanumeric(12),
|
|
24
|
+
operation: 'session.spawn',
|
|
25
|
+
key: faker.string.alphanumeric(16),
|
|
26
|
+
daemon: faker.string.alpha({ length: 8, casing: 'lower' }),
|
|
27
|
+
daemonID: faker.string.uuid(),
|
|
28
|
+
retentionMs: faker.number.int({ min: 1000, max: 86_400_000 }),
|
|
29
|
+
payloadHash: faker.string.hexadecimal({ length: 64, casing: 'lower', prefix: '' }),
|
|
30
|
+
claimID: faker.string.uuid(),
|
|
31
|
+
},
|
|
32
|
+
overrides,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { toMessageID } from '../shared/to-message-id';
|
|
3
|
+
import { toSessionID } from '../shared/to-session-id';
|
|
4
|
+
import type { MessageRecord } from '../store/message-record';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A message accepted for a session and not yet delivered: a fresh message
|
|
8
|
+
* id and a fresh atc id, an arbitrary sender, text, and send time, and no
|
|
9
|
+
* agent session id, delivery, answer, or turn. An override replaces the
|
|
10
|
+
* field it names.
|
|
11
|
+
*/
|
|
12
|
+
export function buildMockMessageRecord(overrides: Partial<MessageRecord> = {}): MessageRecord {
|
|
13
|
+
return {
|
|
14
|
+
id: toMessageID(faker.string.uuid()),
|
|
15
|
+
atcID: toSessionID(faker.string.uuid()),
|
|
16
|
+
from: faker.person.firstName(),
|
|
17
|
+
text: faker.lorem.sentence(),
|
|
18
|
+
status: 'accepted',
|
|
19
|
+
sentAt: faker.date.past().getTime(),
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { MirrorSession } from '../client/to-mirror-session';
|
|
3
|
+
import { mergeDeep } from './merge-deep';
|
|
4
|
+
import type { MockOverrides } from './mock-overrides';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A client's mirror of a live, running Claude session in a local terminal
|
|
8
|
+
* on the `local` target: read, unpinned, not resumable, unable to eject,
|
|
9
|
+
* with no parent and the agent's default model. The id, name, directory,
|
|
10
|
+
* last message, and times are arbitrary. Overrides replace the defaults
|
|
11
|
+
* field by field.
|
|
12
|
+
*/
|
|
13
|
+
export function buildMockMirrorSession(
|
|
14
|
+
overrides: MockOverrides<MirrorSession, keyof MirrorSession> = {},
|
|
15
|
+
): MirrorSession {
|
|
16
|
+
const cwd = faker.system.directoryPath();
|
|
17
|
+
|
|
18
|
+
return mergeDeep<MirrorSession>(
|
|
19
|
+
{
|
|
20
|
+
id: faker.string.uuid(),
|
|
21
|
+
name: faker.word.noun(),
|
|
22
|
+
cwd,
|
|
23
|
+
pinned: false,
|
|
24
|
+
lastAttachedAt: faker.date.recent().getTime(),
|
|
25
|
+
repoRoot: cwd,
|
|
26
|
+
state: 'running',
|
|
27
|
+
unread: false,
|
|
28
|
+
lastMsg: faker.lorem.sentence(),
|
|
29
|
+
createdAt: faker.date.past().getTime(),
|
|
30
|
+
kind: 'pty',
|
|
31
|
+
alive: true,
|
|
32
|
+
resumable: false,
|
|
33
|
+
canEject: false,
|
|
34
|
+
agent: 'claude',
|
|
35
|
+
parent: null,
|
|
36
|
+
target: 'local',
|
|
37
|
+
model: null,
|
|
38
|
+
harness: 'running',
|
|
39
|
+
},
|
|
40
|
+
overrides,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { RegistryDaemon } from '../federation/types';
|
|
3
|
+
import { mergeDeep } from './merge-deep';
|
|
4
|
+
import type { MockOverrides } from './mock-overrides';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A registry daemon with an arbitrary name, address, pinned daemon id,
|
|
8
|
+
* and token. Its incarnation is the first 8 characters of the
|
|
9
|
+
* daemon id in play, the override's when one is given, so a gateway id
|
|
10
|
+
* built from it routes back unless an override sets the incarnation too.
|
|
11
|
+
* Overrides merge into fresh defaults at every depth.
|
|
12
|
+
*/
|
|
13
|
+
export function buildMockRegistryDaemon(
|
|
14
|
+
overrides: MockOverrides<RegistryDaemon, keyof RegistryDaemon> = {},
|
|
15
|
+
): RegistryDaemon {
|
|
16
|
+
const daemonID = overrides.daemonID ?? faker.string.uuid();
|
|
17
|
+
|
|
18
|
+
return mergeDeep<RegistryDaemon>(
|
|
19
|
+
{
|
|
20
|
+
name: faker.string.alpha({ length: 8, casing: 'lower' }),
|
|
21
|
+
address: { host: faker.internet.ipv4(), port: faker.internet.port() },
|
|
22
|
+
daemonID,
|
|
23
|
+
incarnation: daemonID.slice(0, 8),
|
|
24
|
+
token: faker.string.hexadecimal({ length: 32, casing: 'lower', prefix: '' }),
|
|
25
|
+
},
|
|
26
|
+
overrides,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { SessionDescriptor } from '../daemon/sessions';
|
|
3
|
+
import { toDaemonID } from '../shared/to-daemon-id';
|
|
4
|
+
import { toSessionID } from '../shared/to-session-id';
|
|
5
|
+
import { mergeDeep } from './merge-deep';
|
|
6
|
+
import type { MockOverrides } from './mock-overrides';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The fields the defaults set; every other field is absent until an
|
|
10
|
+
* override gives it whole.
|
|
11
|
+
*/
|
|
12
|
+
type DefaultedKey =
|
|
13
|
+
| 'id'
|
|
14
|
+
| 'name'
|
|
15
|
+
| 'cwd'
|
|
16
|
+
| 'state'
|
|
17
|
+
| 'unread'
|
|
18
|
+
| 'lastMsg'
|
|
19
|
+
| 'agent'
|
|
20
|
+
| 'pinned'
|
|
21
|
+
| 'lastAttachedAt'
|
|
22
|
+
| 'repoRoot'
|
|
23
|
+
| 'namedBy'
|
|
24
|
+
| 'createdAt'
|
|
25
|
+
| 'kind'
|
|
26
|
+
| 'alive'
|
|
27
|
+
| 'canEject'
|
|
28
|
+
| 'locator'
|
|
29
|
+
| 'lifecycle';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The wire view of a live, running Claude session in a local terminal on
|
|
33
|
+
* the `local` target, as `session.list` and the session events carry it:
|
|
34
|
+
* read, unpinned, named by the user, with no parent, workspace, model, or
|
|
35
|
+
* agent session id. The id, name, directories, last message, times, and
|
|
36
|
+
* daemon id are arbitrary. Overrides merge into fresh defaults at every
|
|
37
|
+
* depth.
|
|
38
|
+
*/
|
|
39
|
+
export function buildMockSessionDescriptor(
|
|
40
|
+
overrides: MockOverrides<SessionDescriptor, DefaultedKey> = {},
|
|
41
|
+
): SessionDescriptor {
|
|
42
|
+
const cwd = faker.system.directoryPath();
|
|
43
|
+
|
|
44
|
+
return mergeDeep<SessionDescriptor, DefaultedKey>(
|
|
45
|
+
{
|
|
46
|
+
id: toSessionID(faker.string.uuid()),
|
|
47
|
+
name: faker.word.noun(),
|
|
48
|
+
cwd,
|
|
49
|
+
state: 'running',
|
|
50
|
+
unread: false,
|
|
51
|
+
lastMsg: faker.lorem.sentence(),
|
|
52
|
+
agent: 'claude',
|
|
53
|
+
pinned: false,
|
|
54
|
+
lastAttachedAt: faker.date.recent().getTime(),
|
|
55
|
+
repoRoot: cwd,
|
|
56
|
+
namedBy: 'user',
|
|
57
|
+
createdAt: faker.date.past().getTime(),
|
|
58
|
+
kind: 'pty',
|
|
59
|
+
alive: true,
|
|
60
|
+
canEject: false,
|
|
61
|
+
locator: { daemonID: toDaemonID(faker.string.uuid()), targetID: 'local' },
|
|
62
|
+
lifecycle: { desired: 'run', vm: 'none', harness: 'running', attachment: 'local' },
|
|
63
|
+
},
|
|
64
|
+
overrides,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { Session } from '../daemon/sessions';
|
|
3
|
+
import { toSessionID } from '../shared/to-session-id';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A session manager's record of an exited Claude session on the `local`
|
|
7
|
+
* target with no terminal: read, unpinned, auto-named, with no parent,
|
|
8
|
+
* withheld env, workspace, model, or agent session id, and its own host.
|
|
9
|
+
* The id, name, directories, and times are arbitrary. An override replaces
|
|
10
|
+
* the field it names.
|
|
11
|
+
*/
|
|
12
|
+
export function buildMockSession(overrides: Partial<Session> = {}): Session {
|
|
13
|
+
const id = overrides.id ?? toSessionID(faker.string.uuid());
|
|
14
|
+
const cwd = faker.system.directoryPath();
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
id,
|
|
18
|
+
name: faker.word.noun(),
|
|
19
|
+
cwd,
|
|
20
|
+
kind: 'pty',
|
|
21
|
+
pty: null,
|
|
22
|
+
state: 'exited',
|
|
23
|
+
unread: false,
|
|
24
|
+
lastMsg: '',
|
|
25
|
+
agent: 'claude',
|
|
26
|
+
pinned: false,
|
|
27
|
+
lastAttachedAt: faker.date.recent().getTime(),
|
|
28
|
+
repoRoot: cwd,
|
|
29
|
+
namedBy: 'auto',
|
|
30
|
+
createdAt: faker.date.past().getTime(),
|
|
31
|
+
parent: null,
|
|
32
|
+
target: 'local',
|
|
33
|
+
targetIdentity: 'local-pty:test',
|
|
34
|
+
withheldEnv: [],
|
|
35
|
+
desired: 'run',
|
|
36
|
+
vm: 'none',
|
|
37
|
+
attachment: 'local',
|
|
38
|
+
suspended: false,
|
|
39
|
+
hostKey: id,
|
|
40
|
+
bridgeEpoch: 0,
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { SortableSessionView } from '../protocol/sortable-session-view';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The ordering fields of a running, unpinned, top-level session, as the
|
|
6
|
+
* session list sorts it. The id and both times are arbitrary.
|
|
7
|
+
*/
|
|
8
|
+
export function buildMockSortableSessionView(
|
|
9
|
+
overrides: Partial<SortableSessionView> = {},
|
|
10
|
+
): SortableSessionView {
|
|
11
|
+
return {
|
|
12
|
+
id: faker.string.uuid(),
|
|
13
|
+
parent: null,
|
|
14
|
+
state: 'running',
|
|
15
|
+
pinned: false,
|
|
16
|
+
lastAttachedAt: faker.date.recent().getTime(),
|
|
17
|
+
createdAt: faker.date.past().getTime(),
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentAdapter } from '../agents/agent-adapter';
|
|
2
|
+
import { buildMockAgentAdapter } from './build-mock-agent-adapter';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An agent adapter for daemon tests whose hooks report attention: id
|
|
6
|
+
* `claude`, a spawn that runs `sleep 30`, sessions that take inbox
|
|
7
|
+
* messages, a `Notification` hook read as needing input and every other
|
|
8
|
+
* hook as a submitted prompt, and `claude --resume` as the resume command.
|
|
9
|
+
* It reads nothing from a hook's payload, so a payload's session id binds
|
|
10
|
+
* nothing. Every other member is the mock adapter's. An override replaces
|
|
11
|
+
* the member it names.
|
|
12
|
+
*/
|
|
13
|
+
export function buildStubAttentionAdapter(overrides: Partial<AgentAdapter> = {}): AgentAdapter {
|
|
14
|
+
return buildMockAgentAdapter({
|
|
15
|
+
takesMessages: true,
|
|
16
|
+
normalizeHook: (hook) => ({
|
|
17
|
+
kind: hook.event === 'Notification' ? 'needs-input' : 'prompt-submitted',
|
|
18
|
+
}),
|
|
19
|
+
buildResumeCommand: () => 'claude --resume',
|
|
20
|
+
...overrides,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BridgeContext } from '../daemon/start-session-bridge';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The daemon's side of a session bridge, for tests that drive one bridge
|
|
5
|
+
* without a daemon behind it: no live session, hook events that go nowhere,
|
|
6
|
+
* every report recorded, every tap attached, every ack for an unknown
|
|
7
|
+
* message, and detaches that do nothing. An override replaces the member it
|
|
8
|
+
* names.
|
|
9
|
+
*/
|
|
10
|
+
export function buildStubBridgeContext(overrides: Partial<BridgeContext> = {}): BridgeContext {
|
|
11
|
+
return {
|
|
12
|
+
findSession: () => {},
|
|
13
|
+
applyHookEvent: () => {},
|
|
14
|
+
applyReport: () => Promise.resolve(true),
|
|
15
|
+
attachTap: () => 'ok',
|
|
16
|
+
ackMessage: () => Promise.resolve('unknown'),
|
|
17
|
+
detachTap: () => {},
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AgentAdapter } from '../agents/agent-adapter';
|
|
2
|
+
import { buildMockAgentAdapter } from './build-mock-agent-adapter';
|
|
3
|
+
|
|
4
|
+
interface StubBrokeredAgentAdapterConfig {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
|
|
7
|
+
// Whether the agent starts only behind the broker; false lets it start
|
|
8
|
+
// on a target that reaches no broker.
|
|
9
|
+
readonly brokerRequired: boolean;
|
|
10
|
+
|
|
11
|
+
// Whether the agent takes its credential from the broker at the moment
|
|
12
|
+
// of the call, as a config change can turn it on or off for an agent
|
|
13
|
+
// whose sessions already exist.
|
|
14
|
+
readonly isSelected: () => boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An agent adapter for daemon tests that takes the credential of a GLM
|
|
19
|
+
* gateway from impd's broker: the auth profile `glm`, a custom bearer
|
|
20
|
+
* secret `glm` for api.z.ai, behind the gateway at
|
|
21
|
+
* https://api.z.ai/api/anthropic with the placeholder
|
|
22
|
+
* `ANTHROPIC_AUTH_TOKEN`. A guest spawn under a binding runs a shell that
|
|
23
|
+
* prints `revision <n>` for the binding's revision and then sleeps, with
|
|
24
|
+
* the binding's variables and `CLAUDE_CONFIG_DIR` set to `claude-config`
|
|
25
|
+
* in the guest folder; a guest spawn without one, and a local spawn, sleep.
|
|
26
|
+
* Every other member is the plain agent adapter's.
|
|
27
|
+
*/
|
|
28
|
+
export function buildStubBrokeredAgentAdapter(
|
|
29
|
+
config: StubBrokeredAgentAdapterConfig,
|
|
30
|
+
): AgentAdapter {
|
|
31
|
+
return buildMockAgentAdapter({
|
|
32
|
+
id: config.id,
|
|
33
|
+
profile: {
|
|
34
|
+
label: 'GLM',
|
|
35
|
+
kind: 'claude',
|
|
36
|
+
bin: 'sh',
|
|
37
|
+
models: null,
|
|
38
|
+
spawnOptions: {
|
|
39
|
+
model: {
|
|
40
|
+
supported: false,
|
|
41
|
+
values: null,
|
|
42
|
+
examples: [],
|
|
43
|
+
default: null,
|
|
44
|
+
backendEffect: null,
|
|
45
|
+
note: null,
|
|
46
|
+
},
|
|
47
|
+
effort: {
|
|
48
|
+
supported: false,
|
|
49
|
+
values: null,
|
|
50
|
+
examples: [],
|
|
51
|
+
default: null,
|
|
52
|
+
backendEffect: null,
|
|
53
|
+
note: null,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
planGuestSpawn: (_opts, guest) =>
|
|
58
|
+
guest.auth === undefined
|
|
59
|
+
? { bin: 'sleep', args: ['30'], files: {} }
|
|
60
|
+
: {
|
|
61
|
+
bin: 'sh',
|
|
62
|
+
args: ['-c', `echo "revision ${String(guest.auth.revision)}"; exec sleep 30`],
|
|
63
|
+
files: {},
|
|
64
|
+
env: { ...guest.auth.env, CLAUDE_CONFIG_DIR: `${guest.dir}/claude-config` },
|
|
65
|
+
},
|
|
66
|
+
findAuthSelection: () =>
|
|
67
|
+
config.isSelected()
|
|
68
|
+
? {
|
|
69
|
+
brokerRequired: config.brokerRequired,
|
|
70
|
+
gateway: {
|
|
71
|
+
id: 'glm',
|
|
72
|
+
baseURL: 'https://api.z.ai/api/anthropic',
|
|
73
|
+
auth: {
|
|
74
|
+
profiles: ['glm'],
|
|
75
|
+
placeholderEnv: { ANTHROPIC_AUTH_TOKEN: 'imp-broker-placeholder' },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
profiles: new Map([
|
|
79
|
+
[
|
|
80
|
+
'glm',
|
|
81
|
+
{
|
|
82
|
+
name: 'glm',
|
|
83
|
+
secret: 'glm',
|
|
84
|
+
kind: 'custom',
|
|
85
|
+
host: 'api.z.ai',
|
|
86
|
+
header: 'authorization',
|
|
87
|
+
scheme: 'bearer',
|
|
88
|
+
env: {},
|
|
89
|
+
dependencies: [],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
]),
|
|
93
|
+
}
|
|
94
|
+
: null,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type ChannelOpener<Address, Channel> = (address: Address) => Promise<Channel>;
|
|
2
|
+
|
|
3
|
+
interface StubChannelOpenerOptions {
|
|
4
|
+
// The position of the dial to hold, counting from 1; no dial is held
|
|
5
|
+
// without it.
|
|
6
|
+
readonly holdDial?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A dial function that opens each connection with the opener at the dial's
|
|
11
|
+
* position in the list, and every dial past the end with the last one, so
|
|
12
|
+
* a test can send a first connection to one place and every reconnect to
|
|
13
|
+
* another. The dial at `holdDial` waits before it opens: `waitForHeld`
|
|
14
|
+
* resolves once that dial has started, and `releaseHeld` lets it go on.
|
|
15
|
+
* `countOpened` returns how many dials it has made.
|
|
16
|
+
*/
|
|
17
|
+
export function buildStubChannelOpener<Address, Channel>(
|
|
18
|
+
openers: readonly [ChannelOpener<Address, Channel>, ...ChannelOpener<Address, Channel>[]],
|
|
19
|
+
options: StubChannelOpenerOptions = {},
|
|
20
|
+
) {
|
|
21
|
+
const reached = Promise.withResolvers<void>();
|
|
22
|
+
const released = Promise.withResolvers<void>();
|
|
23
|
+
let opened = 0;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
open: async (address: Address): Promise<Channel> => {
|
|
27
|
+
opened++;
|
|
28
|
+
|
|
29
|
+
const opener = openers[Math.min(opened, openers.length) - 1] ?? openers[0];
|
|
30
|
+
|
|
31
|
+
if (opened === options.holdDial) {
|
|
32
|
+
reached.resolve();
|
|
33
|
+
|
|
34
|
+
await released.promise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return opener(address);
|
|
38
|
+
},
|
|
39
|
+
countOpened: (): number => opened,
|
|
40
|
+
waitForHeld: (): Promise<void> => reached.promise,
|
|
41
|
+
releaseHeld: (): void => {
|
|
42
|
+
released.resolve();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { mock } from 'bun:test';
|
|
2
|
+
import type { Mock } from 'bun:test';
|
|
3
|
+
import type { ClaudeHeadlessRun } from '../agents/make-claude-headless-runner';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A stand-in for the Claude headless runner an adapter takes: it records
|
|
7
|
+
* each request and its event hooks, starts no turn, fires no hook, and
|
|
8
|
+
* hands back a handle whose stop does nothing. A test reads what the
|
|
9
|
+
* adapter asked for through the mock matchers.
|
|
10
|
+
*/
|
|
11
|
+
export function buildStubClaudeHeadlessRun(): Mock<ClaudeHeadlessRun> {
|
|
12
|
+
return mock<ClaudeHeadlessRun>(() => ({ stop: () => {} }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ClientMachineDeps } from '../client/build-client-machine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The screens a client machine opens, as side effects that each append
|
|
5
|
+
* their name, and the session or mode they were given, to `calls` in the
|
|
6
|
+
* order they run, and draw nothing.
|
|
7
|
+
*/
|
|
8
|
+
export function buildStubClientScreens() {
|
|
9
|
+
const calls: string[] = [];
|
|
10
|
+
|
|
11
|
+
const deps: ClientMachineDeps = {
|
|
12
|
+
openHome: () => {
|
|
13
|
+
calls.push('openHome');
|
|
14
|
+
},
|
|
15
|
+
openAttached: (sessionID) => {
|
|
16
|
+
calls.push(`openAttached:${sessionID}`);
|
|
17
|
+
},
|
|
18
|
+
openOverlay: () => {
|
|
19
|
+
calls.push('openOverlay');
|
|
20
|
+
},
|
|
21
|
+
openHelp: () => {
|
|
22
|
+
calls.push('openHelp');
|
|
23
|
+
},
|
|
24
|
+
openPicker: (resume) => {
|
|
25
|
+
calls.push(`openPicker:${resume}`);
|
|
26
|
+
},
|
|
27
|
+
openEject: (sessionID) => {
|
|
28
|
+
calls.push(`openEject:${sessionID}`);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return { deps, calls };
|
|
33
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Clock } from '../shared/system-clock';
|
|
2
|
+
|
|
3
|
+
interface StubTimer {
|
|
4
|
+
readonly dueAt: number;
|
|
5
|
+
readonly callback: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A clock that moves only when the test advances it, for code that takes a
|
|
10
|
+
* clock in place of the wall clock and the runtime's timers. It starts at
|
|
11
|
+
* the given epoch milliseconds. `advance` moves the time forward and runs,
|
|
12
|
+
* in due order, every scheduled callback whose time has come, including
|
|
13
|
+
* one a callback schedules within the step. `collectPending` returns, in
|
|
14
|
+
* due order, how many milliseconds each callback still waiting has left,
|
|
15
|
+
* which lets a test wait until the code under test has scheduled one and
|
|
16
|
+
* check its delay.
|
|
17
|
+
*/
|
|
18
|
+
export function buildStubClock(startMs: number) {
|
|
19
|
+
let time = startMs;
|
|
20
|
+
let nextID = 0;
|
|
21
|
+
|
|
22
|
+
const timers = new Map<number, StubTimer>();
|
|
23
|
+
|
|
24
|
+
const findDue = (until: number): [number, StubTimer] | null => {
|
|
25
|
+
let due: [number, StubTimer] | null = null;
|
|
26
|
+
|
|
27
|
+
for (const entry of timers) {
|
|
28
|
+
if (entry[1].dueAt <= until && (due === null || entry[1].dueAt < due[1].dueAt)) {
|
|
29
|
+
due = entry;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return due;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const clock: Clock = {
|
|
37
|
+
now: () => time,
|
|
38
|
+
schedule: (callback, delayMs) => {
|
|
39
|
+
const id = nextID++;
|
|
40
|
+
|
|
41
|
+
timers.set(id, { dueAt: time + Math.max(0, delayMs), callback });
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
timers.delete(id);
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...clock,
|
|
51
|
+
advance(ms: number): void {
|
|
52
|
+
const until = time + ms;
|
|
53
|
+
|
|
54
|
+
for (let due = findDue(until); due !== null; due = findDue(until)) {
|
|
55
|
+
timers.delete(due[0]);
|
|
56
|
+
|
|
57
|
+
time = due[1].dueAt;
|
|
58
|
+
|
|
59
|
+
due[1].callback();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
time = until;
|
|
63
|
+
},
|
|
64
|
+
collectPending: (): number[] =>
|
|
65
|
+
[...timers.values()].map((timer) => timer.dueAt - time).toSorted((a, b) => a - b),
|
|
66
|
+
};
|
|
67
|
+
}
|