@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.
Files changed (94) hide show
  1. package/README.md +13 -0
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  3. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  4. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  6. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  7. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  9. package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  10. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  11. package/package.json +8 -3
  12. package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
  13. package/src/__tests__/backup-ops-matching.test.ts +15 -13
  14. package/src/__tests__/backup-ops.test.ts +105 -0
  15. package/src/__tests__/backup.test.ts +114 -673
  16. package/src/__tests__/cloud-releases.test.ts +82 -0
  17. package/src/__tests__/commands-mock.ts +101 -0
  18. package/src/__tests__/debug.test.ts +17 -91
  19. package/src/__tests__/devices.test.ts +2 -2
  20. package/src/__tests__/gateway-token.test.ts +267 -0
  21. package/src/__tests__/host-teleport-docker.test.ts +371 -0
  22. package/src/__tests__/host-teleport.test.ts +405 -0
  23. package/src/__tests__/nginx-ingress.test.ts +536 -23
  24. package/src/__tests__/orphan-detection.test.ts +1 -1
  25. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  26. package/src/__tests__/ps-cloud-status.test.ts +6 -6
  27. package/src/__tests__/root-backup-docker.test.ts +294 -0
  28. package/src/__tests__/root-backup.test.ts +527 -0
  29. package/src/__tests__/root-restore-command.test.ts +147 -0
  30. package/src/__tests__/root-signing-key.test.ts +50 -0
  31. package/src/__tests__/setup.test.ts +3 -3
  32. package/src/__tests__/shell-mode.test.ts +83 -0
  33. package/src/__tests__/sleep.test.ts +11 -0
  34. package/src/__tests__/teleport.test.ts +68 -2996
  35. package/src/__tests__/tunnel.test.ts +10 -2
  36. package/src/__tests__/upgrade-local.test.ts +21 -11
  37. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  38. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  39. package/src/__tests__/wake.test.ts +4 -1
  40. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  41. package/src/commands/backup.ts +44 -399
  42. package/src/commands/client.ts +3 -3
  43. package/src/commands/completions.ts +0 -2
  44. package/src/commands/debug.ts +75 -131
  45. package/src/commands/devices.ts +51 -17
  46. package/src/commands/gateway/token.ts +77 -23
  47. package/src/commands/pair.ts +2 -2
  48. package/src/commands/restore.ts +77 -444
  49. package/src/commands/retire.ts +2 -3
  50. package/src/commands/sleep.ts +15 -2
  51. package/src/commands/teleport.ts +47 -1559
  52. package/src/commands/tunnel.ts +48 -7
  53. package/src/commands/upgrade.ts +78 -156
  54. package/src/commands/wake.ts +1 -1
  55. package/src/index.ts +0 -4
  56. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  57. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  58. package/src/lib/__tests__/web-dist.test.ts +27 -1
  59. package/src/lib/assistant-client.ts +1 -1
  60. package/src/lib/assistant-config.ts +23 -18
  61. package/src/lib/backup-ops.ts +37 -251
  62. package/src/lib/cloud-client.ts +10 -13
  63. package/src/lib/cloud-login.ts +2 -1
  64. package/src/lib/cloud-releases.ts +54 -10
  65. package/src/lib/cloud-values.ts +1 -1
  66. package/src/lib/confirm-action.ts +2 -2
  67. package/src/lib/docker.ts +59 -22
  68. package/src/lib/github-repository.ts +2 -0
  69. package/src/lib/guardian-token.ts +1 -23
  70. package/src/lib/hatch-local.ts +17 -1
  71. package/src/lib/host-teleport.ts +506 -0
  72. package/src/lib/http-client.ts +25 -0
  73. package/src/lib/ingress-config.ts +31 -0
  74. package/src/lib/live-voice/tunnel.test.ts +1 -1
  75. package/src/lib/live-voice/tunnel.ts +1 -1
  76. package/src/lib/local.ts +4 -2
  77. package/src/lib/nginx-ingress.ts +454 -79
  78. package/src/lib/process.ts +13 -0
  79. package/src/lib/root-backup.ts +917 -0
  80. package/src/lib/root-signing-key.ts +31 -0
  81. package/src/lib/runtime-url.ts +0 -29
  82. package/src/lib/teleport-backup.ts +3 -16
  83. package/src/lib/tunnel.ts +1 -0
  84. package/src/lib/upgrade-lifecycle.ts +63 -141
  85. package/src/lib/web-dist.ts +79 -1
  86. package/src/__tests__/bundle-staging.test.ts +0 -268
  87. package/src/__tests__/unpair.test.ts +0 -160
  88. package/src/commands/rollback.ts +0 -479
  89. package/src/commands/unpair.ts +0 -122
  90. package/src/lib/__tests__/job-polling.test.ts +0 -278
  91. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  92. package/src/lib/bundle-staging.ts +0 -220
  93. package/src/lib/job-polling.ts +0 -195
  94. 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
- });