@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,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,50 @@
|
|
|
1
|
+
import type { AgentAdapter } from '../agents/agent-adapter';
|
|
2
|
+
import { buildMockAgentAdapter } from './build-mock-agent-adapter';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An agent adapter `glm` for daemon tests that only starts behind impd's
|
|
6
|
+
* broker and takes the credential of a GLM gateway from it: the auth
|
|
7
|
+
* profile `glm`, a custom bearer secret `glm` for api.z.ai, behind the
|
|
8
|
+
* gateway at https://api.z.ai/api/anthropic with the placeholder
|
|
9
|
+
* `ANTHROPIC_AUTH_TOKEN`. Every guest spawn sleeps, with the binding's
|
|
10
|
+
* variables, when it has one, and `CLAUDE_CONFIG_DIR` set to
|
|
11
|
+
* `claude-config` in the guest folder. Every other member is the plain
|
|
12
|
+
* agent adapter's.
|
|
13
|
+
*/
|
|
14
|
+
export function buildStubBrokeredGatewayAdapter(): AgentAdapter {
|
|
15
|
+
return buildMockAgentAdapter({
|
|
16
|
+
id: 'glm',
|
|
17
|
+
planGuestSpawn: (_opts, guest) => ({
|
|
18
|
+
bin: 'sleep',
|
|
19
|
+
args: ['30'],
|
|
20
|
+
files: {},
|
|
21
|
+
env: { ...guest.auth?.env, CLAUDE_CONFIG_DIR: `${guest.dir}/claude-config` },
|
|
22
|
+
}),
|
|
23
|
+
findAuthSelection: () => ({
|
|
24
|
+
brokerRequired: true,
|
|
25
|
+
gateway: {
|
|
26
|
+
id: 'glm',
|
|
27
|
+
baseURL: 'https://api.z.ai/api/anthropic',
|
|
28
|
+
auth: {
|
|
29
|
+
profiles: ['glm'],
|
|
30
|
+
placeholderEnv: { ANTHROPIC_AUTH_TOKEN: 'imp-broker-placeholder' },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
profiles: new Map([
|
|
34
|
+
[
|
|
35
|
+
'glm',
|
|
36
|
+
{
|
|
37
|
+
name: 'glm',
|
|
38
|
+
secret: 'glm',
|
|
39
|
+
kind: 'custom',
|
|
40
|
+
host: 'api.z.ai',
|
|
41
|
+
header: 'authorization',
|
|
42
|
+
scheme: 'bearer',
|
|
43
|
+
env: {},
|
|
44
|
+
dependencies: [],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
]),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DaemonContext } from '../daemon/daemon-context';
|
|
2
|
+
import { TargetAccess } from '../daemon/target-access';
|
|
3
|
+
import { toDaemonID } from '../shared/to-daemon-id';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The daemon a connection serves, for tests that drive one connection
|
|
7
|
+
* without a daemon behind it: build `atc/test`, daemon id `d-1`, a 64 KiB
|
|
8
|
+
* outbound queue, a day's idempotency retention, no sessions, `claude` as
|
|
9
|
+
* the last agent used, an empty fleet, a principal that reaches no target,
|
|
10
|
+
* every session bound to the `local` target and visible, and resyncs and
|
|
11
|
+
* detaches that do nothing. Every other
|
|
12
|
+
* member throws when called, naming itself, so a test that reaches one it
|
|
13
|
+
* did not wire fails loudly. An override replaces the member it names.
|
|
14
|
+
*/
|
|
15
|
+
export function buildStubDaemonContext(overrides: Partial<DaemonContext> = {}): DaemonContext {
|
|
16
|
+
return {
|
|
17
|
+
build: 'atc/test',
|
|
18
|
+
daemonID: toDaemonID('d-1'),
|
|
19
|
+
queueBytes: 65_536,
|
|
20
|
+
idempotencyRetentionMs: 86_400_000,
|
|
21
|
+
collectSessions: () => [],
|
|
22
|
+
collectSpawnDirs: makeUnreached('collectSpawnDirs'),
|
|
23
|
+
collectAgents: makeUnreached('collectAgents'),
|
|
24
|
+
collectFleet: () => Promise.resolve([]),
|
|
25
|
+
loadLastUsedAgent: () => Promise.resolve('claude'),
|
|
26
|
+
findAdapter: makeUnreached('findAdapter'),
|
|
27
|
+
buildTargetAccess: () => new TargetAccess([]),
|
|
28
|
+
hasListedPrincipal: makeUnreached('hasListedPrincipal'),
|
|
29
|
+
findSessionGrant: () => ({ target: 'local', targetIdentity: 'local-pty' }),
|
|
30
|
+
findTargetIdentity: makeUnreached('findTargetIdentity'),
|
|
31
|
+
canSeeSession: () => true,
|
|
32
|
+
isSessionVisible: makeUnreached('isSessionVisible'),
|
|
33
|
+
findPermissionSession: makeUnreached('findPermissionSession'),
|
|
34
|
+
resolveSpawnParent: makeUnreached('resolveSpawnParent'),
|
|
35
|
+
resolveSpawnTarget: makeUnreached('resolveSpawnTarget'),
|
|
36
|
+
requireWorkspaceTarget: makeUnreached('requireWorkspaceTarget'),
|
|
37
|
+
buildDefaultWorkspaceDir: makeUnreached('buildDefaultWorkspaceDir'),
|
|
38
|
+
requireAgentTarget: makeUnreached('requireAgentTarget'),
|
|
39
|
+
findSource: makeUnreached('findSource'),
|
|
40
|
+
collectAlternateGitURLs: makeUnreached('collectAlternateGitURLs'),
|
|
41
|
+
checkRepositoryAccess: makeUnreached('checkRepositoryAccess'),
|
|
42
|
+
spawnSession: makeUnreached('spawnSession'),
|
|
43
|
+
killSession: makeUnreached('killSession'),
|
|
44
|
+
forgetSession: makeUnreached('forgetSession'),
|
|
45
|
+
revokeSessionAuth: makeUnreached('revokeSessionAuth'),
|
|
46
|
+
updateSessionAuth: makeUnreached('updateSessionAuth'),
|
|
47
|
+
updateSession: makeUnreached('updateSession'),
|
|
48
|
+
quitDaemon: makeUnreached('quitDaemon'),
|
|
49
|
+
ackSession: makeUnreached('ackSession'),
|
|
50
|
+
buildResumeCommand: makeUnreached('buildResumeCommand'),
|
|
51
|
+
readSessionScreen: makeUnreached('readSessionScreen'),
|
|
52
|
+
readSessionRecord: makeUnreached('readSessionRecord'),
|
|
53
|
+
loadSessionTranscript: makeUnreached('loadSessionTranscript'),
|
|
54
|
+
readEvents: makeUnreached('readEvents'),
|
|
55
|
+
readReport: makeUnreached('readReport'),
|
|
56
|
+
answerPermission: makeUnreached('answerPermission'),
|
|
57
|
+
restoreFleet: makeUnreached('restoreFleet'),
|
|
58
|
+
attachSession: makeUnreached('attachSession'),
|
|
59
|
+
detachSession: () => {},
|
|
60
|
+
detachClient: makeUnreached('detachClient'),
|
|
61
|
+
writeSessionInput: makeUnreached('writeSessionInput'),
|
|
62
|
+
writeSessionLine: makeUnreached('writeSessionLine'),
|
|
63
|
+
ejectSession: makeUnreached('ejectSession'),
|
|
64
|
+
adoptSession: makeUnreached('adoptSession'),
|
|
65
|
+
resizeSession: makeUnreached('resizeSession'),
|
|
66
|
+
resyncClient: () => Promise.resolve(),
|
|
67
|
+
getEffectiveDims: makeUnreached('getEffectiveDims'),
|
|
68
|
+
writeSessionMessage: makeUnreached('writeSessionMessage'),
|
|
69
|
+
readMessage: makeUnreached('readMessage'),
|
|
70
|
+
attachTap: makeUnreached('attachTap'),
|
|
71
|
+
detachTap: () => {},
|
|
72
|
+
ackMessage: makeUnreached('ackMessage'),
|
|
73
|
+
...overrides,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function makeUnreached(member: string): () => never {
|
|
78
|
+
return () => {
|
|
79
|
+
throw new Error(`the stub daemon context has no ${member}`);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { waitFor } from './wait-for';
|
|
2
|
+
|
|
3
|
+
type DaemonAnswer = Readonly<Record<string, unknown>>;
|
|
4
|
+
|
|
5
|
+
interface StubRequest {
|
|
6
|
+
readonly m: string;
|
|
7
|
+
readonly p: DaemonAnswer;
|
|
8
|
+
readonly resolve: (answer: DaemonAnswer) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface StubDaemonRequestsOptions {
|
|
12
|
+
// A count the client under test raises each time it acts on something,
|
|
13
|
+
// such as a draw or a callback it makes.
|
|
14
|
+
readonly countReactions: () => number;
|
|
15
|
+
|
|
16
|
+
// Milliseconds each wait for a request or a reaction lasts before it
|
|
17
|
+
// rejects; five seconds when absent.
|
|
18
|
+
readonly timeoutMs?: number;
|
|
19
|
+
|
|
20
|
+
// The clock each wait reads its deadline from; the wall clock when absent.
|
|
21
|
+
readonly now?: () => number;
|
|
22
|
+
|
|
23
|
+
// Waits out the pause between a wait's checks; a real sleep when absent.
|
|
24
|
+
readonly wait?: (ms: number) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A daemon request channel that answers only when the test says so. Every
|
|
29
|
+
* request sent through `sendRequest` waits until `answer` or
|
|
30
|
+
* `answerOldest` resolves it. Each of those waits for a request under the
|
|
31
|
+
* method to be waiting, resolves the latest or the oldest one, and then
|
|
32
|
+
* waits for the client to react: to send another request, or to raise the
|
|
33
|
+
* count `countReactions` reads. A wait that outlasts `timeoutMs` rejects:
|
|
34
|
+
* with `no <method> request is waiting for an answer` when nothing was
|
|
35
|
+
* sent under the method, and with `nothing reacted to the <method> answer`
|
|
36
|
+
* when the client never reacted. A test that takes the `now` and `wait`
|
|
37
|
+
* of a stub clock steps that timeout without waiting it out. `collectSent` returns the params of every
|
|
38
|
+
* request sent under a method, answered or not, in the order they were
|
|
39
|
+
* sent.
|
|
40
|
+
*/
|
|
41
|
+
export function buildStubDaemonRequests(options: StubDaemonRequestsOptions) {
|
|
42
|
+
const sent: StubRequest[] = [];
|
|
43
|
+
|
|
44
|
+
const answered = new Set<StubRequest>();
|
|
45
|
+
|
|
46
|
+
const waitOptions = {
|
|
47
|
+
timeoutMs: options.timeoutMs ?? 5000,
|
|
48
|
+
now: options.now ?? Date.now,
|
|
49
|
+
wait: options.wait ?? Bun.sleep,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const countAll = () => sent.length + options.countReactions();
|
|
53
|
+
|
|
54
|
+
const resolveAndWait = async (request: Readonly<StubRequest>, value: DaemonAnswer) => {
|
|
55
|
+
const before = countAll();
|
|
56
|
+
|
|
57
|
+
answered.add(request);
|
|
58
|
+
request.resolve(value);
|
|
59
|
+
|
|
60
|
+
await waitFor(() => {
|
|
61
|
+
if (countAll() <= before) {
|
|
62
|
+
throw new Error(`nothing reacted to the ${request.m} answer`);
|
|
63
|
+
}
|
|
64
|
+
}, waitOptions);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
sendRequest(m: string, p: DaemonAnswer = {}): Promise<DaemonAnswer> {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
sent.push({ m, p, resolve });
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
async answer(m: string, value: DaemonAnswer): Promise<void> {
|
|
74
|
+
const request = await waitFor(() => {
|
|
75
|
+
const latest = sent.findLast((r) => r.m === m && !answered.has(r));
|
|
76
|
+
|
|
77
|
+
if (latest === undefined) {
|
|
78
|
+
throw new Error(`no ${m} request is waiting for an answer`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return latest;
|
|
82
|
+
}, waitOptions);
|
|
83
|
+
|
|
84
|
+
await resolveAndWait(request, value);
|
|
85
|
+
},
|
|
86
|
+
async answerOldest(m: string, value: DaemonAnswer): Promise<void> {
|
|
87
|
+
const request = await waitFor(() => {
|
|
88
|
+
const oldest = sent.find((r) => r.m === m && !answered.has(r));
|
|
89
|
+
|
|
90
|
+
if (oldest === undefined) {
|
|
91
|
+
throw new Error(`no ${m} request is waiting for an answer`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return oldest;
|
|
95
|
+
}, waitOptions);
|
|
96
|
+
|
|
97
|
+
await resolveAndWait(request, value);
|
|
98
|
+
},
|
|
99
|
+
collectSent(m: string): DaemonAnswer[] {
|
|
100
|
+
return sent.filter((request) => request.m === m).map((request) => request.p);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -11,11 +11,11 @@ import type {
|
|
|
11
11
|
import { LocalPTYProvider } from '../daemon/local-pty-provider';
|
|
12
12
|
|
|
13
13
|
// One provider operation, in the order the daemon called it.
|
|
14
|
-
type
|
|
14
|
+
type StubDirCall =
|
|
15
15
|
| { readonly op: 'transfer'; readonly dir: string; readonly bytes: number }
|
|
16
16
|
| { readonly op: 'run'; readonly argv: readonly string[]; readonly cwd: string };
|
|
17
17
|
|
|
18
|
-
interface
|
|
18
|
+
interface StubDirOptions {
|
|
19
19
|
// Capabilities the provider declares as missing.
|
|
20
20
|
readonly lacking?: readonly ExecutionCapability[];
|
|
21
21
|
|
|
@@ -25,22 +25,16 @@ interface FixtureDirOptions {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
* owns: a transfer unpacks the archive with tar into the directory it is
|
|
30
|
-
* given, and a command runs as a child process in its working directory.
|
|
31
|
-
* Paths pass through unchanged, so a test points them into its own temp
|
|
32
|
-
* tree. Every transfer and command is recorded in `calls`. Harnesses start
|
|
33
|
-
* on a local pseudo-terminal, and each one's spec is recorded in
|
|
34
|
-
* `harnesses`.
|
|
28
|
+
* The directory-tree provider the factory below builds, one per call.
|
|
35
29
|
*/
|
|
36
|
-
|
|
30
|
+
class StubDirProvider implements ExecutionProvider {
|
|
37
31
|
readonly kind = 'fixture-dir';
|
|
38
32
|
|
|
39
33
|
readonly remote = false;
|
|
40
34
|
|
|
41
35
|
readonly capabilities: ExecutionCapabilities;
|
|
42
36
|
|
|
43
|
-
readonly calls:
|
|
37
|
+
readonly calls: StubDirCall[] = [];
|
|
44
38
|
|
|
45
39
|
// Every harness the provider started, as the daemon specified it.
|
|
46
40
|
readonly harnesses: HarnessSpec[] = [];
|
|
@@ -49,7 +43,7 @@ export class FixtureDirProvider implements ExecutionProvider {
|
|
|
49
43
|
|
|
50
44
|
private readonly terminals = new LocalPTYProvider();
|
|
51
45
|
|
|
52
|
-
constructor(options:
|
|
46
|
+
constructor(options: StubDirOptions = {}) {
|
|
53
47
|
const lacking = new Set(options.lacking);
|
|
54
48
|
|
|
55
49
|
this.capabilities = {
|
|
@@ -129,3 +123,17 @@ export class FixtureDirProvider implements ExecutionProvider {
|
|
|
129
123
|
|
|
130
124
|
readonly dispose = (): void => {};
|
|
131
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Builds an execution provider of kind `fixture-dir` for tests whose host is
|
|
129
|
+
* a directory tree the test owns: a transfer unpacks the archive with tar
|
|
130
|
+
* into the directory it is given, and a command runs as a child process in
|
|
131
|
+
* its working directory. Paths pass through unchanged, so a test points them
|
|
132
|
+
* into its own temp tree. Every transfer and command is recorded in `calls`.
|
|
133
|
+
* Harnesses start on a local pseudo-terminal, and each one's spec is
|
|
134
|
+
* recorded in `harnesses`. Suspending or destroying a host rejects, since
|
|
135
|
+
* the test's own tree neither sleeps nor goes away.
|
|
136
|
+
*/
|
|
137
|
+
export function buildStubDirProvider(options: StubDirOptions = {}): StubDirProvider {
|
|
138
|
+
return new StubDirProvider(options);
|
|
139
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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 StubExecutionProviderConfig {
|
|
9
|
+
// The provider kind the daemon reports for the target.
|
|
10
|
+
readonly kind?: string;
|
|
11
|
+
|
|
12
|
+
// Capabilities that differ from a local pseudo-terminal's.
|
|
13
|
+
readonly capabilities?: Partial<ExecutionCapabilities>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Called with each harness spec before the harness starts; a throw from
|
|
17
|
+
* it aborts the spawn.
|
|
18
|
+
*/
|
|
19
|
+
readonly onSpawn?: (spec: HarnessSpec) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface StubExecutionProvider extends ExecutionProvider {
|
|
23
|
+
// Every host the daemon put to sleep, in order.
|
|
24
|
+
readonly suspended: string[];
|
|
25
|
+
|
|
26
|
+
// Every host the daemon destroyed, in order.
|
|
27
|
+
readonly destroyed: string[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Rejects every host suspend with the error, recording nothing, until
|
|
31
|
+
* called with null.
|
|
32
|
+
*/
|
|
33
|
+
readonly setSuspendFailure: (error: Readonly<Error> | null) => void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Rejects every host destroy with the error, recording nothing, until
|
|
37
|
+
* called with null.
|
|
38
|
+
*/
|
|
39
|
+
readonly setDestroyFailure: (error: Readonly<Error> | null) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* An execution provider for daemon tests that runs harnesses, transfers, and
|
|
44
|
+
* commands on this machine as the local pseudo-terminal provider does, with
|
|
45
|
+
* the capabilities the config changes on top of its own. It reports each
|
|
46
|
+
* harness spec to the config before starting it. A host suspend or
|
|
47
|
+
* destroy does nothing to the machine: it records the host in `suspended` or
|
|
48
|
+
* `destroyed` and resolves, or rejects with the error a failure setter gave.
|
|
49
|
+
* The daemon calls them only when the capabilities declare `suspend` or
|
|
50
|
+
* `destroy`.
|
|
51
|
+
*/
|
|
52
|
+
export function buildStubExecutionProvider(
|
|
53
|
+
config: StubExecutionProviderConfig = {},
|
|
54
|
+
): StubExecutionProvider {
|
|
55
|
+
const local = new LocalPTYProvider();
|
|
56
|
+
|
|
57
|
+
const onSpawn = config.onSpawn ?? (() => {});
|
|
58
|
+
const suspended: string[] = [];
|
|
59
|
+
const destroyed: string[] = [];
|
|
60
|
+
|
|
61
|
+
// The errors a suspend and a destroy reject with, or null while they
|
|
62
|
+
// succeed.
|
|
63
|
+
const failures: { suspend: Readonly<Error> | null; destroy: Readonly<Error> | null } = {
|
|
64
|
+
suspend: null,
|
|
65
|
+
destroy: null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
kind: config.kind ?? 'stub',
|
|
70
|
+
remote: false,
|
|
71
|
+
capabilities: { ...local.capabilities, ...config.capabilities },
|
|
72
|
+
prepareHost: local.prepareHost,
|
|
73
|
+
spawnHarness: (spec) => {
|
|
74
|
+
onSpawn(spec);
|
|
75
|
+
|
|
76
|
+
return local.spawnHarness(spec);
|
|
77
|
+
},
|
|
78
|
+
transferArchive: local.transferArchive,
|
|
79
|
+
runCommand: local.runCommand,
|
|
80
|
+
suspendHost: (host) => {
|
|
81
|
+
const failure = failures.suspend;
|
|
82
|
+
|
|
83
|
+
if (failure !== null) {
|
|
84
|
+
return Promise.reject(failure);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
suspended.push(host);
|
|
88
|
+
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
},
|
|
91
|
+
destroyHost: (host) => {
|
|
92
|
+
const failure = failures.destroy;
|
|
93
|
+
|
|
94
|
+
if (failure !== null) {
|
|
95
|
+
return Promise.reject(failure);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
destroyed.push(host);
|
|
99
|
+
|
|
100
|
+
return Promise.resolve();
|
|
101
|
+
},
|
|
102
|
+
dispose: local.dispose,
|
|
103
|
+
suspended,
|
|
104
|
+
destroyed,
|
|
105
|
+
setSuspendFailure: (error) => {
|
|
106
|
+
failures.suspend = error;
|
|
107
|
+
},
|
|
108
|
+
setDestroyFailure: (error) => {
|
|
109
|
+
failures.destroy = error;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|