@mycadet/cli 0.12.11 → 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 +2 -2
- 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 +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- 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 +2 -2
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- 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-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 +2 -2
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/nginx-ingress.test.ts +536 -23
- 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 +6 -6
- 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 +3 -3
- 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.test.ts +10 -2
- 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 +51 -17
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/pair.ts +2 -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 +48 -7
- package/src/commands/upgrade.ts +78 -156
- 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 +23 -18
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login.ts +2 -1
- 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 +59 -22
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +17 -1
- 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 +1 -1
- package/src/lib/local.ts +4 -2
- package/src/lib/nginx-ingress.ts +454 -79
- 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 +1 -0
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +79 -1
- 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,666 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import type { AssistantEntry } from "../assistant-config.js";
|
|
4
|
-
import {
|
|
5
|
-
MigrationInProgressError,
|
|
6
|
-
localRuntimeExportToGcs,
|
|
7
|
-
localRuntimeIdentity,
|
|
8
|
-
localRuntimeImportFromGcs,
|
|
9
|
-
localRuntimePollJobStatus,
|
|
10
|
-
} from "../local-runtime-client.js";
|
|
11
|
-
|
|
12
|
-
const RUNTIME_URL = "http://127.0.0.1:8765";
|
|
13
|
-
const TOKEN = "local-bearer-token";
|
|
14
|
-
|
|
15
|
-
// All tests in this file exercise the local/docker code path (cloud="local"),
|
|
16
|
-
// which builds `{runtimeUrl}/v1/migrations/<subpath>` URLs and uses
|
|
17
|
-
// guardian-token bearer auth. The cloud path (cloud="cadet") is covered
|
|
18
|
-
// by `runtime-url.test.ts` (URL construction) and the teleport tests
|
|
19
|
-
// (call-site wiring).
|
|
20
|
-
const ENTRY: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId"> = {
|
|
21
|
-
cloud: "local",
|
|
22
|
-
runtimeUrl: RUNTIME_URL,
|
|
23
|
-
assistantId: "ast-test-1",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
interface CapturedCall {
|
|
27
|
-
url: string;
|
|
28
|
-
method: string;
|
|
29
|
-
headers: Record<string, string>;
|
|
30
|
-
body: unknown;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function captureFetch(
|
|
34
|
-
responder: (call: CapturedCall) => Response | Promise<Response>,
|
|
35
|
-
): {
|
|
36
|
-
calls: CapturedCall[];
|
|
37
|
-
fetchMock: typeof globalThis.fetch;
|
|
38
|
-
} {
|
|
39
|
-
const calls: CapturedCall[] = [];
|
|
40
|
-
const fetchMock = mock(
|
|
41
|
-
async (url: string | URL | Request, init?: RequestInit) => {
|
|
42
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
43
|
-
const rawHeaders = (init?.headers ?? {}) as
|
|
44
|
-
| Record<string, string>
|
|
45
|
-
| Headers;
|
|
46
|
-
const headers: Record<string, string> = {};
|
|
47
|
-
if (rawHeaders instanceof Headers) {
|
|
48
|
-
rawHeaders.forEach((v, k) => {
|
|
49
|
-
headers[k] = v;
|
|
50
|
-
});
|
|
51
|
-
} else {
|
|
52
|
-
Object.assign(headers, rawHeaders);
|
|
53
|
-
}
|
|
54
|
-
let parsedBody: unknown = undefined;
|
|
55
|
-
const b = init?.body;
|
|
56
|
-
if (typeof b === "string") {
|
|
57
|
-
try {
|
|
58
|
-
parsedBody = JSON.parse(b);
|
|
59
|
-
} catch {
|
|
60
|
-
parsedBody = b;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const call: CapturedCall = {
|
|
64
|
-
url: urlStr,
|
|
65
|
-
method: init?.method ?? "GET",
|
|
66
|
-
headers,
|
|
67
|
-
body: parsedBody,
|
|
68
|
-
};
|
|
69
|
-
calls.push(call);
|
|
70
|
-
return responder(call);
|
|
71
|
-
},
|
|
72
|
-
);
|
|
73
|
-
return { calls, fetchMock: fetchMock as unknown as typeof globalThis.fetch };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let originalFetch: typeof globalThis.fetch;
|
|
77
|
-
beforeEach(() => {
|
|
78
|
-
originalFetch = globalThis.fetch;
|
|
79
|
-
});
|
|
80
|
-
afterEach(() => {
|
|
81
|
-
globalThis.fetch = originalFetch;
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("localRuntimeExportToGcs", () => {
|
|
85
|
-
test("POSTs {upload_url, description} with Bearer auth and returns job_id on 202", async () => {
|
|
86
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
87
|
-
return new Response(
|
|
88
|
-
JSON.stringify({
|
|
89
|
-
job_id: "export-job-1",
|
|
90
|
-
status: "pending",
|
|
91
|
-
type: "export",
|
|
92
|
-
}),
|
|
93
|
-
{ status: 202, headers: { "Content-Type": "application/json" } },
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
globalThis.fetch = fetchMock;
|
|
97
|
-
|
|
98
|
-
const result = await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
99
|
-
uploadUrl: "https://storage.example/signed/abc",
|
|
100
|
-
description: "teleport export",
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
expect(result.jobId).toBe("export-job-1");
|
|
104
|
-
expect(calls).toHaveLength(1);
|
|
105
|
-
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/migrations/export-to-gcs`);
|
|
106
|
-
expect(calls[0]!.method).toBe("POST");
|
|
107
|
-
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
108
|
-
expect(calls[0]!.headers["Content-Type"]).toBe("application/json");
|
|
109
|
-
expect(calls[0]!.body).toEqual({
|
|
110
|
-
upload_url: "https://storage.example/signed/abc",
|
|
111
|
-
description: "teleport export",
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("omits description when not provided", async () => {
|
|
116
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
117
|
-
return new Response(
|
|
118
|
-
JSON.stringify({ job_id: "j", status: "pending", type: "export" }),
|
|
119
|
-
{ status: 202 },
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
globalThis.fetch = fetchMock;
|
|
123
|
-
|
|
124
|
-
await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
125
|
-
uploadUrl: "https://storage.example/signed/abc",
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
expect(calls[0]!.body).toEqual({
|
|
129
|
-
upload_url: "https://storage.example/signed/abc",
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("409 export_in_progress (nested {error:{code,job_id}}) throws MigrationInProgressError carrying existing job_id", async () => {
|
|
134
|
-
const { fetchMock } = captureFetch(() => {
|
|
135
|
-
return new Response(
|
|
136
|
-
JSON.stringify({
|
|
137
|
-
error: {
|
|
138
|
-
code: "export_in_progress",
|
|
139
|
-
job_id: "existing-export-42",
|
|
140
|
-
},
|
|
141
|
-
}),
|
|
142
|
-
{ status: 409 },
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
globalThis.fetch = fetchMock;
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
149
|
-
uploadUrl: "https://storage.example/signed/abc",
|
|
150
|
-
});
|
|
151
|
-
throw new Error("expected to throw");
|
|
152
|
-
} catch (err) {
|
|
153
|
-
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
154
|
-
const mip = err as MigrationInProgressError;
|
|
155
|
-
expect(mip.kind).toBe("export_in_progress");
|
|
156
|
-
expect(mip.existingJobId).toBe("existing-export-42");
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("409 export_in_progress regression: nested job_id 'abc-123' is surfaced (not empty)", async () => {
|
|
161
|
-
const { fetchMock } = captureFetch(() => {
|
|
162
|
-
return new Response(
|
|
163
|
-
JSON.stringify({
|
|
164
|
-
error: { code: "export_in_progress", job_id: "abc-123" },
|
|
165
|
-
}),
|
|
166
|
-
{ status: 409 },
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
globalThis.fetch = fetchMock;
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
173
|
-
uploadUrl: "https://storage.example/signed/abc",
|
|
174
|
-
});
|
|
175
|
-
throw new Error("expected to throw");
|
|
176
|
-
} catch (err) {
|
|
177
|
-
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
178
|
-
const mip = err as MigrationInProgressError;
|
|
179
|
-
expect(mip.existingJobId).toBe("abc-123");
|
|
180
|
-
expect(mip.existingJobId).not.toBe("");
|
|
181
|
-
expect(mip.kind).toBe("export_in_progress");
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("409 export_in_progress with legacy flat shape is still parsed", async () => {
|
|
186
|
-
const { fetchMock } = captureFetch(() => {
|
|
187
|
-
return new Response(
|
|
188
|
-
JSON.stringify({
|
|
189
|
-
code: "export_in_progress",
|
|
190
|
-
job_id: "legacy-export-9",
|
|
191
|
-
}),
|
|
192
|
-
{ status: 409 },
|
|
193
|
-
);
|
|
194
|
-
});
|
|
195
|
-
globalThis.fetch = fetchMock;
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
199
|
-
uploadUrl: "https://storage.example/signed/abc",
|
|
200
|
-
});
|
|
201
|
-
throw new Error("expected to throw");
|
|
202
|
-
} catch (err) {
|
|
203
|
-
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
204
|
-
const mip = err as MigrationInProgressError;
|
|
205
|
-
expect(mip.kind).toBe("export_in_progress");
|
|
206
|
-
expect(mip.existingJobId).toBe("legacy-export-9");
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("non-202 non-409 responses throw with status + body", async () => {
|
|
211
|
-
const { fetchMock } = captureFetch(() => {
|
|
212
|
-
return new Response("boom", { status: 500 });
|
|
213
|
-
});
|
|
214
|
-
globalThis.fetch = fetchMock;
|
|
215
|
-
|
|
216
|
-
await expect(
|
|
217
|
-
localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
218
|
-
uploadUrl: "https://storage.example/signed/abc",
|
|
219
|
-
}),
|
|
220
|
-
).rejects.toThrow(/500/);
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe("localRuntimeImportFromGcs", () => {
|
|
225
|
-
test("POSTs {bundle_url} with Bearer auth and returns job_id on 202", async () => {
|
|
226
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
227
|
-
return new Response(
|
|
228
|
-
JSON.stringify({
|
|
229
|
-
job_id: "import-job-1",
|
|
230
|
-
status: "pending",
|
|
231
|
-
type: "import",
|
|
232
|
-
}),
|
|
233
|
-
{ status: 202 },
|
|
234
|
-
);
|
|
235
|
-
});
|
|
236
|
-
globalThis.fetch = fetchMock;
|
|
237
|
-
|
|
238
|
-
const result = await localRuntimeImportFromGcs(ENTRY, TOKEN, {
|
|
239
|
-
bundleUrl: "https://storage.example/signed/dl-xyz",
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
expect(result.jobId).toBe("import-job-1");
|
|
243
|
-
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/migrations/import-from-gcs`);
|
|
244
|
-
expect(calls[0]!.method).toBe("POST");
|
|
245
|
-
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
246
|
-
expect(calls[0]!.body).toEqual({
|
|
247
|
-
bundle_url: "https://storage.example/signed/dl-xyz",
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("409 import_in_progress (nested {error:{code,job_id}}) throws MigrationInProgressError carrying existing job_id", async () => {
|
|
252
|
-
const { fetchMock } = captureFetch(() => {
|
|
253
|
-
return new Response(
|
|
254
|
-
JSON.stringify({
|
|
255
|
-
error: {
|
|
256
|
-
code: "import_in_progress",
|
|
257
|
-
job_id: "existing-import-7",
|
|
258
|
-
},
|
|
259
|
-
}),
|
|
260
|
-
{ status: 409 },
|
|
261
|
-
);
|
|
262
|
-
});
|
|
263
|
-
globalThis.fetch = fetchMock;
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
await localRuntimeImportFromGcs(ENTRY, TOKEN, {
|
|
267
|
-
bundleUrl: "https://storage.example/signed/dl-xyz",
|
|
268
|
-
});
|
|
269
|
-
throw new Error("expected to throw");
|
|
270
|
-
} catch (err) {
|
|
271
|
-
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
272
|
-
const mip = err as MigrationInProgressError;
|
|
273
|
-
expect(mip.kind).toBe("import_in_progress");
|
|
274
|
-
expect(mip.existingJobId).toBe("existing-import-7");
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("409 import_in_progress with legacy flat shape is still parsed", async () => {
|
|
279
|
-
const { fetchMock } = captureFetch(() => {
|
|
280
|
-
return new Response(
|
|
281
|
-
JSON.stringify({
|
|
282
|
-
code: "import_in_progress",
|
|
283
|
-
job_id: "legacy-import-2",
|
|
284
|
-
}),
|
|
285
|
-
{ status: 409 },
|
|
286
|
-
);
|
|
287
|
-
});
|
|
288
|
-
globalThis.fetch = fetchMock;
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
await localRuntimeImportFromGcs(ENTRY, TOKEN, {
|
|
292
|
-
bundleUrl: "https://storage.example/signed/dl-xyz",
|
|
293
|
-
});
|
|
294
|
-
throw new Error("expected to throw");
|
|
295
|
-
} catch (err) {
|
|
296
|
-
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
297
|
-
const mip = err as MigrationInProgressError;
|
|
298
|
-
expect(mip.kind).toBe("import_in_progress");
|
|
299
|
-
expect(mip.existingJobId).toBe("legacy-import-2");
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
describe("localRuntimePollJobStatus", () => {
|
|
305
|
-
test("GETs /v1/migrations/jobs/{jobId} with Bearer auth and parses processing", async () => {
|
|
306
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
307
|
-
return new Response(
|
|
308
|
-
JSON.stringify({
|
|
309
|
-
job_id: "poll-1",
|
|
310
|
-
type: "export",
|
|
311
|
-
status: "processing",
|
|
312
|
-
}),
|
|
313
|
-
{ status: 200 },
|
|
314
|
-
);
|
|
315
|
-
});
|
|
316
|
-
globalThis.fetch = fetchMock;
|
|
317
|
-
|
|
318
|
-
const status = await localRuntimePollJobStatus(ENTRY, TOKEN, "poll-1");
|
|
319
|
-
|
|
320
|
-
expect(status).toEqual({
|
|
321
|
-
jobId: "poll-1",
|
|
322
|
-
type: "export",
|
|
323
|
-
status: "processing",
|
|
324
|
-
});
|
|
325
|
-
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/migrations/jobs/poll-1`);
|
|
326
|
-
expect(calls[0]!.method).toBe("GET");
|
|
327
|
-
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("parses complete with bundle_key", async () => {
|
|
331
|
-
const { fetchMock } = captureFetch(() => {
|
|
332
|
-
return new Response(
|
|
333
|
-
JSON.stringify({
|
|
334
|
-
job_id: "poll-2",
|
|
335
|
-
type: "export",
|
|
336
|
-
status: "complete",
|
|
337
|
-
bundle_key: "bundles/x.tar.gz",
|
|
338
|
-
}),
|
|
339
|
-
{ status: 200 },
|
|
340
|
-
);
|
|
341
|
-
});
|
|
342
|
-
globalThis.fetch = fetchMock;
|
|
343
|
-
|
|
344
|
-
const status = await localRuntimePollJobStatus(ENTRY, TOKEN, "poll-2");
|
|
345
|
-
|
|
346
|
-
expect(status.status).toBe("complete");
|
|
347
|
-
if (status.status === "complete") {
|
|
348
|
-
expect(status.bundleKey).toBe("bundles/x.tar.gz");
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
test("parses failed with error", async () => {
|
|
353
|
-
const { fetchMock } = captureFetch(() => {
|
|
354
|
-
return new Response(
|
|
355
|
-
JSON.stringify({
|
|
356
|
-
job_id: "poll-3",
|
|
357
|
-
type: "import",
|
|
358
|
-
status: "failed",
|
|
359
|
-
error: "corrupted bundle",
|
|
360
|
-
}),
|
|
361
|
-
{ status: 200 },
|
|
362
|
-
);
|
|
363
|
-
});
|
|
364
|
-
globalThis.fetch = fetchMock;
|
|
365
|
-
|
|
366
|
-
const status = await localRuntimePollJobStatus(ENTRY, TOKEN, "poll-3");
|
|
367
|
-
|
|
368
|
-
expect(status.status).toBe("failed");
|
|
369
|
-
if (status.status === "failed") {
|
|
370
|
-
expect(status.error).toBe("corrupted bundle");
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
test("404 → throws 'Migration job not found'", async () => {
|
|
375
|
-
const { fetchMock } = captureFetch(() => {
|
|
376
|
-
return new Response("{}", { status: 404 });
|
|
377
|
-
});
|
|
378
|
-
globalThis.fetch = fetchMock;
|
|
379
|
-
|
|
380
|
-
await expect(
|
|
381
|
-
localRuntimePollJobStatus(ENTRY, TOKEN, "missing"),
|
|
382
|
-
).rejects.toThrow(/Migration job not found/);
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// ---------------------------------------------------------------------------
|
|
387
|
-
// Cloud assistants (cloud="cadet") route through the cloud's
|
|
388
|
-
// wildcard runtime proxy at `/v1/assistants/<id>/migrations/...` with
|
|
389
|
-
// platform-token auth (NOT guardian-token bearer). This block asserts the
|
|
390
|
-
// actual URL and headers built by the helpers — not mocked, not abstracted.
|
|
391
|
-
// Regression guard for the routing bug fixed in this PR.
|
|
392
|
-
// ---------------------------------------------------------------------------
|
|
393
|
-
const CADET_ENTRY: Pick<
|
|
394
|
-
AssistantEntry,
|
|
395
|
-
"cloud" | "runtimeUrl" | "assistantId"
|
|
396
|
-
> = {
|
|
397
|
-
cloud: "cadet",
|
|
398
|
-
runtimeUrl: "https://cloud.mycadet.ai",
|
|
399
|
-
assistantId: "11111111-2222-3333-4444-555555555555",
|
|
400
|
-
};
|
|
401
|
-
// `vak_` prefix bypasses `fetchOrganizationId` (org-scoped API keys); the
|
|
402
|
-
// auth header collapses to a single `Authorization: Bearer vak_...` so this
|
|
403
|
-
// test stays free of network mocks.
|
|
404
|
-
const VAK_TOKEN = "vak_platform-token";
|
|
405
|
-
|
|
406
|
-
describe("cadet-cloud routing through wildcard proxy", () => {
|
|
407
|
-
test("export-to-gcs URL has /v1/assistants/<id>/migrations/ prefix and uses platform-token bearer (no guardian)", async () => {
|
|
408
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
409
|
-
return new Response(
|
|
410
|
-
JSON.stringify({ job_id: "wp-export-1", status: "pending" }),
|
|
411
|
-
{ status: 202, headers: { "Content-Type": "application/json" } },
|
|
412
|
-
);
|
|
413
|
-
});
|
|
414
|
-
globalThis.fetch = fetchMock;
|
|
415
|
-
|
|
416
|
-
const result = await localRuntimeExportToGcs(CADET_ENTRY, VAK_TOKEN, {
|
|
417
|
-
uploadUrl: "https://storage.example/signed/x",
|
|
418
|
-
description: "teleport export",
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
expect(result.jobId).toBe("wp-export-1");
|
|
422
|
-
expect(calls[0]!.url).toBe(
|
|
423
|
-
`https://cloud.mycadet.ai/v1/assistants/11111111-2222-3333-4444-555555555555/migrations/export-to-gcs`,
|
|
424
|
-
);
|
|
425
|
-
expect(calls[0]!.method).toBe("POST");
|
|
426
|
-
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${VAK_TOKEN}`);
|
|
427
|
-
expect(calls[0]!.body).toEqual({
|
|
428
|
-
upload_url: "https://storage.example/signed/x",
|
|
429
|
-
description: "teleport export",
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
test("import-from-gcs URL has /v1/assistants/<id>/migrations/ prefix", async () => {
|
|
434
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
435
|
-
return new Response(
|
|
436
|
-
JSON.stringify({ job_id: "wp-import-1", status: "pending" }),
|
|
437
|
-
{ status: 202 },
|
|
438
|
-
);
|
|
439
|
-
});
|
|
440
|
-
globalThis.fetch = fetchMock;
|
|
441
|
-
|
|
442
|
-
await localRuntimeImportFromGcs(CADET_ENTRY, VAK_TOKEN, {
|
|
443
|
-
bundleUrl: "https://storage.example/download/y",
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
expect(calls[0]!.url).toBe(
|
|
447
|
-
`https://cloud.mycadet.ai/v1/assistants/11111111-2222-3333-4444-555555555555/migrations/import-from-gcs`,
|
|
448
|
-
);
|
|
449
|
-
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${VAK_TOKEN}`);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
test("jobs/<id> URL has /v1/assistants/<id>/migrations/ prefix (NOT the dedicated platform endpoint)", async () => {
|
|
453
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
454
|
-
return new Response(
|
|
455
|
-
JSON.stringify({
|
|
456
|
-
job_id: "wp-export-1",
|
|
457
|
-
status: "complete",
|
|
458
|
-
type: "export",
|
|
459
|
-
bundle_key: "exports/org-1/x.vbundle",
|
|
460
|
-
}),
|
|
461
|
-
{ status: 200 },
|
|
462
|
-
);
|
|
463
|
-
});
|
|
464
|
-
globalThis.fetch = fetchMock;
|
|
465
|
-
|
|
466
|
-
const status = await localRuntimePollJobStatus(
|
|
467
|
-
CADET_ENTRY,
|
|
468
|
-
VAK_TOKEN,
|
|
469
|
-
"wp-export-1",
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
expect(calls[0]!.url).toBe(
|
|
473
|
-
`https://cloud.mycadet.ai/v1/assistants/11111111-2222-3333-4444-555555555555/migrations/jobs/wp-export-1`,
|
|
474
|
-
);
|
|
475
|
-
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${VAK_TOKEN}`);
|
|
476
|
-
expect(status.status).toBe("complete");
|
|
477
|
-
if (status.status === "complete") {
|
|
478
|
-
expect(status.bundleKey).toBe("exports/org-1/x.vbundle");
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
describe("localRuntimeIdentity", () => {
|
|
484
|
-
test("local entry: GETs /v1/health with Bearer auth and returns the version", async () => {
|
|
485
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
486
|
-
return new Response(
|
|
487
|
-
JSON.stringify({
|
|
488
|
-
status: "healthy",
|
|
489
|
-
timestamp: "2025-01-01T00:00:00Z",
|
|
490
|
-
version: "0.6.5",
|
|
491
|
-
}),
|
|
492
|
-
{
|
|
493
|
-
status: 200,
|
|
494
|
-
headers: { "Content-Type": "application/json" },
|
|
495
|
-
},
|
|
496
|
-
);
|
|
497
|
-
});
|
|
498
|
-
globalThis.fetch = fetchMock;
|
|
499
|
-
|
|
500
|
-
const result = await localRuntimeIdentity(ENTRY, TOKEN);
|
|
501
|
-
|
|
502
|
-
expect(result.version).toBe("0.6.5");
|
|
503
|
-
expect(calls).toHaveLength(1);
|
|
504
|
-
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/health`);
|
|
505
|
-
expect(calls[0]!.method).toBe("GET");
|
|
506
|
-
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test("GETs /v1/health, not /v1/identity (works on pre-onboarding runtimes)", async () => {
|
|
510
|
-
// Regression guard: /v1/identity reads IDENTITY.md (written during
|
|
511
|
-
// onboarding, NOT hatch) and 404s on freshly-hatched targets. /v1/health
|
|
512
|
-
// returns the version field unconditionally, so it's the right source.
|
|
513
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
514
|
-
return new Response(JSON.stringify({ version: "0.7.0" }), {
|
|
515
|
-
status: 200,
|
|
516
|
-
});
|
|
517
|
-
});
|
|
518
|
-
globalThis.fetch = fetchMock;
|
|
519
|
-
|
|
520
|
-
await localRuntimeIdentity(ENTRY, TOKEN);
|
|
521
|
-
|
|
522
|
-
expect(calls[0]!.url.endsWith("/v1/health")).toBe(true);
|
|
523
|
-
expect(calls[0]!.url).not.toContain("/v1/identity");
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
test("cadet entry: GETs /v1/assistants/<id>/health through the wildcard proxy", async () => {
|
|
527
|
-
const { calls, fetchMock } = captureFetch(() => {
|
|
528
|
-
return new Response(JSON.stringify({ version: "0.7.2" }), {
|
|
529
|
-
status: 200,
|
|
530
|
-
});
|
|
531
|
-
});
|
|
532
|
-
globalThis.fetch = fetchMock;
|
|
533
|
-
|
|
534
|
-
const result = await localRuntimeIdentity(CADET_ENTRY, VAK_TOKEN);
|
|
535
|
-
|
|
536
|
-
expect(result.version).toBe("0.7.2");
|
|
537
|
-
expect(calls[0]!.url).toBe(
|
|
538
|
-
`https://cloud.mycadet.ai/v1/assistants/11111111-2222-3333-4444-555555555555/health`,
|
|
539
|
-
);
|
|
540
|
-
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${VAK_TOKEN}`);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
test("non-2xx status throws with status + statusText", async () => {
|
|
544
|
-
const { fetchMock } = captureFetch(() => {
|
|
545
|
-
return new Response("nope", {
|
|
546
|
-
status: 503,
|
|
547
|
-
statusText: "Service Unavailable",
|
|
548
|
-
});
|
|
549
|
-
});
|
|
550
|
-
globalThis.fetch = fetchMock;
|
|
551
|
-
|
|
552
|
-
await expect(localRuntimeIdentity(ENTRY, TOKEN)).rejects.toThrow(
|
|
553
|
-
/Failed to fetch runtime identity: 503/,
|
|
554
|
-
);
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
test("missing version in body throws", async () => {
|
|
558
|
-
const { fetchMock } = captureFetch(() => {
|
|
559
|
-
return new Response(JSON.stringify({}), { status: 200 });
|
|
560
|
-
});
|
|
561
|
-
globalThis.fetch = fetchMock;
|
|
562
|
-
|
|
563
|
-
await expect(localRuntimeIdentity(ENTRY, TOKEN)).rejects.toThrow(
|
|
564
|
-
/Runtime identity response missing version/,
|
|
565
|
-
);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
test("non-string version in body throws", async () => {
|
|
569
|
-
const { fetchMock } = captureFetch(() => {
|
|
570
|
-
return new Response(JSON.stringify({ version: 123 }), { status: 200 });
|
|
571
|
-
});
|
|
572
|
-
globalThis.fetch = fetchMock;
|
|
573
|
-
|
|
574
|
-
await expect(localRuntimeIdentity(ENTRY, TOKEN)).rejects.toThrow(
|
|
575
|
-
/Runtime identity response missing version/,
|
|
576
|
-
);
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
test("empty-string version in body throws", async () => {
|
|
580
|
-
const { fetchMock } = captureFetch(() => {
|
|
581
|
-
return new Response(JSON.stringify({ version: "" }), { status: 200 });
|
|
582
|
-
});
|
|
583
|
-
globalThis.fetch = fetchMock;
|
|
584
|
-
|
|
585
|
-
await expect(localRuntimeIdentity(ENTRY, TOKEN)).rejects.toThrow(
|
|
586
|
-
/Runtime identity response missing version/,
|
|
587
|
-
);
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
// ---------------------------------------------------------------------
|
|
591
|
-
// 401-retry parity with cloudRequestSignedUrl (Codex P2 regression
|
|
592
|
-
// guard). localRuntimeIdentity is the first network call in the
|
|
593
|
-
// backup/teleport export flow for cadet-cloud assistants, so a stale
|
|
594
|
-
// Cadet-Organization-Id cache entry would surface as a hard abort
|
|
595
|
-
// before any other helper got a chance to clear the cache and retry.
|
|
596
|
-
// ---------------------------------------------------------------------
|
|
597
|
-
|
|
598
|
-
test("cadet entry: retries once after 401 with a fresh org-ID lookup", async () => {
|
|
599
|
-
// Use a non-vak session token so authHeaders fetches + caches an org ID.
|
|
600
|
-
const SESSION_TOKEN = "session-abcdef";
|
|
601
|
-
const PLATFORM_URL = "https://cloud.mycadet.ai";
|
|
602
|
-
const ASSISTANT_ID = "11111111-2222-3333-4444-555555555555";
|
|
603
|
-
|
|
604
|
-
let healthCalls = 0;
|
|
605
|
-
const orgIdFetchedAs: string[] = [];
|
|
606
|
-
|
|
607
|
-
const fetchMock = mock(async (url: string | URL | Request) => {
|
|
608
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
609
|
-
if (urlStr.endsWith("/v1/organizations/")) {
|
|
610
|
-
// Each org-ID fetch returns a different ID to prove that the
|
|
611
|
-
// second health request DID re-resolve the org rather than
|
|
612
|
-
// reuse a stale cache entry.
|
|
613
|
-
orgIdFetchedAs.push(`org-${orgIdFetchedAs.length + 1}`);
|
|
614
|
-
return new Response(
|
|
615
|
-
JSON.stringify({
|
|
616
|
-
results: [{ id: orgIdFetchedAs[orgIdFetchedAs.length - 1]!, name: "My Cadet" }],
|
|
617
|
-
}),
|
|
618
|
-
{ status: 200 },
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
if (urlStr.endsWith(`/v1/assistants/${ASSISTANT_ID}/health`)) {
|
|
622
|
-
healthCalls += 1;
|
|
623
|
-
if (healthCalls === 1) {
|
|
624
|
-
return new Response("unauthorized", { status: 401 });
|
|
625
|
-
}
|
|
626
|
-
return new Response(JSON.stringify({ version: "0.7.4" }), {
|
|
627
|
-
status: 200,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
return new Response("unexpected", { status: 500 });
|
|
631
|
-
});
|
|
632
|
-
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
|
|
633
|
-
|
|
634
|
-
const result = await localRuntimeIdentity(
|
|
635
|
-
{
|
|
636
|
-
cloud: "cadet",
|
|
637
|
-
runtimeUrl: PLATFORM_URL,
|
|
638
|
-
assistantId: ASSISTANT_ID,
|
|
639
|
-
},
|
|
640
|
-
SESSION_TOKEN,
|
|
641
|
-
);
|
|
642
|
-
|
|
643
|
-
expect(result.version).toBe("0.7.4");
|
|
644
|
-
expect(healthCalls).toBe(2);
|
|
645
|
-
// Two org-ID fetches: the first to satisfy the initial authHeaders
|
|
646
|
-
// call, the second after the 401-driven cache invalidation.
|
|
647
|
-
expect(orgIdFetchedAs).toEqual(["org-1", "org-2"]);
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
test("local entry: does NOT retry after 401 (guardian-token refresh is the caller's job via callRuntimeWithAuthRetry)", async () => {
|
|
651
|
-
let identityCalls = 0;
|
|
652
|
-
const fetchMock = mock(async () => {
|
|
653
|
-
identityCalls += 1;
|
|
654
|
-
return new Response("unauthorized", {
|
|
655
|
-
status: 401,
|
|
656
|
-
statusText: "Unauthorized",
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
|
|
660
|
-
|
|
661
|
-
await expect(localRuntimeIdentity(ENTRY, TOKEN)).rejects.toThrow(
|
|
662
|
-
/Failed to fetch runtime identity: 401/,
|
|
663
|
-
);
|
|
664
|
-
expect(identityCalls).toBe(1);
|
|
665
|
-
});
|
|
666
|
-
});
|