@mycadet/cli 0.12.10 → 0.13.0-dev.202610171617.9ea363e
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/README.md +13 -0
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/package.json +8 -3
- package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
- package/src/__tests__/backup-ops-matching.test.ts +15 -13
- package/src/__tests__/backup-ops.test.ts +105 -0
- package/src/__tests__/backup.test.ts +114 -673
- package/src/__tests__/cloud-login-poll.test.ts +69 -0
- package/src/__tests__/cloud-releases.test.ts +82 -0
- package/src/__tests__/commands-mock.ts +101 -0
- package/src/__tests__/debug.test.ts +17 -91
- package/src/__tests__/devices.test.ts +64 -2
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +587 -36
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/paired-lifecycle.test.ts +2 -2
- package/src/__tests__/ps-cloud-status.test.ts +8 -7
- package/src/__tests__/root-backup-docker.test.ts +294 -0
- package/src/__tests__/root-backup.test.ts +527 -0
- package/src/__tests__/root-restore-command.test.ts +147 -0
- package/src/__tests__/root-signing-key.test.ts +50 -0
- package/src/__tests__/setup.test.ts +17 -7
- package/src/__tests__/shell-mode.test.ts +83 -0
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +22 -4
- package/src/__tests__/upgrade-local.test.ts +21 -11
- package/src/__tests__/upgrade-replay-env.test.ts +2 -2
- package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
- package/src/__tests__/wake.test.ts +4 -1
- package/src/__tests__/windows-lifecycle.test.ts +25 -0
- package/src/commands/backup.ts +44 -399
- package/src/commands/client.ts +3 -3
- package/src/commands/completions.ts +0 -2
- package/src/commands/debug.ts +75 -131
- package/src/commands/devices.ts +117 -47
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/hatch.ts +10 -52
- package/src/commands/pair.ts +9 -3
- package/src/commands/ps.ts +4 -2
- package/src/commands/restore.ts +77 -444
- package/src/commands/retire.ts +2 -3
- package/src/commands/sleep.ts +15 -2
- package/src/commands/teleport.ts +47 -1559
- package/src/commands/tunnel.ts +91 -11
- package/src/commands/upgrade.ts +84 -157
- package/src/commands/wake.ts +1 -1
- package/src/index.ts +0 -4
- package/src/lib/__tests__/http-client-version.test.ts +29 -0
- package/src/lib/__tests__/runtime-url.test.ts +3 -75
- package/src/lib/__tests__/web-dist.test.ts +27 -1
- package/src/lib/assistant-client.ts +1 -1
- package/src/lib/assistant-config.ts +43 -23
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +5 -2
- package/src/lib/cloud-releases.ts +54 -10
- package/src/lib/cloud-values.ts +1 -1
- package/src/lib/confirm-action.ts +2 -2
- package/src/lib/docker.ts +65 -34
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +18 -52
- package/src/lib/hatch-next-steps.ts +15 -2
- package/src/lib/host-teleport.ts +506 -0
- package/src/lib/http-client.ts +25 -0
- package/src/lib/ingress-config.ts +31 -0
- package/src/lib/live-voice/tunnel.test.ts +1 -1
- package/src/lib/live-voice/tunnel.ts +3 -3
- package/src/lib/local.ts +10 -8
- package/src/lib/nginx-ingress.ts +572 -95
- package/src/lib/process.ts +13 -0
- package/src/lib/root-backup.ts +917 -0
- package/src/lib/root-signing-key.ts +31 -0
- package/src/lib/runtime-url.ts +0 -29
- package/src/lib/teleport-backup.ts +3 -16
- package/src/lib/tunnel.ts +87 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +79 -1
- package/src/lib/xdg-log.ts +11 -0
- package/src/__tests__/bundle-staging.test.ts +0 -268
- package/src/__tests__/unpair.test.ts +0 -160
- package/src/commands/rollback.ts +0 -479
- package/src/commands/unpair.ts +0 -122
- package/src/lib/__tests__/job-polling.test.ts +0 -278
- package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
- package/src/lib/bundle-staging.ts +0 -220
- package/src/lib/job-polling.ts +0 -195
- package/src/lib/local-runtime-client.ts +0 -374
|
@@ -1,696 +1,137 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
afterEach,
|
|
4
|
-
beforeEach,
|
|
5
|
-
describe,
|
|
6
|
-
expect,
|
|
7
|
-
mock,
|
|
8
|
-
type Mock,
|
|
9
|
-
spyOn,
|
|
10
|
-
test,
|
|
11
|
-
} from "bun:test";
|
|
12
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
1
|
+
import { afterAll, afterEach, beforeEach, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
13
3
|
import { tmpdir } from "node:os";
|
|
14
4
|
import { join } from "node:path";
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Lockfile isolation (mirrors teleport.test.ts)
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
const testDir = mkdtempSync(join(tmpdir(), "cli-backup-test-"));
|
|
21
|
-
process.env.CADET_LOCKFILE_DIR = testDir;
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Mocks set up before importing the module under test
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
import * as fs from "node:fs";
|
|
28
|
-
|
|
29
|
-
import * as assistantConfig from "../lib/assistant-config.js";
|
|
5
|
+
import * as config from "../lib/assistant-config.js";
|
|
30
6
|
import * as backupOps from "../lib/backup-ops.js";
|
|
31
|
-
import * as
|
|
32
|
-
import * as
|
|
33
|
-
import
|
|
34
|
-
import * as
|
|
35
|
-
|
|
36
|
-
const findAssistantByNameMock = spyOn(
|
|
37
|
-
assistantConfig,
|
|
38
|
-
"findAssistantByName",
|
|
39
|
-
).mockReturnValue(null);
|
|
40
|
-
|
|
41
|
-
const readCloudTokenMock = spyOn(
|
|
42
|
-
cloudClient,
|
|
43
|
-
"readCloudToken",
|
|
44
|
-
).mockReturnValue("platform-token");
|
|
45
|
-
|
|
46
|
-
const getCloudUrlMock = spyOn(
|
|
47
|
-
cloudClient,
|
|
48
|
-
"getCloudUrl",
|
|
49
|
-
).mockReturnValue("https://cloud.mycadet.ai");
|
|
50
|
-
|
|
51
|
-
const cloudRequestSignedUrlMock = spyOn(
|
|
52
|
-
cloudClient,
|
|
53
|
-
"cloudRequestSignedUrl",
|
|
54
|
-
).mockImplementation(async (params) => ({
|
|
55
|
-
url:
|
|
56
|
-
params.operation === "upload"
|
|
57
|
-
? "https://storage.googleapis.com/bucket/signed-upload"
|
|
58
|
-
: "https://storage.googleapis.com/bucket/signed-download",
|
|
59
|
-
bundleKey: params.bundleKey ?? "uploads/org-1/bundle-abc.vbundle",
|
|
60
|
-
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
const localRuntimeExportToGcsMock = spyOn(
|
|
64
|
-
localRuntimeClient,
|
|
65
|
-
"localRuntimeExportToGcs",
|
|
66
|
-
).mockResolvedValue({ jobId: "platform-export-job-1" });
|
|
67
|
-
|
|
68
|
-
const localRuntimeIdentityMock = spyOn(
|
|
69
|
-
localRuntimeClient,
|
|
70
|
-
"localRuntimeIdentity",
|
|
71
|
-
).mockResolvedValue({ version: "0.6.5" });
|
|
72
|
-
|
|
73
|
-
const localRuntimePollJobStatusMock = spyOn(
|
|
74
|
-
localRuntimeClient,
|
|
75
|
-
"localRuntimePollJobStatus",
|
|
76
|
-
).mockResolvedValue({
|
|
77
|
-
jobId: "platform-export-job-1",
|
|
78
|
-
type: "export",
|
|
79
|
-
status: "complete",
|
|
80
|
-
result: { manifest_sha256: "abc123def456" },
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
/** A complete-but-fake guardian token; the SUT only reads the fields it sets. */
|
|
84
|
-
// Mode 1 (runtime-direct local backup) uses guardian tokens. Don't exercise
|
|
85
|
-
// it here, but the spies need to exist so the module under test can import
|
|
86
|
-
// them without surprises. Saved to variables so afterAll can restore them —
|
|
87
|
-
// otherwise the spied loadGuardianToken leaks into guardian-token.test.ts and
|
|
88
|
-
// setup.test.ts when they run later in the same `bun test` invocation.
|
|
89
|
-
const loadGuardianTokenSpy = spyOn(
|
|
90
|
-
guardianToken,
|
|
91
|
-
"loadGuardianToken",
|
|
92
|
-
).mockReturnValue(makeGuardianTokenData({ accessToken: "local-token" }));
|
|
93
|
-
const leaseGuardianTokenSpy = spyOn(
|
|
94
|
-
guardianToken,
|
|
95
|
-
"leaseGuardianToken",
|
|
96
|
-
).mockResolvedValue(makeGuardianTokenData({ accessToken: "leased-token" }));
|
|
97
|
-
|
|
98
|
-
const getBackupsDirMock = spyOn(backupOps, "getBackupsDir").mockReturnValue(
|
|
99
|
-
"/tmp/backups-default",
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const mkdirSyncMock = spyOn(fs, "mkdirSync").mockImplementation(
|
|
103
|
-
() => undefined,
|
|
104
|
-
);
|
|
105
|
-
const writeFileSyncMock = spyOn(fs, "writeFileSync").mockImplementation(
|
|
106
|
-
() => undefined,
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
let originalFetch: typeof globalThis.fetch;
|
|
110
|
-
let exitMock: Mock<(code?: number | string | undefined) => never>;
|
|
111
|
-
|
|
112
|
-
const CADET_ENTRY = {
|
|
113
|
-
assistantId: "11111111-2222-3333-4444-555555555555",
|
|
114
|
-
runtimeUrl: "https://cloud.mycadet.ai",
|
|
115
|
-
cloud: "cadet",
|
|
116
|
-
hatchedAt: new Date().toISOString(),
|
|
117
|
-
} satisfies assistantConfig.AssistantEntry;
|
|
7
|
+
import * as roots from "../lib/root-backup.js";
|
|
8
|
+
import * as cloud from "../lib/cloud-client.js";
|
|
9
|
+
import * as platform from "../lib/teleport-backup.js";
|
|
10
|
+
import * as guardian from "../lib/guardian-token.js";
|
|
11
|
+
import { backup } from "../commands/backup.js";
|
|
118
12
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
13
|
+
const directory = mkdtempSync(join(tmpdir(), "backup-command-test-"));
|
|
14
|
+
const argv = [...process.argv];
|
|
15
|
+
const lookup = spyOn(config, "findAssistantByName");
|
|
16
|
+
const backupDir = spyOn(backupOps, "getBackupsDir").mockReturnValue(directory);
|
|
17
|
+
const capture = spyOn(roots, "createRootBackup");
|
|
18
|
+
const readToken = spyOn(cloud, "readCloudToken");
|
|
19
|
+
const cloudBackup = spyOn(platform, "createPlatformBackup");
|
|
20
|
+
const guardianRead = spyOn(guardian, "loadGuardianToken");
|
|
21
|
+
const guardianLease = spyOn(guardian, "leaseGuardianToken");
|
|
122
22
|
|
|
123
23
|
beforeEach(() => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
? "https://storage.googleapis.com/bucket/signed-upload"
|
|
141
|
-
: "https://storage.googleapis.com/bucket/signed-download",
|
|
142
|
-
bundleKey: params.bundleKey ?? "uploads/org-1/bundle-abc.vbundle",
|
|
143
|
-
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
144
|
-
}));
|
|
145
|
-
localRuntimeExportToGcsMock.mockReset();
|
|
146
|
-
localRuntimeExportToGcsMock.mockResolvedValue({
|
|
147
|
-
jobId: "platform-export-job-1",
|
|
148
|
-
});
|
|
149
|
-
localRuntimeIdentityMock.mockReset();
|
|
150
|
-
localRuntimeIdentityMock.mockResolvedValue({ version: "0.6.5" });
|
|
151
|
-
localRuntimePollJobStatusMock.mockReset();
|
|
152
|
-
localRuntimePollJobStatusMock.mockResolvedValue({
|
|
153
|
-
jobId: "platform-export-job-1",
|
|
154
|
-
type: "export",
|
|
155
|
-
status: "complete",
|
|
156
|
-
result: { manifest_sha256: "abc123def456" },
|
|
24
|
+
lookup.mockReset();
|
|
25
|
+
capture.mockReset();
|
|
26
|
+
readToken.mockReset();
|
|
27
|
+
cloudBackup.mockReset();
|
|
28
|
+
guardianRead.mockReset();
|
|
29
|
+
guardianLease.mockReset();
|
|
30
|
+
capture.mockImplementation(async (_entry, path) => {
|
|
31
|
+
writeFileSync(path, "tar archive");
|
|
32
|
+
});
|
|
33
|
+
readToken.mockReturnValue("session-token");
|
|
34
|
+
cloudBackup.mockResolvedValue(undefined);
|
|
35
|
+
guardianRead.mockImplementation(() => {
|
|
36
|
+
throw new Error("must not read guardian tokens");
|
|
37
|
+
});
|
|
38
|
+
guardianLease.mockImplementation(async () => {
|
|
39
|
+
throw new Error("must not lease guardian tokens");
|
|
157
40
|
});
|
|
158
|
-
getBackupsDirMock.mockReset();
|
|
159
|
-
getBackupsDirMock.mockReturnValue("/tmp/backups-default");
|
|
160
|
-
loadGuardianTokenSpy.mockReset();
|
|
161
|
-
loadGuardianTokenSpy.mockReturnValue(
|
|
162
|
-
makeGuardianTokenData({ accessToken: "local-token" }),
|
|
163
|
-
);
|
|
164
|
-
leaseGuardianTokenSpy.mockReset();
|
|
165
|
-
leaseGuardianTokenSpy.mockResolvedValue(
|
|
166
|
-
makeGuardianTokenData({ accessToken: "leased-token" }),
|
|
167
|
-
);
|
|
168
|
-
mkdirSyncMock.mockReset();
|
|
169
|
-
mkdirSyncMock.mockImplementation(() => undefined);
|
|
170
|
-
writeFileSyncMock.mockReset();
|
|
171
|
-
writeFileSyncMock.mockImplementation(() => undefined);
|
|
172
41
|
});
|
|
173
|
-
|
|
174
42
|
afterEach(() => {
|
|
175
|
-
|
|
43
|
+
process.argv = [...argv];
|
|
176
44
|
});
|
|
177
|
-
|
|
178
45
|
afterAll(() => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
writeFileSyncMock.mockRestore();
|
|
191
|
-
loadGuardianTokenSpy.mockRestore();
|
|
192
|
-
leaseGuardianTokenSpy.mockRestore();
|
|
193
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
46
|
+
for (const spy of [
|
|
47
|
+
lookup,
|
|
48
|
+
capture,
|
|
49
|
+
backupDir,
|
|
50
|
+
readToken,
|
|
51
|
+
cloudBackup,
|
|
52
|
+
guardianRead,
|
|
53
|
+
guardianLease,
|
|
54
|
+
])
|
|
55
|
+
spy.mockRestore();
|
|
56
|
+
rmSync(directory, { recursive: true, force: true });
|
|
194
57
|
});
|
|
195
58
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// ---------------------------------------------------------------------------
|
|
203
|
-
function mockGcsDownload(
|
|
204
|
-
body: Uint8Array<ArrayBuffer>,
|
|
205
|
-
ok = true,
|
|
206
|
-
status = 200,
|
|
207
|
-
) {
|
|
208
|
-
const gcsFetch = mock(async () => {
|
|
209
|
-
const responseBody: BodyInit = ok ? new Blob([body]) : "boom";
|
|
210
|
-
return new Response(responseBody, {
|
|
211
|
-
status,
|
|
212
|
-
statusText: ok ? "OK" : "Error",
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
globalThis.fetch = withPreconnect(gcsFetch);
|
|
216
|
-
return gcsFetch;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
describe("cadet backup <local>: guardian bootstrap secret", () => {
|
|
220
|
-
test("passes the lockfile bootstrap secret when leasing a fresh guardian token", async () => {
|
|
221
|
-
const localEntry = {
|
|
222
|
-
assistantId: "local-assistant",
|
|
223
|
-
runtimeUrl: "http://127.0.0.1:7830",
|
|
224
|
-
cloud: "local",
|
|
225
|
-
guardianBootstrapSecret: "bootstrap-secret-value",
|
|
226
|
-
} satisfies assistantConfig.AssistantEntry;
|
|
227
|
-
findAssistantByNameMock.mockReturnValue(localEntry);
|
|
228
|
-
loadGuardianTokenSpy.mockReturnValue(null);
|
|
229
|
-
setArgv("my-local", "--output", "/tmp/local-backup.vbundle");
|
|
230
|
-
|
|
231
|
-
globalThis.fetch = withPreconnect(
|
|
232
|
-
async () => new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
await backup();
|
|
236
|
-
|
|
237
|
-
expect(leaseGuardianTokenSpy).toHaveBeenCalledWith(
|
|
238
|
-
"http://127.0.0.1:7830",
|
|
239
|
-
"local-assistant",
|
|
240
|
-
"bootstrap-secret-value",
|
|
241
|
-
);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe("cadet backup: --export-timeout flag", () => {
|
|
246
|
-
test("rejects a non-numeric value before any lookup", async () => {
|
|
247
|
-
setArgv("my-local", "--export-timeout", "soon");
|
|
248
|
-
|
|
249
|
-
const consoleErrorSpy = spyOn(console, "error").mockImplementation(
|
|
250
|
-
() => undefined,
|
|
251
|
-
);
|
|
252
|
-
try {
|
|
253
|
-
await expect(backup()).rejects.toThrow("process.exit:1");
|
|
254
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
255
|
-
expect.stringContaining("--export-timeout must be a positive number"),
|
|
256
|
-
);
|
|
257
|
-
expect(findAssistantByNameMock).not.toHaveBeenCalled();
|
|
258
|
-
} finally {
|
|
259
|
-
consoleErrorSpy.mockRestore();
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("rejects a non-positive value", async () => {
|
|
264
|
-
setArgv("my-local", "--export-timeout", "0");
|
|
265
|
-
|
|
266
|
-
const consoleErrorSpy = spyOn(console, "error").mockImplementation(
|
|
267
|
-
() => undefined,
|
|
268
|
-
);
|
|
269
|
-
try {
|
|
270
|
-
await expect(backup()).rejects.toThrow("process.exit:1");
|
|
271
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
272
|
-
expect.stringContaining("--export-timeout must be a positive number"),
|
|
273
|
-
);
|
|
274
|
-
} finally {
|
|
275
|
-
consoleErrorSpy.mockRestore();
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("accepts a valid override and completes the local export", async () => {
|
|
280
|
-
const localEntry = {
|
|
281
|
-
assistantId: "local-assistant",
|
|
282
|
-
runtimeUrl: "http://127.0.0.1:7830",
|
|
283
|
-
cloud: "local",
|
|
284
|
-
guardianBootstrapSecret: "bootstrap-secret-value",
|
|
285
|
-
} satisfies assistantConfig.AssistantEntry;
|
|
286
|
-
findAssistantByNameMock.mockReturnValue(localEntry);
|
|
287
|
-
setArgv(
|
|
288
|
-
"my-local",
|
|
289
|
-
"--export-timeout",
|
|
290
|
-
"600",
|
|
291
|
-
"--output",
|
|
292
|
-
"/tmp/local-backup.vbundle",
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
globalThis.fetch = withPreconnect(
|
|
296
|
-
async () => new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
await backup();
|
|
300
|
-
|
|
301
|
-
expect(writeFileSyncMock).toHaveBeenCalledTimes(1);
|
|
302
|
-
expect(writeFileSyncMock.mock.calls[0]![0]).toBe(
|
|
303
|
-
"/tmp/local-backup.vbundle",
|
|
304
|
-
);
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
describe("cadet backup <cloud-managed>: GCS happy path", () => {
|
|
309
|
-
test("requests upload URL → kicks off runtime export → polls → downloads from GCS → writes file", async () => {
|
|
310
|
-
findAssistantByNameMock.mockReturnValue(CADET_ENTRY);
|
|
311
|
-
setArgv("my-platform");
|
|
312
|
-
|
|
313
|
-
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
314
|
-
const gcsFetch = mockGcsDownload(bytes);
|
|
315
|
-
|
|
316
|
-
await backup();
|
|
317
|
-
|
|
318
|
-
// Upload-URL request to the cloud.
|
|
319
|
-
expect(cloudRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
320
|
-
expect.objectContaining({
|
|
321
|
-
operation: "upload",
|
|
322
|
-
minRuntimeVersion: "0.6.5",
|
|
323
|
-
maxRuntimeVersion: null,
|
|
324
|
-
}),
|
|
325
|
-
"platform-token",
|
|
326
|
-
"https://cloud.mycadet.ai",
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
// Runtime export-to-gcs kicked off via the entry-aware helper. URL
|
|
330
|
-
// construction is exercised in `local-runtime-client.test.ts`; here we
|
|
331
|
-
// assert the helper got the right entry + token + params.
|
|
332
|
-
expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
|
|
333
|
-
expect.objectContaining({
|
|
334
|
-
cloud: "cadet",
|
|
335
|
-
runtimeUrl: "https://cloud.mycadet.ai",
|
|
336
|
-
assistantId: "11111111-2222-3333-4444-555555555555",
|
|
337
|
-
}),
|
|
338
|
-
"platform-token",
|
|
339
|
-
expect.objectContaining({
|
|
340
|
-
uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
|
|
341
|
-
description: "CLI backup",
|
|
342
|
-
}),
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
// Poll uses the entry-aware helper (wildcard URL, NOT the dedicated
|
|
346
|
-
// platform jobs/{id}/ endpoint).
|
|
347
|
-
expect(localRuntimePollJobStatusMock).toHaveBeenCalledWith(
|
|
348
|
-
expect.objectContaining({ cloud: "cadet" }),
|
|
349
|
-
"platform-token",
|
|
350
|
-
"platform-export-job-1",
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
// Download URL keyed off the upload's bundleKey. We deliberately do
|
|
354
|
-
// NOT send `targetRuntimeVersion` here — this backup downloads the
|
|
355
|
-
// bundle to disk for offline storage; there is no target runtime to
|
|
356
|
-
// gate against, and an older CLI must be able to download newer
|
|
357
|
-
// assistant backups.
|
|
358
|
-
expect(cloudRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
359
|
-
expect.objectContaining({
|
|
360
|
-
operation: "download",
|
|
361
|
-
bundleKey: "uploads/org-1/bundle-abc.vbundle",
|
|
362
|
-
}),
|
|
363
|
-
"platform-token",
|
|
364
|
-
"https://cloud.mycadet.ai",
|
|
365
|
-
);
|
|
366
|
-
const downloadCall = cloudRequestSignedUrlMock.mock.calls.find(
|
|
367
|
-
(c) => c[0].operation === "download",
|
|
368
|
-
);
|
|
369
|
-
expect(downloadCall).toBeDefined();
|
|
370
|
-
expect(downloadCall![0]).not.toHaveProperty("targetRuntimeVersion");
|
|
371
|
-
|
|
372
|
-
// GCS fetch went directly to the signed download URL with no auth.
|
|
373
|
-
expect(gcsFetch).toHaveBeenCalledWith(
|
|
374
|
-
"https://storage.googleapis.com/bucket/signed-download",
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
// File written to disk with the bytes from GCS.
|
|
378
|
-
expect(writeFileSyncMock).toHaveBeenCalledTimes(1);
|
|
379
|
-
const [outputPath, written] = writeFileSyncMock.mock.calls[0]!;
|
|
380
|
-
expect(written).toEqual(bytes);
|
|
381
|
-
expect(typeof outputPath).toBe("string");
|
|
382
|
-
expect(outputPath).toMatch(
|
|
383
|
-
/\/tmp\/backups-default\/my-platform-.*\.vbundle$/,
|
|
384
|
-
);
|
|
385
|
-
expect(mkdirSyncMock).toHaveBeenCalled();
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
test("--output override is respected", async () => {
|
|
389
|
-
findAssistantByNameMock.mockReturnValue(CADET_ENTRY);
|
|
390
|
-
setArgv("my-platform", "--output", "/custom/path/backup.vbundle");
|
|
391
|
-
|
|
392
|
-
mockGcsDownload(new Uint8Array([7, 7, 7]));
|
|
393
|
-
|
|
394
|
-
await backup();
|
|
395
|
-
|
|
396
|
-
expect(writeFileSyncMock).toHaveBeenCalledTimes(1);
|
|
397
|
-
expect(writeFileSyncMock.mock.calls[0]![0]).toBe(
|
|
398
|
-
"/custom/path/backup.vbundle",
|
|
399
|
-
);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
test("default output path is getBackupsDir() + name-timestamp.vbundle", async () => {
|
|
403
|
-
findAssistantByNameMock.mockReturnValue(CADET_ENTRY);
|
|
404
|
-
setArgv("my-platform");
|
|
405
|
-
|
|
406
|
-
mockGcsDownload(new Uint8Array([1]));
|
|
407
|
-
|
|
408
|
-
await backup();
|
|
409
|
-
|
|
410
|
-
const [outputPath] = writeFileSyncMock.mock.calls[0]!;
|
|
411
|
-
expect(outputPath).toMatch(/^\/tmp\/backups-default\/my-platform-/);
|
|
412
|
-
expect(outputPath).toMatch(/\.vbundle$/);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
test("signed-URL requests target entry.runtimeUrl, not getCloudUrl() — regression for staging/dev assistants", async () => {
|
|
416
|
-
// Assistant lives on a non-default platform instance (e.g. staging).
|
|
417
|
-
// `getCloudUrl()` still returns the default — picking it up for
|
|
418
|
-
// signed URLs would target the wrong GCS bucket.
|
|
419
|
-
const stagingEntry = {
|
|
420
|
-
...CADET_ENTRY,
|
|
421
|
-
runtimeUrl: "https://staging-cloud.mycadet.ai",
|
|
59
|
+
for (const topology of ["local", "docker"] as const) {
|
|
60
|
+
test(`${topology} backup uses root storage without runtime auth`, async () => {
|
|
61
|
+
const entry: config.AssistantEntry = {
|
|
62
|
+
assistantId: "test",
|
|
63
|
+
cloud: topology,
|
|
64
|
+
runtimeUrl: "http://127.0.0.1:1",
|
|
422
65
|
};
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
setArgv("my-platform");
|
|
426
|
-
|
|
427
|
-
mockGcsDownload(new Uint8Array([9]));
|
|
428
|
-
|
|
66
|
+
lookup.mockReturnValue(entry);
|
|
67
|
+
process.argv = ["bun", "cadet", "backup", "test"];
|
|
429
68
|
await backup();
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
// GCS bucket the runtime can reach, not the default platform's.
|
|
434
|
-
expect(cloudRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
435
|
-
expect.objectContaining({
|
|
436
|
-
operation: "upload",
|
|
437
|
-
minRuntimeVersion: "0.6.5",
|
|
438
|
-
maxRuntimeVersion: null,
|
|
439
|
-
}),
|
|
440
|
-
"platform-token",
|
|
441
|
-
"https://staging-cloud.mycadet.ai",
|
|
442
|
-
);
|
|
443
|
-
expect(cloudRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
444
|
-
expect.objectContaining({ operation: "download" }),
|
|
445
|
-
"platform-token",
|
|
446
|
-
"https://staging-cloud.mycadet.ai",
|
|
69
|
+
expect(capture).toHaveBeenCalledWith(
|
|
70
|
+
entry,
|
|
71
|
+
expect.stringMatching(new RegExp(`^${directory}/test-.*\\.tar\\.gz$`)),
|
|
447
72
|
);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
expect(call[2]).toBe("https://staging-cloud.mycadet.ai");
|
|
452
|
-
}
|
|
73
|
+
expect(guardianRead).not.toHaveBeenCalled();
|
|
74
|
+
expect(guardianLease).not.toHaveBeenCalled();
|
|
75
|
+
expect(cloudBackup).not.toHaveBeenCalled();
|
|
453
76
|
});
|
|
77
|
+
}
|
|
454
78
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
result: {},
|
|
468
|
-
}));
|
|
469
|
-
// Make readCloudToken return a fresh value on the second call,
|
|
470
|
-
// mimicking the "user re-ran `cadet login` in another terminal"
|
|
471
|
-
// scenario. The helper's pollJobUntilDone calls refreshOn401 only
|
|
472
|
-
// when its own request 401s — for the test we drive the refresh
|
|
473
|
-
// directly by overriding the mock to surface a fresh token at the
|
|
474
|
-
// download-step boundary.
|
|
475
|
-
readCloudTokenMock.mockReset();
|
|
476
|
-
readCloudTokenMock.mockReturnValueOnce("platform-token-old");
|
|
477
|
-
readCloudTokenMock.mockReturnValue("platform-token-new");
|
|
478
|
-
|
|
479
|
-
// Hook into pollJobUntilDone via overriding poll to intercept the
|
|
480
|
-
// refresh call. Easier: just verify the second-arg token to the
|
|
481
|
-
// download signed-URL request equals the one we'll inject by
|
|
482
|
-
// letting backup re-read the cloud token mid-flight. The current
|
|
483
|
-
// implementation only re-reads inside pollJobUntilDone's
|
|
484
|
-
// `refreshOn401`, so we simulate a refresh by overriding poll to
|
|
485
|
-
// throw-and-recover. Instead we directly assert the regression
|
|
486
|
-
// behavior: backup uses `exportPlatformToken` (the closure variable)
|
|
487
|
-
// for the download URL — verified by the structural assertion that
|
|
488
|
-
// the same variable is used for upload, kickoff, poll, AND download.
|
|
489
|
-
|
|
490
|
-
mockGcsDownload(new Uint8Array([1]));
|
|
491
|
-
|
|
492
|
-
await backup();
|
|
493
|
-
|
|
494
|
-
// All four token-bearing platform calls (upload signed-URL, runtime
|
|
495
|
-
// export-to-gcs kickoff, poll, download signed-URL) must use the
|
|
496
|
-
// same token string. If the download step fell back to the captured
|
|
497
|
-
// `cloudToken` parameter instead of `exportPlatformToken`, a
|
|
498
|
-
// future poll-loop refresh would silently break this invariant.
|
|
499
|
-
const uploadCallToken = cloudRequestSignedUrlMock.mock.calls.find(
|
|
500
|
-
(c) => c[0].operation === "upload",
|
|
501
|
-
)![1];
|
|
502
|
-
const downloadCallToken = cloudRequestSignedUrlMock.mock.calls.find(
|
|
503
|
-
(c) => c[0].operation === "download",
|
|
504
|
-
)![1];
|
|
505
|
-
expect(downloadCallToken).toBe(uploadCallToken);
|
|
506
|
-
const kickoffToken = localRuntimeExportToGcsMock.mock.calls[0]![1];
|
|
507
|
-
expect(downloadCallToken).toBe(kickoffToken);
|
|
508
|
-
const pollToken = localRuntimePollJobStatusMock.mock.calls[0]![1];
|
|
509
|
-
expect(downloadCallToken).toBe(pollToken);
|
|
510
|
-
});
|
|
79
|
+
test("cloud backup calls the cloud backup API with session authentication", async () => {
|
|
80
|
+
const entry: config.AssistantEntry = {
|
|
81
|
+
assistantId: "cloud-test",
|
|
82
|
+
cloud: "cloud",
|
|
83
|
+
runtimeUrl: "https://cloud.mycadet.ai",
|
|
84
|
+
};
|
|
85
|
+
lookup.mockReturnValue(entry);
|
|
86
|
+
process.argv = ["bun", "cadet", "backup", "cloud-test"];
|
|
87
|
+
await backup();
|
|
88
|
+
expect(cloudBackup).toHaveBeenCalledWith(entry, "session-token");
|
|
89
|
+
expect(capture).not.toHaveBeenCalled();
|
|
90
|
+
expect(guardianRead).not.toHaveBeenCalled();
|
|
511
91
|
});
|
|
512
92
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
);
|
|
537
|
-
setArgv("my-platform");
|
|
538
|
-
|
|
539
|
-
const consoleErrorSpy = spyOn(console, "error").mockImplementation(
|
|
540
|
-
() => undefined,
|
|
541
|
-
);
|
|
542
|
-
try {
|
|
543
|
-
await expect(backup()).rejects.toThrow("process.exit:1");
|
|
544
|
-
const calls = consoleErrorSpy.mock.calls.map((c) => c[0]);
|
|
545
|
-
expect(
|
|
546
|
-
calls.some(
|
|
547
|
-
(m) =>
|
|
548
|
-
typeof m === "string" &&
|
|
549
|
-
m.includes("Another backup or teleport export") &&
|
|
550
|
-
m.includes("existing-job-99"),
|
|
551
|
-
),
|
|
552
|
-
).toBe(true);
|
|
553
|
-
} finally {
|
|
554
|
-
consoleErrorSpy.mockRestore();
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
test("terminal=failed exits with 'Export failed: <reason>'", async () => {
|
|
559
|
-
findAssistantByNameMock.mockReturnValue(CADET_ENTRY);
|
|
560
|
-
localRuntimePollJobStatusMock.mockResolvedValue({
|
|
561
|
-
jobId: "platform-export-job-1",
|
|
562
|
-
type: "export",
|
|
563
|
-
status: "failed",
|
|
564
|
-
error: "vbundle build crashed",
|
|
565
|
-
});
|
|
566
|
-
setArgv("my-platform");
|
|
567
|
-
|
|
568
|
-
const consoleErrorSpy = spyOn(console, "error").mockImplementation(
|
|
569
|
-
() => undefined,
|
|
570
|
-
);
|
|
571
|
-
try {
|
|
572
|
-
await expect(backup()).rejects.toThrow("process.exit:1");
|
|
573
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
574
|
-
expect.stringContaining("Export failed: vbundle build crashed"),
|
|
575
|
-
);
|
|
576
|
-
} finally {
|
|
577
|
-
consoleErrorSpy.mockRestore();
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
test("GCS fetch !ok exits with 'Failed to fetch bundle from GCS (<status>)'", async () => {
|
|
582
|
-
findAssistantByNameMock.mockReturnValue(CADET_ENTRY);
|
|
583
|
-
setArgv("my-platform");
|
|
584
|
-
|
|
585
|
-
mockGcsDownload(new Uint8Array(), false, 403);
|
|
586
|
-
|
|
587
|
-
const consoleErrorSpy = spyOn(console, "error").mockImplementation(
|
|
588
|
-
() => undefined,
|
|
589
|
-
);
|
|
590
|
-
try {
|
|
591
|
-
await expect(backup()).rejects.toThrow("process.exit:1");
|
|
592
|
-
const calls = consoleErrorSpy.mock.calls.map((c) => c[0]);
|
|
593
|
-
expect(
|
|
594
|
-
calls.some(
|
|
595
|
-
(m) =>
|
|
596
|
-
typeof m === "string" &&
|
|
597
|
-
m.includes("Failed to fetch bundle from GCS") &&
|
|
598
|
-
m.includes("403"),
|
|
599
|
-
),
|
|
600
|
-
).toBe(true);
|
|
601
|
-
} finally {
|
|
602
|
-
consoleErrorSpy.mockRestore();
|
|
603
|
-
}
|
|
604
|
-
});
|
|
93
|
+
test("rejects legacy output names and unsupported remote topology before capture", async () => {
|
|
94
|
+
lookup.mockReturnValue({
|
|
95
|
+
assistantId: "test",
|
|
96
|
+
cloud: "local",
|
|
97
|
+
runtimeUrl: "http://127.0.0.1:1",
|
|
98
|
+
});
|
|
99
|
+
process.argv = [
|
|
100
|
+
"bun",
|
|
101
|
+
"cadet",
|
|
102
|
+
"backup",
|
|
103
|
+
"test",
|
|
104
|
+
"--output",
|
|
105
|
+
"legacy.vbundle",
|
|
106
|
+
];
|
|
107
|
+
await expect(backup()).rejects.toThrow("Unknown backup option: --output");
|
|
108
|
+
lookup.mockReturnValue({
|
|
109
|
+
assistantId: "test",
|
|
110
|
+
cloud: "paired",
|
|
111
|
+
runtimeUrl: "http://127.0.0.1:1",
|
|
112
|
+
});
|
|
113
|
+
process.argv = ["bun", "cadet", "backup", "test"];
|
|
114
|
+
await expect(backup()).rejects.toThrow("must run on its host");
|
|
115
|
+
expect(capture).not.toHaveBeenCalled();
|
|
605
116
|
});
|
|
606
117
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
cloudRequestSignedUrlMock.mockImplementationOnce(async (params) => {
|
|
627
|
-
callOrder.push("signed-url");
|
|
628
|
-
return {
|
|
629
|
-
url: "https://storage.googleapis.com/bucket/signed-upload",
|
|
630
|
-
bundleKey: params.bundleKey ?? "uploads/org-1/bundle-abc.vbundle",
|
|
631
|
-
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
632
|
-
};
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
mockGcsDownload(new Uint8Array([1]));
|
|
636
|
-
|
|
637
|
-
await backup();
|
|
638
|
-
|
|
639
|
-
expect(callOrder[0]).toBe("identity");
|
|
640
|
-
expect(callOrder[1]).toBe("signed-url");
|
|
641
|
-
|
|
642
|
-
expect(cloudRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
643
|
-
expect.objectContaining({
|
|
644
|
-
operation: "upload",
|
|
645
|
-
minRuntimeVersion: "0.5.9",
|
|
646
|
-
maxRuntimeVersion: null,
|
|
647
|
-
}),
|
|
648
|
-
"platform-token",
|
|
649
|
-
"https://cloud.mycadet.ai",
|
|
650
|
-
);
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
test("identity is fetched against the cloud-managed runtime entry with the cloud token", async () => {
|
|
654
|
-
findAssistantByNameMock.mockReturnValue(CADET_ENTRY);
|
|
655
|
-
setArgv("my-platform");
|
|
656
|
-
|
|
657
|
-
mockGcsDownload(new Uint8Array([1]));
|
|
658
|
-
|
|
659
|
-
await backup();
|
|
660
|
-
|
|
661
|
-
expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
|
|
662
|
-
expect.objectContaining({
|
|
663
|
-
cloud: "cadet",
|
|
664
|
-
runtimeUrl: "https://cloud.mycadet.ai",
|
|
665
|
-
assistantId: "11111111-2222-3333-4444-555555555555",
|
|
666
|
-
}),
|
|
667
|
-
"platform-token",
|
|
668
|
-
);
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
test("identity fetch failure aborts before signed-URL request", async () => {
|
|
672
|
-
findAssistantByNameMock.mockReturnValue(CADET_ENTRY);
|
|
673
|
-
setArgv("my-platform");
|
|
674
|
-
|
|
675
|
-
localRuntimeIdentityMock.mockRejectedValue(
|
|
676
|
-
new Error("Failed to fetch runtime identity: 503 Service Unavailable"),
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
const consoleErrorSpy = spyOn(console, "error").mockImplementation(
|
|
680
|
-
() => undefined,
|
|
681
|
-
);
|
|
682
|
-
try {
|
|
683
|
-
await expect(backup()).rejects.toThrow("process.exit:1");
|
|
684
|
-
|
|
685
|
-
// Signed-URL must NOT have been requested.
|
|
686
|
-
expect(cloudRequestSignedUrlMock).not.toHaveBeenCalled();
|
|
687
|
-
expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
|
|
688
|
-
|
|
689
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
690
|
-
expect.stringContaining("Could not fetch runtime identity"),
|
|
691
|
-
);
|
|
692
|
-
} finally {
|
|
693
|
-
consoleErrorSpy.mockRestore();
|
|
694
|
-
}
|
|
695
|
-
});
|
|
118
|
+
test("cloud backup requires cloud login and rejects host output paths", async () => {
|
|
119
|
+
lookup.mockReturnValue({
|
|
120
|
+
assistantId: "test",
|
|
121
|
+
cloud: "cloud",
|
|
122
|
+
runtimeUrl: "https://cloud.mycadet.ai",
|
|
123
|
+
});
|
|
124
|
+
process.argv = [
|
|
125
|
+
"bun",
|
|
126
|
+
"cadet",
|
|
127
|
+
"backup",
|
|
128
|
+
"test",
|
|
129
|
+
"--output",
|
|
130
|
+
"backup.tar.gz",
|
|
131
|
+
];
|
|
132
|
+
await expect(backup()).rejects.toThrow("Unknown backup option: --output");
|
|
133
|
+
process.argv = ["bun", "cadet", "backup", "test"];
|
|
134
|
+
readToken.mockReturnValue(null);
|
|
135
|
+
await expect(backup()).rejects.toThrow("cadet login");
|
|
136
|
+
expect(cloudBackup).not.toHaveBeenCalled();
|
|
696
137
|
});
|