@vellumai/cli 0.6.6 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/AGENTS.md +8 -2
  2. package/README.md +49 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/assistant-config.test.ts +1 -7
  5. package/src/__tests__/backup.test.ts +475 -0
  6. package/src/__tests__/config-utils.test.ts +146 -0
  7. package/src/__tests__/env-drift.test.ts +10 -32
  8. package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
  9. package/src/__tests__/multi-local.test.ts +0 -5
  10. package/src/__tests__/sleep.test.ts +1 -2
  11. package/src/__tests__/teleport.test.ts +988 -1266
  12. package/src/commands/backup.ts +117 -71
  13. package/src/commands/client.ts +10 -9
  14. package/src/commands/env.ts +93 -0
  15. package/src/commands/events.ts +2 -0
  16. package/src/commands/exec.ts +58 -13
  17. package/src/commands/login.ts +77 -12
  18. package/src/commands/logs.ts +2 -7
  19. package/src/commands/ps.ts +144 -25
  20. package/src/commands/restore.ts +26 -47
  21. package/src/commands/sleep.ts +5 -2
  22. package/src/commands/ssh.ts +17 -7
  23. package/src/commands/teleport.ts +462 -584
  24. package/src/commands/terminal.ts +9 -221
  25. package/src/commands/tunnel.ts +2 -7
  26. package/src/commands/upgrade.ts +108 -7
  27. package/src/commands/wake.ts +2 -1
  28. package/src/components/DefaultMainScreen.tsx +328 -154
  29. package/src/index.ts +5 -7
  30. package/src/lib/__tests__/docker.test.ts +50 -74
  31. package/src/lib/__tests__/job-polling.test.ts +278 -0
  32. package/src/lib/__tests__/local-runtime-client.test.ts +480 -0
  33. package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
  34. package/src/lib/__tests__/runtime-url.test.ts +87 -0
  35. package/src/lib/__tests__/terminal-session.test.ts +202 -0
  36. package/src/lib/assistant-client.ts +5 -21
  37. package/src/lib/assistant-config.ts +46 -24
  38. package/src/lib/cli-error.ts +1 -0
  39. package/src/lib/client-identity.ts +67 -0
  40. package/src/lib/docker.ts +75 -77
  41. package/src/lib/environments/__tests__/paths.test.ts +2 -0
  42. package/src/lib/environments/resolve.ts +89 -7
  43. package/src/lib/environments/seeds.ts +8 -5
  44. package/src/lib/environments/types.ts +10 -0
  45. package/src/lib/hatch-local.ts +15 -120
  46. package/src/lib/health-check.ts +98 -0
  47. package/src/lib/job-polling.ts +195 -0
  48. package/src/lib/local-runtime-client.ts +231 -0
  49. package/src/lib/local.ts +165 -72
  50. package/src/lib/orphan-detection.ts +2 -35
  51. package/src/lib/platform-client.ts +190 -194
  52. package/src/lib/platform-releases.ts +23 -0
  53. package/src/lib/retire-local.ts +6 -2
  54. package/src/lib/runtime-url.ts +30 -0
  55. package/src/lib/sync-cloud-assistants.ts +126 -0
  56. package/src/lib/terminal-client.ts +6 -1
  57. package/src/lib/terminal-session.ts +536 -0
  58. package/src/lib/tui-log.ts +60 -0
  59. package/src/lib/xdg-log.ts +10 -4
  60. package/src/shared/provider-env-vars.ts +2 -3
  61. package/src/__tests__/orphan-detection.test.ts +0 -214
@@ -13,7 +13,7 @@ import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
 
15
15
  // ---------------------------------------------------------------------------
16
- // Temp directory for lockfile isolation (same pattern as assistant-config.test.ts)
16
+ // Temp directory for lockfile isolation
17
17
  // ---------------------------------------------------------------------------
18
18
 
19
19
  const testDir = mkdtempSync(join(tmpdir(), "cli-teleport-test-"));
@@ -23,16 +23,10 @@ process.env.VELLUM_LOCKFILE_DIR = testDir;
23
23
  // Mocks — must be set up before importing the module under test
24
24
  // ---------------------------------------------------------------------------
25
25
 
26
- // Import the real modules — do NOT mock them with mock.module() because
27
- // Bun's mock.module() replaces modules globally and the replacement leaks
28
- // into other test files (e.g. platform-client.test.ts, guardian-token.test.ts,
29
- // multi-local.test.ts) running in the same process with no way to unmock.
30
- // Instead, we use spyOn() on the imported module namespace objects — these
31
- // mutate only the current process's live binding and are restored via
32
- // mockRestore() in afterAll.
33
26
  import * as assistantConfig from "../lib/assistant-config.js";
34
27
  import * as guardianToken from "../lib/guardian-token.js";
35
28
  import * as platformClient from "../lib/platform-client.js";
29
+ import * as localRuntimeClient from "../lib/local-runtime-client.js";
36
30
 
37
31
  const findAssistantByNameMock = spyOn(
38
32
  assistantConfig,
@@ -97,49 +91,31 @@ const hatchAssistantMock = spyOn(
97
91
  reusedExisting: false,
98
92
  });
99
93
 
100
- const platformInitiateExportMock = spyOn(
94
+ const platformPollJobStatusMock = spyOn(
101
95
  platformClient,
102
- "platformInitiateExport",
96
+ "platformPollJobStatus",
103
97
  ).mockResolvedValue({
104
- jobId: "job-1",
105
- status: "pending",
98
+ jobId: "platform-job-1",
99
+ type: "export",
100
+ status: "complete",
101
+ bundleKey: "platform-bundle-key-abc",
106
102
  });
107
103
 
108
- const platformPollExportStatusMock = spyOn(
104
+ const platformRequestSignedUrlMock = spyOn(
109
105
  platformClient,
110
- "platformPollExportStatus",
111
- ).mockResolvedValue({
112
- status: "complete" as string,
113
- downloadUrl: "https://cdn.example.com/bundle.tar.gz",
114
- });
115
-
116
- const platformDownloadExportMock = spyOn(
117
- platformClient,
118
- "platformDownloadExport",
119
- ).mockImplementation(async () => {
120
- const data = new Uint8Array([10, 20, 30]);
121
- return new Response(data, { status: 200 });
122
- });
123
-
124
- const platformImportPreflightMock = spyOn(
125
- platformClient,
126
- "platformImportPreflight",
127
- ).mockResolvedValue({
128
- statusCode: 200,
129
- body: {
130
- can_import: true,
131
- summary: {
132
- files_to_create: 2,
133
- files_to_overwrite: 1,
134
- files_unchanged: 0,
135
- total_files: 3,
136
- },
137
- } as Record<string, unknown>,
138
- });
106
+ "platformRequestSignedUrl",
107
+ ).mockImplementation(async (params) => ({
108
+ url:
109
+ params.operation === "upload"
110
+ ? "https://storage.googleapis.com/bucket/signed-upload"
111
+ : "https://storage.googleapis.com/bucket/signed-download",
112
+ bundleKey: params.bundleKey ?? "bundle-key-123",
113
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
114
+ }));
139
115
 
140
- const platformImportBundleMock = spyOn(
116
+ const platformImportBundleFromGcsMock = spyOn(
141
117
  platformClient,
142
- "platformImportBundle",
118
+ "platformImportBundleFromGcs",
143
119
  ).mockResolvedValue({
144
120
  statusCode: 200,
145
121
  body: {
@@ -154,20 +130,6 @@ const platformImportBundleMock = spyOn(
154
130
  } as Record<string, unknown>,
155
131
  });
156
132
 
157
- const platformRequestUploadUrlMock = spyOn(
158
- platformClient,
159
- "platformRequestUploadUrl",
160
- ).mockResolvedValue({
161
- uploadUrl: "https://storage.googleapis.com/bucket/signed-upload-url",
162
- bundleKey: "bundle-key-123",
163
- expiresAt: new Date(Date.now() + 3600_000).toISOString(),
164
- });
165
-
166
- const platformUploadToSignedUrlMock = spyOn(
167
- platformClient,
168
- "platformUploadToSignedUrl",
169
- ).mockResolvedValue(undefined);
170
-
171
133
  const platformImportPreflightFromGcsMock = spyOn(
172
134
  platformClient,
173
135
  "platformImportPreflightFromGcs",
@@ -184,23 +146,6 @@ const platformImportPreflightFromGcsMock = spyOn(
184
146
  } as Record<string, unknown>,
185
147
  });
186
148
 
187
- const platformImportBundleFromGcsMock = spyOn(
188
- platformClient,
189
- "platformImportBundleFromGcs",
190
- ).mockResolvedValue({
191
- statusCode: 200,
192
- body: {
193
- success: true,
194
- summary: {
195
- total_files: 3,
196
- files_created: 2,
197
- files_overwritten: 1,
198
- files_skipped: 0,
199
- backups_created: 1,
200
- },
201
- } as Record<string, unknown>,
202
- });
203
-
204
149
  const checkExistingPlatformAssistantMock = spyOn(
205
150
  platformClient,
206
151
  "checkExistingPlatformAssistant",
@@ -241,6 +186,35 @@ const fetchOrganizationIdMock = spyOn(
241
186
  "fetchOrganizationId",
242
187
  ).mockResolvedValue("org-1");
243
188
 
189
+ const localRuntimeExportToGcsMock = spyOn(
190
+ localRuntimeClient,
191
+ "localRuntimeExportToGcs",
192
+ ).mockResolvedValue({ jobId: "local-export-job-1" });
193
+
194
+ const localRuntimeImportFromGcsMock = spyOn(
195
+ localRuntimeClient,
196
+ "localRuntimeImportFromGcs",
197
+ ).mockResolvedValue({ jobId: "local-import-job-1" });
198
+
199
+ const localRuntimePollJobStatusMock = spyOn(
200
+ localRuntimeClient,
201
+ "localRuntimePollJobStatus",
202
+ ).mockImplementation(async (_runtimeUrl, _token, jobId) => ({
203
+ jobId,
204
+ type: jobId.includes("import") ? "import" : "export",
205
+ status: "complete",
206
+ result: {
207
+ success: true,
208
+ summary: {
209
+ total_files: 3,
210
+ files_created: 2,
211
+ files_overwritten: 1,
212
+ files_skipped: 0,
213
+ backups_created: 1,
214
+ },
215
+ },
216
+ }));
217
+
244
218
  const hatchLocalMock = mock(async () => {});
245
219
 
246
220
  mock.module("../lib/hatch-local.js", () => ({
@@ -312,20 +286,18 @@ afterAll(() => {
312
286
  getPlatformUrlMock.mockRestore();
313
287
  hatchAssistantMock.mockRestore();
314
288
  checkExistingPlatformAssistantMock.mockRestore();
315
- platformInitiateExportMock.mockRestore();
316
- platformPollExportStatusMock.mockRestore();
317
- platformDownloadExportMock.mockRestore();
318
- platformImportPreflightMock.mockRestore();
319
- platformImportBundleMock.mockRestore();
320
- platformRequestUploadUrlMock.mockRestore();
321
- platformUploadToSignedUrlMock.mockRestore();
322
- platformImportPreflightFromGcsMock.mockRestore();
289
+ platformPollJobStatusMock.mockRestore();
290
+ platformRequestSignedUrlMock.mockRestore();
323
291
  platformImportBundleFromGcsMock.mockRestore();
292
+ platformImportPreflightFromGcsMock.mockRestore();
324
293
  ensureSelfHostedLocalRegistrationMock.mockRestore();
325
294
  injectCredentialsIntoAssistantMock.mockRestore();
326
295
  fetchCurrentUserMock.mockRestore();
327
296
  fetchOrganizationIdMock.mockRestore();
328
297
  computeDeviceIdMock.mockRestore();
298
+ localRuntimeExportToGcsMock.mockRestore();
299
+ localRuntimeImportFromGcsMock.mockRestore();
300
+ localRuntimePollJobStatusMock.mockRestore();
329
301
  rmSync(testDir, { recursive: true, force: true });
330
302
  delete process.env.VELLUM_LOCKFILE_DIR;
331
303
  });
@@ -335,11 +307,39 @@ let exitMock: ReturnType<typeof mock>;
335
307
  let originalExit: typeof process.exit;
336
308
  let consoleLogSpy: ReturnType<typeof spyOn>;
337
309
  let consoleErrorSpy: ReturnType<typeof spyOn>;
310
+ let fetchCalls: Array<{ url: string; body: unknown }>;
311
+
312
+ function defaultLocalRuntimePollImpl(
313
+ _entry: unknown,
314
+ _token: string,
315
+ jobId: string,
316
+ ): Promise<{
317
+ jobId: string;
318
+ type: "export" | "import";
319
+ status: "complete";
320
+ result: Record<string, unknown>;
321
+ }> {
322
+ return Promise.resolve({
323
+ jobId,
324
+ type: jobId.includes("import") ? "import" : "export",
325
+ status: "complete",
326
+ result: {
327
+ success: true,
328
+ summary: {
329
+ total_files: 3,
330
+ files_created: 2,
331
+ files_overwritten: 1,
332
+ files_skipped: 0,
333
+ backups_created: 1,
334
+ },
335
+ },
336
+ });
337
+ }
338
338
 
339
339
  beforeEach(() => {
340
340
  originalArgv = [...process.argv];
341
+ fetchCalls = [];
341
342
 
342
- // Reset all mocks
343
343
  findAssistantByNameMock.mockReset();
344
344
  findAssistantByNameMock.mockReturnValue(null);
345
345
  saveAssistantEntryMock.mockReset();
@@ -369,35 +369,24 @@ beforeEach(() => {
369
369
  },
370
370
  reusedExisting: false,
371
371
  });
372
- platformInitiateExportMock.mockReset();
373
- platformInitiateExportMock.mockResolvedValue({
374
- jobId: "job-1",
375
- status: "pending",
376
- });
377
- platformPollExportStatusMock.mockReset();
378
- platformPollExportStatusMock.mockResolvedValue({
372
+ platformPollJobStatusMock.mockReset();
373
+ platformPollJobStatusMock.mockResolvedValue({
374
+ jobId: "platform-job-1",
375
+ type: "export",
379
376
  status: "complete",
380
- downloadUrl: "https://cdn.example.com/bundle.tar.gz",
381
- });
382
- platformDownloadExportMock.mockReset();
383
- platformDownloadExportMock.mockResolvedValue(
384
- new Response(new Uint8Array([10, 20, 30]), { status: 200 }),
385
- );
386
- platformImportPreflightMock.mockReset();
387
- platformImportPreflightMock.mockResolvedValue({
388
- statusCode: 200,
389
- body: {
390
- can_import: true,
391
- summary: {
392
- files_to_create: 2,
393
- files_to_overwrite: 1,
394
- files_unchanged: 0,
395
- total_files: 3,
396
- },
397
- },
398
- });
399
- platformImportBundleMock.mockReset();
400
- platformImportBundleMock.mockResolvedValue({
377
+ bundleKey: "platform-bundle-key-abc",
378
+ });
379
+ platformRequestSignedUrlMock.mockReset();
380
+ platformRequestSignedUrlMock.mockImplementation(async (params) => ({
381
+ url:
382
+ params.operation === "upload"
383
+ ? "https://storage.googleapis.com/bucket/signed-upload"
384
+ : "https://storage.googleapis.com/bucket/signed-download",
385
+ bundleKey: params.bundleKey ?? "bundle-key-123",
386
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
387
+ }));
388
+ platformImportBundleFromGcsMock.mockReset();
389
+ platformImportBundleFromGcsMock.mockResolvedValue({
401
390
  statusCode: 200,
402
391
  body: {
403
392
  success: true,
@@ -410,14 +399,6 @@ beforeEach(() => {
410
399
  },
411
400
  },
412
401
  });
413
- platformRequestUploadUrlMock.mockReset();
414
- platformRequestUploadUrlMock.mockResolvedValue({
415
- uploadUrl: "https://storage.googleapis.com/bucket/signed-upload-url",
416
- bundleKey: "bundle-key-123",
417
- expiresAt: new Date(Date.now() + 3600_000).toISOString(),
418
- });
419
- platformUploadToSignedUrlMock.mockReset();
420
- platformUploadToSignedUrlMock.mockResolvedValue(undefined);
421
402
  platformImportPreflightFromGcsMock.mockReset();
422
403
  platformImportPreflightFromGcsMock.mockResolvedValue({
423
404
  statusCode: 200,
@@ -431,20 +412,6 @@ beforeEach(() => {
431
412
  },
432
413
  },
433
414
  });
434
- platformImportBundleFromGcsMock.mockReset();
435
- platformImportBundleFromGcsMock.mockResolvedValue({
436
- statusCode: 200,
437
- body: {
438
- success: true,
439
- summary: {
440
- total_files: 3,
441
- files_created: 2,
442
- files_overwritten: 1,
443
- files_skipped: 0,
444
- backups_created: 1,
445
- },
446
- },
447
- });
448
415
  checkExistingPlatformAssistantMock.mockReset();
449
416
  checkExistingPlatformAssistantMock.mockResolvedValue(null);
450
417
  ensureSelfHostedLocalRegistrationMock.mockReset();
@@ -471,6 +438,17 @@ beforeEach(() => {
471
438
  computeDeviceIdMock.mockReset();
472
439
  computeDeviceIdMock.mockReturnValue("device-id-123");
473
440
 
441
+ localRuntimeExportToGcsMock.mockReset();
442
+ localRuntimeExportToGcsMock.mockResolvedValue({
443
+ jobId: "local-export-job-1",
444
+ });
445
+ localRuntimeImportFromGcsMock.mockReset();
446
+ localRuntimeImportFromGcsMock.mockResolvedValue({
447
+ jobId: "local-import-job-1",
448
+ });
449
+ localRuntimePollJobStatusMock.mockReset();
450
+ localRuntimePollJobStatusMock.mockImplementation(defaultLocalRuntimePollImpl);
451
+
474
452
  hatchLocalMock.mockReset();
475
453
  hatchLocalMock.mockResolvedValue(undefined);
476
454
  hatchDockerMock.mockReset();
@@ -505,7 +483,6 @@ afterEach(() => {
505
483
  });
506
484
 
507
485
  function setArgv(...args: string[]): void {
508
- // teleport reads process.argv.slice(3)
509
486
  process.argv = ["bun", "vellum", "teleport", ...args];
510
487
  }
511
488
 
@@ -521,44 +498,24 @@ function makeEntry(
521
498
  };
522
499
  }
523
500
 
524
- /** Create a mock fetch that handles export and import endpoints. */
525
- function createFetchMock() {
526
- return mock(async (url: string | URL | Request) => {
527
- const urlStr = typeof url === "string" ? url : url.toString();
528
- if (urlStr.includes("/export")) {
529
- return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
530
- }
531
- if (urlStr.includes("/import-preflight")) {
532
- return new Response(
533
- JSON.stringify({
534
- can_import: true,
535
- summary: {
536
- files_to_create: 1,
537
- files_to_overwrite: 0,
538
- files_unchanged: 0,
539
- total_files: 1,
540
- },
541
- }),
542
- { status: 200, headers: { "Content-Type": "application/json" } },
543
- );
544
- }
545
- if (urlStr.includes("/import")) {
546
- return new Response(
547
- JSON.stringify({
548
- success: true,
549
- summary: {
550
- total_files: 1,
551
- files_created: 1,
552
- files_overwritten: 0,
553
- files_skipped: 0,
554
- backups_created: 0,
555
- },
556
- }),
557
- { status: 200, headers: { "Content-Type": "application/json" } },
558
- );
559
- }
560
- return new Response("not found", { status: 404 });
561
- });
501
+ /**
502
+ * Tracking fetch mock — records every call so tests can verify that the CLI
503
+ * never sends a bundle-sized request body. With the new GCS-unified flow
504
+ * all bundle bytes travel between the runtime and GCS directly, so the CLI
505
+ * should make zero fetch calls carrying binary payloads.
506
+ */
507
+ function installTrackingFetch(): () => void {
508
+ const originalFetch = globalThis.fetch;
509
+ globalThis.fetch = mock(
510
+ async (url: string | URL | Request, init?: RequestInit) => {
511
+ const urlStr = typeof url === "string" ? url : url.toString();
512
+ fetchCalls.push({ url: urlStr, body: init?.body });
513
+ return new Response("not found", { status: 404 });
514
+ },
515
+ ) as unknown as typeof globalThis.fetch;
516
+ return () => {
517
+ globalThis.fetch = originalFetch;
518
+ };
562
519
  }
563
520
 
564
521
  // ---------------------------------------------------------------------------
@@ -663,20 +620,13 @@ describe("same-environment rejection", () => {
663
620
  return null;
664
621
  });
665
622
 
666
- const originalFetch = globalThis.fetch;
667
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
668
-
669
- try {
670
- await expect(teleport()).rejects.toThrow("process.exit:1");
671
- expect(consoleErrorSpy).toHaveBeenCalledWith(
672
- expect.stringContaining("Cannot teleport between two local assistants"),
673
- );
674
- } finally {
675
- globalThis.fetch = originalFetch;
676
- }
623
+ await expect(teleport()).rejects.toThrow("process.exit:1");
624
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
625
+ expect.stringContaining("Cannot teleport between two local assistants"),
626
+ );
677
627
  });
678
628
 
679
- test("source docker, target docker -> error (after resolving target)", async () => {
629
+ test("source docker, target docker -> error", async () => {
680
630
  setArgv("--from", "src", "--docker", "dst");
681
631
 
682
632
  const srcEntry = makeEntry("src", { cloud: "docker" });
@@ -688,22 +638,13 @@ describe("same-environment rejection", () => {
688
638
  return null;
689
639
  });
690
640
 
691
- const originalFetch = globalThis.fetch;
692
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
693
-
694
- try {
695
- await expect(teleport()).rejects.toThrow("process.exit:1");
696
- expect(consoleErrorSpy).toHaveBeenCalledWith(
697
- expect.stringContaining(
698
- "Cannot teleport between two docker assistants",
699
- ),
700
- );
701
- } finally {
702
- globalThis.fetch = originalFetch;
703
- }
641
+ await expect(teleport()).rejects.toThrow("process.exit:1");
642
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
643
+ expect.stringContaining("Cannot teleport between two docker assistants"),
644
+ );
704
645
  });
705
646
 
706
- test("source vellum, target platform -> error (after resolving target)", async () => {
647
+ test("source vellum, target platform -> error", async () => {
707
648
  setArgv("--from", "src", "--platform", "dst");
708
649
 
709
650
  const srcEntry = makeEntry("src", {
@@ -721,19 +662,12 @@ describe("same-environment rejection", () => {
721
662
  return null;
722
663
  });
723
664
 
724
- const originalFetch = globalThis.fetch;
725
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
726
-
727
- try {
728
- await expect(teleport()).rejects.toThrow("process.exit:1");
729
- expect(consoleErrorSpy).toHaveBeenCalledWith(
730
- expect.stringContaining(
731
- "Cannot teleport between two platform assistants",
732
- ),
733
- );
734
- } finally {
735
- globalThis.fetch = originalFetch;
736
- }
665
+ await expect(teleport()).rejects.toThrow("process.exit:1");
666
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
667
+ expect.stringContaining(
668
+ "Cannot teleport between two platform assistants",
669
+ ),
670
+ );
737
671
  });
738
672
 
739
673
  test("same-env rejection happens before hatching (no orphaned assistants)", async () => {
@@ -749,59 +683,15 @@ describe("same-environment rejection", () => {
749
683
  expect(consoleErrorSpy).toHaveBeenCalledWith(
750
684
  expect.stringContaining("Cannot teleport between two local assistants"),
751
685
  );
752
- // Crucially: no hatch should have been called — the early guard fires first
753
- expect(hatchLocalMock).not.toHaveBeenCalled();
754
- expect(hatchDockerMock).not.toHaveBeenCalled();
755
- expect(hatchAssistantMock).not.toHaveBeenCalled();
756
- });
757
-
758
- test("same-env rejection before hatching for docker", async () => {
759
- setArgv("--from", "my-docker", "--docker");
760
-
761
- const dockerEntry = makeEntry("my-docker", { cloud: "docker" });
762
- findAssistantByNameMock.mockImplementation((name: string) => {
763
- if (name === "my-docker") return dockerEntry;
764
- return null;
765
- });
766
-
767
- await expect(teleport()).rejects.toThrow("process.exit:1");
768
- expect(consoleErrorSpy).toHaveBeenCalledWith(
769
- expect.stringContaining("Cannot teleport between two docker assistants"),
770
- );
771
- expect(hatchLocalMock).not.toHaveBeenCalled();
772
- expect(hatchDockerMock).not.toHaveBeenCalled();
773
- expect(hatchAssistantMock).not.toHaveBeenCalled();
774
- });
775
-
776
- test("same-env rejection before hatching for platform (vellum cloud)", async () => {
777
- setArgv("--from", "my-cloud", "--platform");
778
-
779
- const platformEntry = makeEntry("my-cloud", {
780
- cloud: "vellum",
781
- runtimeUrl: "https://platform.vellum.ai",
782
- });
783
- findAssistantByNameMock.mockImplementation((name: string) => {
784
- if (name === "my-cloud") return platformEntry;
785
- return null;
786
- });
787
-
788
- await expect(teleport()).rejects.toThrow("process.exit:1");
789
- expect(consoleErrorSpy).toHaveBeenCalledWith(
790
- expect.stringContaining(
791
- "Cannot teleport between two platform assistants",
792
- ),
793
- );
794
686
  expect(hatchLocalMock).not.toHaveBeenCalled();
795
687
  expect(hatchDockerMock).not.toHaveBeenCalled();
796
688
  expect(hatchAssistantMock).not.toHaveBeenCalled();
797
689
  });
798
690
 
799
691
  test("flag says docker but resolved target is local -> rejects cloud mismatch", async () => {
800
- // User passes --docker but the named target is actually a local assistant
801
692
  setArgv("--from", "src", "--docker", "misidentified");
802
693
 
803
694
  const srcEntry = makeEntry("src", { cloud: "vellum" });
804
- // Target is actually local despite the --docker flag
805
695
  const dstEntry = makeEntry("misidentified", { cloud: "local" });
806
696
 
807
697
  findAssistantByNameMock.mockImplementation((name: string) => {
@@ -832,16 +722,11 @@ describe("resolveOrHatchTarget", () => {
832
722
  const result = await resolveOrHatchTarget("docker", "my-docker");
833
723
  expect(result).toBe(dockerEntry);
834
724
  expect(hatchDockerMock).not.toHaveBeenCalled();
835
- expect(consoleLogSpy).toHaveBeenCalledWith(
836
- expect.stringContaining("Target: my-docker (docker)"),
837
- );
838
725
  });
839
726
 
840
727
  test("name not found -> hatch docker", async () => {
841
728
  const newEntry = makeEntry("new-one", { cloud: "docker" });
842
729
  findAssistantByNameMock.mockImplementation((name: string) => {
843
- // First call: lookup by name -> not found
844
- // Second call: after hatch -> found
845
730
  if (name === "new-one" && hatchDockerMock.mock.calls.length > 0) {
846
731
  return newEntry;
847
732
  }
@@ -859,12 +744,10 @@ describe("resolveOrHatchTarget", () => {
859
744
  expect(result).toBe(newEntry);
860
745
  });
861
746
 
862
- test("no name -> hatch local with null name, discovers via diff", async () => {
747
+ test("no name -> hatch local, discovers via diff", async () => {
863
748
  const existingEntry = makeEntry("existing-local", { cloud: "local" });
864
749
  const newEntry = makeEntry("auto-generated", { cloud: "local" });
865
750
 
866
- // Before hatch: only the existing entry
867
- // After hatch: existing + new entry
868
751
  loadAllAssistantsMock.mockImplementation(() => {
869
752
  if (hatchLocalMock.mock.calls.length > 0) {
870
753
  return [existingEntry, newEntry];
@@ -873,13 +756,7 @@ describe("resolveOrHatchTarget", () => {
873
756
  });
874
757
 
875
758
  const result = await resolveOrHatchTarget("local");
876
- expect(hatchLocalMock).toHaveBeenCalledWith(
877
- "vellum",
878
- null,
879
- false,
880
- false,
881
- {},
882
- );
759
+ expect(hatchLocalMock).toHaveBeenCalled();
883
760
  expect(result).toBe(newEntry);
884
761
  });
885
762
 
@@ -903,16 +780,10 @@ describe("resolveOrHatchTarget", () => {
903
780
 
904
781
  const result = await resolveOrHatchTarget("platform", "nonexistent");
905
782
  expect(hatchAssistantMock).toHaveBeenCalledWith("platform-token");
906
- expect(saveAssistantEntryMock).toHaveBeenCalledWith(
907
- expect.objectContaining({
908
- assistantId: "platform-new-id",
909
- cloud: "vellum",
910
- }),
911
- );
912
783
  expect(result.assistantId).toBe("platform-new-id");
913
784
  });
914
785
 
915
- test("platform with no name -> blocks when hatch returns reusedExisting (defensive safety net)", async () => {
786
+ test("platform with no name -> blocks when hatch returns reusedExisting", async () => {
916
787
  findAssistantByNameMock.mockReturnValue(null);
917
788
  hatchAssistantMock.mockResolvedValue({
918
789
  assistant: {
@@ -923,25 +794,12 @@ describe("resolveOrHatchTarget", () => {
923
794
  reusedExisting: true,
924
795
  });
925
796
 
926
- // Defensive safety net: even though the pre-check in teleport() should
927
- // catch this first, resolveOrHatchTarget still blocks on reusedExisting
928
- // to guard against a TOCTOU race (matching the Swift client behavior).
929
797
  await expect(resolveOrHatchTarget("platform", undefined)).rejects.toThrow(
930
798
  "process.exit:1",
931
799
  );
932
800
  expect(consoleErrorSpy).toHaveBeenCalledWith(
933
801
  expect.stringContaining("already have a platform assistant"),
934
802
  );
935
- expect(consoleErrorSpy).toHaveBeenCalledWith(
936
- expect.stringContaining("Retire it first"),
937
- );
938
- // The existing assistant is saved to the lockfile so `vellum retire` can find it
939
- expect(saveAssistantEntryMock).toHaveBeenCalledWith(
940
- expect.objectContaining({
941
- assistantId: "existing-platform-id",
942
- cloud: "vellum",
943
- }),
944
- );
945
803
  });
946
804
 
947
805
  test("existing assistant with wrong cloud -> rejects", async () => {
@@ -973,212 +831,192 @@ describe("resolveOrHatchTarget", () => {
973
831
  });
974
832
 
975
833
  // ---------------------------------------------------------------------------
976
- // Auto-retire tests
834
+ // Unified GCS teleport flow — the four directions
977
835
  // ---------------------------------------------------------------------------
978
836
 
979
- describe("auto-retire", () => {
980
- test("local -> docker: stops source before hatch, retires after import", async () => {
981
- setArgv("--from", "my-local", "--docker");
837
+ describe("unified GCS flow — four directions", () => {
838
+ test("local platform: requests upload URL, drives local runtime export, imports from GCS", async () => {
839
+ setArgv("--from", "my-local", "--platform");
982
840
 
983
- const localEntry = makeEntry("my-local", {
984
- cloud: "local",
985
- resources: {
986
- instanceDir: "/home/test",
987
- pidFile: "/home/test/.vellum/assistant.pid",
988
- signingKey: "key",
989
- daemonPort: 7821,
990
- gatewayPort: 7830,
991
- qdrantPort: 6333,
992
- cesPort: 8090,
993
- },
994
- });
995
- const dockerEntry = makeEntry("new-docker", { cloud: "docker" });
841
+ const localEntry = makeEntry("my-local", { cloud: "local" });
842
+ findAssistantByNameMock.mockImplementation((name: string) =>
843
+ name === "my-local" ? localEntry : null,
844
+ );
996
845
 
997
- findAssistantByNameMock.mockImplementation((name: string) => {
998
- if (name === "my-local") return localEntry;
999
- return null;
1000
- });
846
+ const restoreFetch = installTrackingFetch();
847
+ try {
848
+ await teleport();
1001
849
 
1002
- // Simulate hatch creating a new docker entry
1003
- loadAllAssistantsMock.mockImplementation(() => {
1004
- if (hatchDockerMock.mock.calls.length > 0) {
1005
- return [localEntry, dockerEntry];
1006
- }
1007
- return [localEntry];
1008
- });
850
+ // Signed-URL request for upload pinned to the platform target's URL
851
+ // so upload and download land on the same platform.
852
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
853
+ expect.objectContaining({ operation: "upload" }),
854
+ "platform-token",
855
+ "https://platform.vellum.ai",
856
+ );
1009
857
 
1010
- const originalFetch = globalThis.fetch;
1011
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
858
+ // Runtime export-to-gcs kicked off with the signed upload URL.
859
+ // Helper takes an entry, not a bare URL — the entry's cloud drives
860
+ // URL construction (local → gateway loopback path).
861
+ expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
862
+ expect.objectContaining({
863
+ cloud: "local",
864
+ runtimeUrl: "http://localhost:7821",
865
+ }),
866
+ "local-token",
867
+ expect.objectContaining({
868
+ uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
869
+ }),
870
+ );
1012
871
 
1013
- try {
1014
- await teleport();
1015
- // Source should be stopped (slept) before hatch
1016
- expect(stopProcessByPidFileMock).toHaveBeenCalled();
1017
- // Retire happens after successful import
1018
- expect(retireLocalMock).toHaveBeenCalledWith("my-local", localEntry);
1019
- expect(removeAssistantEntryMock).toHaveBeenCalledWith("my-local");
872
+ // Poll continued until complete
873
+ expect(localRuntimePollJobStatusMock).toHaveBeenCalled();
874
+
875
+ // Import via GCS with the bundleKey returned from signed-URL request
876
+ expect(platformImportBundleFromGcsMock).toHaveBeenCalledWith(
877
+ "bundle-key-123",
878
+ "platform-token",
879
+ expect.any(String),
880
+ );
881
+
882
+ // No download-URL request on the import side (platform target pulls
883
+ // directly from GCS).
884
+ const downloadOps = platformRequestSignedUrlMock.mock.calls.filter(
885
+ (call: unknown[]) =>
886
+ (call[0] as { operation: string }).operation === "download",
887
+ );
888
+ expect(downloadOps.length).toBe(0);
1020
889
  } finally {
1021
- globalThis.fetch = originalFetch;
890
+ restoreFetch();
1022
891
  }
1023
892
  });
1024
893
 
1025
- test("docker -> local: sleeps containers before hatch, retires after import", async () => {
1026
- setArgv("--from", "my-docker", "--local");
894
+ test("platform local: drives platform export, reads bundle_key, requests download URL for runtime import", async () => {
895
+ setArgv("--from", "my-platform", "--local", "my-local");
1027
896
 
1028
- const dockerEntry = makeEntry("my-docker", { cloud: "docker" });
1029
- const localEntry = makeEntry("new-local", { cloud: "local" });
897
+ const platformEntry = makeEntry("my-platform", {
898
+ cloud: "vellum",
899
+ runtimeUrl: "https://platform.vellum.ai",
900
+ });
901
+ const localEntry = makeEntry("my-local", {
902
+ cloud: "local",
903
+ bearerToken: "local-bearer",
904
+ });
1030
905
 
1031
906
  findAssistantByNameMock.mockImplementation((name: string) => {
1032
- if (name === "my-docker") return dockerEntry;
907
+ if (name === "my-platform") return platformEntry;
908
+ if (name === "my-local") return localEntry;
1033
909
  return null;
1034
910
  });
1035
911
 
1036
- loadAllAssistantsMock.mockImplementation(() => {
1037
- if (hatchLocalMock.mock.calls.length > 0) {
1038
- return [dockerEntry, localEntry];
1039
- }
1040
- return [dockerEntry];
1041
- });
1042
-
1043
- const originalFetch = globalThis.fetch;
1044
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
912
+ // Platform poll returns export-complete with a bundle_key.
913
+ platformPollJobStatusMock.mockResolvedValue({
914
+ jobId: "platform-export-job-1",
915
+ type: "export",
916
+ status: "complete",
917
+ bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
918
+ });
919
+
920
+ // The bundle key now flows from the upload signed-URL request rather than
921
+ // the job-status payload — pin it so the download-URL assertion below
922
+ // still uses the same expected key.
923
+ platformRequestSignedUrlMock.mockImplementation(async (params) => ({
924
+ url:
925
+ params.operation === "upload"
926
+ ? "https://storage.googleapis.com/bucket/signed-upload"
927
+ : "https://storage.googleapis.com/bucket/signed-download",
928
+ bundleKey:
929
+ params.bundleKey ?? "platform-exports/org-1/bundle-abc.vbundle",
930
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
931
+ }));
1045
932
 
933
+ const restoreFetch = installTrackingFetch();
1046
934
  try {
1047
935
  await teleport();
1048
- // Docker source should be slept (containers stopped) before hatch
1049
- expect(sleepContainersMock).toHaveBeenCalled();
1050
- // Retire happens after successful import
1051
- expect(retireDockerMock).toHaveBeenCalledWith("my-docker");
1052
- expect(removeAssistantEntryMock).toHaveBeenCalledWith("my-docker");
1053
- } finally {
1054
- globalThis.fetch = originalFetch;
1055
- }
1056
- });
1057
-
1058
- test("--keep-source skips retire and removeAssistantEntry", async () => {
1059
- setArgv("--from", "my-local", "--docker", "--keep-source");
1060
-
1061
- const localEntry = makeEntry("my-local", { cloud: "local" });
1062
- const dockerEntry = makeEntry("new-docker", { cloud: "docker" });
1063
-
1064
- findAssistantByNameMock.mockImplementation((name: string) => {
1065
- if (name === "my-local") return localEntry;
1066
- return null;
1067
- });
1068
-
1069
- loadAllAssistantsMock.mockImplementation(() => {
1070
- if (hatchDockerMock.mock.calls.length > 0) {
1071
- return [localEntry, dockerEntry];
1072
- }
1073
- return [localEntry];
1074
- });
1075
936
 
1076
- const originalFetch = globalThis.fetch;
1077
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1078
-
1079
- try {
1080
- await teleport();
1081
- expect(retireLocalMock).not.toHaveBeenCalled();
1082
- expect(retireDockerMock).not.toHaveBeenCalled();
1083
- expect(removeAssistantEntryMock).not.toHaveBeenCalled();
1084
- expect(consoleLogSpy).toHaveBeenCalledWith(
1085
- expect.stringContaining("kept (--keep-source)"),
937
+ // Platform side: requested an upload URL, kicked off a runtime export to
938
+ // GCS, and polled the unified job status.
939
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
940
+ expect.objectContaining({ operation: "upload" }),
941
+ "platform-token",
942
+ "https://platform.vellum.ai",
943
+ );
944
+ // For platform sources, export-to-gcs is reached via the platform's
945
+ // wildcard runtime proxy. The helper builds the assistant-scoped URL
946
+ // from the entry (`/v1/assistants/<id>/migrations/export-to-gcs`) and
947
+ // sends platform-token auth — no guardian-token bootstrap.
948
+ expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
949
+ expect.objectContaining({
950
+ cloud: "vellum",
951
+ runtimeUrl: "https://platform.vellum.ai",
952
+ assistantId: "my-platform",
953
+ }),
954
+ "platform-token",
955
+ expect.objectContaining({
956
+ uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
957
+ description: "teleport export",
958
+ }),
959
+ );
960
+ // Polling for platform sources also goes through the wildcard via
961
+ // localRuntimePollJobStatus(entry, ...) — the dedicated
962
+ // `/v1/migrations/jobs/{id}/` endpoint queries platform-side
963
+ // ImportJob records and would 404 on runtime-created job IDs.
964
+ expect(localRuntimePollJobStatusMock).toHaveBeenCalledWith(
965
+ expect.objectContaining({
966
+ cloud: "vellum",
967
+ runtimeUrl: "https://platform.vellum.ai",
968
+ }),
969
+ "platform-token",
970
+ "local-export-job-1",
1086
971
  );
1087
- } finally {
1088
- globalThis.fetch = originalFetch;
1089
- }
1090
- });
1091
-
1092
- test("platform transfers skip retire", async () => {
1093
- setArgv("--from", "my-local", "--platform");
1094
-
1095
- const localEntry = makeEntry("my-local", { cloud: "local" });
1096
-
1097
- findAssistantByNameMock.mockImplementation((name: string) => {
1098
- if (name === "my-local") return localEntry;
1099
- return null;
1100
- });
1101
-
1102
- const originalFetch = globalThis.fetch;
1103
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1104
-
1105
- try {
1106
- await teleport();
1107
- expect(retireLocalMock).not.toHaveBeenCalled();
1108
- expect(retireDockerMock).not.toHaveBeenCalled();
1109
- expect(removeAssistantEntryMock).not.toHaveBeenCalled();
1110
- } finally {
1111
- globalThis.fetch = originalFetch;
1112
- }
1113
- });
1114
-
1115
- test("dry-run without existing target does not hatch or export", async () => {
1116
- setArgv("--from", "my-local", "--docker", "--dry-run");
1117
-
1118
- const localEntry = makeEntry("my-local", { cloud: "local" });
1119
-
1120
- findAssistantByNameMock.mockImplementation((name: string) => {
1121
- if (name === "my-local") return localEntry;
1122
- return null;
1123
- });
1124
-
1125
- await teleport();
1126
-
1127
- // Should NOT hatch, export, import, or retire
1128
- expect(hatchDockerMock).not.toHaveBeenCalled();
1129
- expect(hatchLocalMock).not.toHaveBeenCalled();
1130
- expect(hatchAssistantMock).not.toHaveBeenCalled();
1131
- expect(retireLocalMock).not.toHaveBeenCalled();
1132
- expect(retireDockerMock).not.toHaveBeenCalled();
1133
- expect(removeAssistantEntryMock).not.toHaveBeenCalled();
1134
- });
1135
-
1136
- test("dry-run with existing target runs preflight without hatching", async () => {
1137
- setArgv("--from", "my-local", "--docker", "my-docker", "--dry-run");
1138
-
1139
- const localEntry = makeEntry("my-local", { cloud: "local" });
1140
- const dockerEntry = makeEntry("my-docker", { cloud: "docker" });
1141
-
1142
- findAssistantByNameMock.mockImplementation((name: string) => {
1143
- if (name === "my-local") return localEntry;
1144
- if (name === "my-docker") return dockerEntry;
1145
- return null;
1146
- });
1147
972
 
1148
- const originalFetch = globalThis.fetch;
1149
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
973
+ // For the local target we request a download URL keyed by the
974
+ // platform's bundle_key. The URL must target the SOURCE platform
975
+ // (where the bundle was written) — pinned so a lockfile change
976
+ // can't split upload and download across instances.
977
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
978
+ {
979
+ operation: "download",
980
+ bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
981
+ },
982
+ "platform-token",
983
+ "https://platform.vellum.ai",
984
+ );
1150
985
 
1151
- try {
1152
- await teleport();
986
+ // Runtime import-from-gcs was kicked off with that URL.
987
+ expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
988
+ expect.objectContaining({
989
+ cloud: "local",
990
+ runtimeUrl: "http://localhost:7821",
991
+ }),
992
+ "local-token",
993
+ expect.objectContaining({
994
+ bundleUrl: "https://storage.googleapis.com/bucket/signed-download",
995
+ }),
996
+ );
997
+ expect(localRuntimePollJobStatusMock).toHaveBeenCalled();
1153
998
 
1154
- // Should NOT hatch or retire
1155
- expect(hatchDockerMock).not.toHaveBeenCalled();
1156
- expect(hatchLocalMock).not.toHaveBeenCalled();
1157
- expect(retireLocalMock).not.toHaveBeenCalled();
1158
- expect(retireDockerMock).not.toHaveBeenCalled();
1159
- expect(removeAssistantEntryMock).not.toHaveBeenCalled();
999
+ // No legacy inline-import helpers were touched.
1000
+ // (Verified by the absence of fetch calls carrying bundle bodies —
1001
+ // see "never buffers bundle bytes" assertion below.)
1160
1002
  } finally {
1161
- globalThis.fetch = originalFetch;
1003
+ restoreFetch();
1162
1004
  }
1163
1005
  });
1164
- });
1165
-
1166
- // ---------------------------------------------------------------------------
1167
- // Full flow tests
1168
- // ---------------------------------------------------------------------------
1169
1006
 
1170
- describe("teleport full flow", () => {
1171
- test("hatch and import: --from my-local --docker", async () => {
1007
+ test("local docker: export via upload URL, import via download URL", async () => {
1172
1008
  setArgv("--from", "my-local", "--docker");
1173
1009
 
1174
1010
  const localEntry = makeEntry("my-local", { cloud: "local" });
1175
- const dockerEntry = makeEntry("new-docker", { cloud: "docker" });
1176
-
1177
- findAssistantByNameMock.mockImplementation((name: string) => {
1178
- if (name === "my-local") return localEntry;
1179
- return null;
1011
+ const dockerEntry = makeEntry("new-docker", {
1012
+ cloud: "docker",
1013
+ runtimeUrl: "http://localhost:7822",
1180
1014
  });
1181
1015
 
1016
+ findAssistantByNameMock.mockImplementation((name: string) =>
1017
+ name === "my-local" ? localEntry : null,
1018
+ );
1019
+
1182
1020
  loadAllAssistantsMock.mockImplementation(() => {
1183
1021
  if (hatchDockerMock.mock.calls.length > 0) {
1184
1022
  return [localEntry, dockerEntry];
@@ -1186,605 +1024,358 @@ describe("teleport full flow", () => {
1186
1024
  return [localEntry];
1187
1025
  });
1188
1026
 
1189
- const originalFetch = globalThis.fetch;
1190
- const fetchMock = createFetchMock();
1191
- globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
1192
-
1193
- try {
1194
- await teleport();
1195
-
1196
- // Verify sequence: export, hatch, import, retire
1197
- expect(hatchDockerMock).toHaveBeenCalled();
1198
- expect(retireLocalMock).toHaveBeenCalledWith("my-local", localEntry);
1199
- expect(removeAssistantEntryMock).toHaveBeenCalledWith("my-local");
1200
- expect(consoleLogSpy).toHaveBeenCalledWith(
1201
- expect.stringContaining("Teleport complete"),
1202
- );
1203
- } finally {
1204
- globalThis.fetch = originalFetch;
1205
- }
1206
- });
1207
-
1208
- test("existing target overwrite: --from my-local --docker my-existing", async () => {
1209
- setArgv("--from", "my-local", "--docker", "my-existing");
1210
-
1211
- const localEntry = makeEntry("my-local", { cloud: "local" });
1212
- const dockerEntry = makeEntry("my-existing", { cloud: "docker" });
1213
-
1214
- findAssistantByNameMock.mockImplementation((name: string) => {
1215
- if (name === "my-local") return localEntry;
1216
- if (name === "my-existing") return dockerEntry;
1217
- return null;
1218
- });
1219
-
1220
- const originalFetch = globalThis.fetch;
1221
- const fetchMock = createFetchMock();
1222
- globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
1223
-
1027
+ const restoreFetch = installTrackingFetch();
1224
1028
  try {
1225
1029
  await teleport();
1226
1030
 
1227
- // No hatch should happen existing target is used
1228
- expect(hatchDockerMock).not.toHaveBeenCalled();
1229
- // Source should still be retired
1230
- expect(retireLocalMock).toHaveBeenCalledWith("my-local", localEntry);
1231
- expect(consoleLogSpy).toHaveBeenCalledWith(
1232
- expect.stringContaining("Teleport complete"),
1233
- );
1234
- } finally {
1235
- globalThis.fetch = originalFetch;
1236
- }
1237
- });
1238
-
1239
- test("legacy --to flag shows deprecation message", async () => {
1240
- setArgv("--from", "source", "--to", "target");
1241
-
1242
- await expect(teleport()).rejects.toThrow("process.exit:1");
1243
- expect(consoleErrorSpy).toHaveBeenCalledWith(
1244
- expect.stringContaining("--to is deprecated"),
1245
- );
1246
- });
1247
- });
1248
-
1249
- // ---------------------------------------------------------------------------
1250
- // Signed-URL upload tests
1251
- // ---------------------------------------------------------------------------
1252
-
1253
- describe("signed-URL upload flow", () => {
1254
- test("happy path: signed URL upload succeeds → GCS-based import used", async () => {
1255
- setArgv("--from", "my-local", "--platform");
1256
-
1257
- const localEntry = makeEntry("my-local", { cloud: "local" });
1258
-
1259
- findAssistantByNameMock.mockImplementation((name: string) => {
1260
- if (name === "my-local") return localEntry;
1261
- return null;
1262
- });
1263
-
1264
- const originalFetch = globalThis.fetch;
1265
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1266
-
1267
- try {
1268
- await teleport();
1269
-
1270
- // Signed-URL flow should be used
1271
- expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1272
- expect(platformUploadToSignedUrlMock).toHaveBeenCalled();
1273
- expect(platformImportBundleFromGcsMock).toHaveBeenCalledWith(
1274
- "bundle-key-123",
1031
+ // Export and import must pin the same platform URL so the bundle
1032
+ // lives in one place end-to-end. For local→docker neither side is
1033
+ // platform, so we default to getPlatformUrl() (resolved once).
1034
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1035
+ expect.objectContaining({ operation: "upload" }),
1275
1036
  "platform-token",
1276
1037
  "https://platform.vellum.ai",
1277
1038
  );
1278
- // Inline import should NOT be called
1279
- expect(platformImportBundleMock).not.toHaveBeenCalled();
1280
- expect(consoleLogSpy).toHaveBeenCalledWith(
1281
- expect.stringContaining("Teleport complete"),
1282
- );
1283
- } finally {
1284
- globalThis.fetch = originalFetch;
1285
- }
1286
- });
1287
-
1288
- test("happy path dry-run: signed URL upload succeeds → GCS-based preflight used", async () => {
1289
- setArgv(
1290
- "--from",
1291
- "my-local",
1292
- "--platform",
1293
- "existing-platform",
1294
- "--dry-run",
1295
- );
1039
+ expect(localRuntimeExportToGcsMock).toHaveBeenCalled();
1296
1040
 
1297
- const localEntry = makeEntry("my-local", { cloud: "local" });
1298
- const platformEntry = makeEntry("existing-platform", {
1299
- cloud: "vellum",
1300
- runtimeUrl: "https://platform.vellum.ai",
1301
- });
1302
-
1303
- findAssistantByNameMock.mockImplementation((name: string) => {
1304
- if (name === "my-local") return localEntry;
1305
- if (name === "existing-platform") return platformEntry;
1306
- return null;
1307
- });
1308
-
1309
- const originalFetch = globalThis.fetch;
1310
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1311
-
1312
- try {
1313
- await teleport();
1314
-
1315
- // Signed-URL flow should be used for preflight
1316
- expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1317
- expect(platformUploadToSignedUrlMock).toHaveBeenCalled();
1318
- expect(platformImportPreflightFromGcsMock).toHaveBeenCalledWith(
1319
- "bundle-key-123",
1041
+ // Import: download-URL for the docker target, then runtime import.
1042
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1043
+ {
1044
+ operation: "download",
1045
+ bundleKey: "bundle-key-123",
1046
+ },
1320
1047
  "platform-token",
1321
1048
  "https://platform.vellum.ai",
1322
1049
  );
1323
- // Inline preflight should NOT be called
1324
- expect(platformImportPreflightMock).not.toHaveBeenCalled();
1325
- } finally {
1326
- globalThis.fetch = originalFetch;
1327
- }
1328
- });
1329
-
1330
- test("fallback: platformRequestUploadUrl throws 503 → falls back to inline import", async () => {
1331
- setArgv("--from", "my-local", "--platform");
1332
-
1333
- const localEntry = makeEntry("my-local", { cloud: "local" });
1334
-
1335
- findAssistantByNameMock.mockImplementation((name: string) => {
1336
- if (name === "my-local") return localEntry;
1337
- return null;
1338
- });
1339
-
1340
- // Simulate 503 — "not available" in the error message
1341
- platformRequestUploadUrlMock.mockRejectedValue(
1342
- new Error("Signed uploads are not available on this platform instance"),
1343
- );
1344
-
1345
- const originalFetch = globalThis.fetch;
1346
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1347
-
1348
- try {
1349
- await teleport();
1350
-
1351
- // Should fall back to inline import
1352
- expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1353
- expect(platformUploadToSignedUrlMock).not.toHaveBeenCalled();
1354
- expect(platformImportBundleFromGcsMock).not.toHaveBeenCalled();
1355
- expect(platformImportBundleMock).toHaveBeenCalled();
1356
- expect(consoleLogSpy).toHaveBeenCalledWith(
1357
- expect.stringContaining("Teleport complete"),
1050
+ expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
1051
+ expect.objectContaining({
1052
+ cloud: "docker",
1053
+ runtimeUrl: "http://localhost:7822",
1054
+ }),
1055
+ "local-token",
1056
+ expect.objectContaining({
1057
+ bundleUrl: "https://storage.googleapis.com/bucket/signed-download",
1058
+ }),
1358
1059
  );
1060
+
1061
+ // Source retirement still happens on success for local↔docker.
1062
+ expect(retireLocalMock).toHaveBeenCalledWith("my-local", localEntry);
1063
+ expect(removeAssistantEntryMock).toHaveBeenCalledWith("my-local");
1359
1064
  } finally {
1360
- globalThis.fetch = originalFetch;
1065
+ restoreFetch();
1361
1066
  }
1362
1067
  });
1363
1068
 
1364
- test("fallback: platformRequestUploadUrl throws 404 falls back to inline import", async () => {
1365
- setArgv("--from", "my-local", "--platform");
1366
-
1367
- const localEntry = makeEntry("my-local", { cloud: "local" });
1069
+ test("docker → local: export via upload URL, import via download URL", async () => {
1070
+ setArgv("--from", "my-docker", "--local");
1368
1071
 
1369
- findAssistantByNameMock.mockImplementation((name: string) => {
1370
- if (name === "my-local") return localEntry;
1371
- return null;
1072
+ const dockerEntry = makeEntry("my-docker", {
1073
+ cloud: "docker",
1074
+ runtimeUrl: "http://localhost:7822",
1075
+ });
1076
+ const localEntry = makeEntry("new-local", {
1077
+ cloud: "local",
1078
+ runtimeUrl: "http://localhost:7823",
1372
1079
  });
1373
1080
 
1374
- // Simulate 404 — endpoint doesn't exist on older platform versions
1375
- platformRequestUploadUrlMock.mockRejectedValue(
1376
- new Error("Signed uploads are not available on this platform instance"),
1081
+ findAssistantByNameMock.mockImplementation((name: string) =>
1082
+ name === "my-docker" ? dockerEntry : null,
1377
1083
  );
1378
1084
 
1379
- const originalFetch = globalThis.fetch;
1380
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1085
+ loadAllAssistantsMock.mockImplementation(() => {
1086
+ if (hatchLocalMock.mock.calls.length > 0) {
1087
+ return [dockerEntry, localEntry];
1088
+ }
1089
+ return [dockerEntry];
1090
+ });
1381
1091
 
1092
+ const restoreFetch = installTrackingFetch();
1382
1093
  try {
1383
1094
  await teleport();
1384
1095
 
1385
- // Should fall back to inline import
1386
- expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1387
- expect(platformUploadToSignedUrlMock).not.toHaveBeenCalled();
1388
- expect(platformImportBundleFromGcsMock).not.toHaveBeenCalled();
1389
- expect(platformImportBundleMock).toHaveBeenCalled();
1390
- expect(consoleLogSpy).toHaveBeenCalledWith(
1391
- expect.stringContaining("Teleport complete"),
1392
- );
1393
- } finally {
1394
- globalThis.fetch = originalFetch;
1395
- }
1396
- });
1397
-
1398
- test("upload error: platformUploadToSignedUrl throws → error propagates", async () => {
1399
- setArgv("--from", "my-local", "--platform");
1400
-
1401
- const localEntry = makeEntry("my-local", { cloud: "local" });
1402
-
1403
- findAssistantByNameMock.mockImplementation((name: string) => {
1404
- if (name === "my-local") return localEntry;
1405
- return null;
1406
- });
1407
-
1408
- // Upload succeeds at getting URL but fails during PUT
1409
- platformUploadToSignedUrlMock.mockRejectedValue(
1410
- new Error("Upload to signed URL failed: 500 Internal Server Error"),
1411
- );
1412
-
1413
- const originalFetch = globalThis.fetch;
1414
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1096
+ // Docker source should be put to sleep first.
1097
+ expect(sleepContainersMock).toHaveBeenCalled();
1415
1098
 
1416
- try {
1417
- await expect(teleport()).rejects.toThrow(
1418
- "Upload to signed URL failed: 500 Internal Server Error",
1099
+ // Export leg: upload-URL (pinned to the same platform as import),
1100
+ // then runtime export.
1101
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1102
+ expect.objectContaining({ operation: "upload" }),
1103
+ "platform-token",
1104
+ "https://platform.vellum.ai",
1105
+ );
1106
+ expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
1107
+ expect.objectContaining({
1108
+ cloud: "docker",
1109
+ runtimeUrl: "http://localhost:7822",
1110
+ }),
1111
+ "local-token",
1112
+ expect.objectContaining({
1113
+ uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
1114
+ }),
1419
1115
  );
1420
- // Should NOT fall back to inline import
1421
- expect(platformImportBundleMock).not.toHaveBeenCalled();
1422
- expect(platformImportBundleFromGcsMock).not.toHaveBeenCalled();
1423
- } finally {
1424
- globalThis.fetch = originalFetch;
1425
- }
1426
- });
1427
-
1428
- test("413 from GCS import: error message includes 'too large'", async () => {
1429
- setArgv("--from", "my-local", "--platform");
1430
-
1431
- const localEntry = makeEntry("my-local", { cloud: "local" });
1432
-
1433
- findAssistantByNameMock.mockImplementation((name: string) => {
1434
- if (name === "my-local") return localEntry;
1435
- return null;
1436
- });
1437
-
1438
- // GCS import returns 413
1439
- platformImportBundleFromGcsMock.mockRejectedValue(
1440
- new Error("Bundle too large to import"),
1441
- );
1442
1116
 
1443
- const originalFetch = globalThis.fetch;
1444
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1117
+ // Import leg: download-URL targets the new local runtime
1118
+ expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
1119
+ expect.objectContaining({
1120
+ cloud: "local",
1121
+ runtimeUrl: "http://localhost:7823",
1122
+ }),
1123
+ "local-token",
1124
+ expect.anything(),
1125
+ );
1445
1126
 
1446
- try {
1447
- await expect(teleport()).rejects.toThrow("too large");
1127
+ // Source retirement
1128
+ expect(retireDockerMock).toHaveBeenCalledWith("my-docker");
1129
+ expect(removeAssistantEntryMock).toHaveBeenCalledWith("my-docker");
1448
1130
  } finally {
1449
- globalThis.fetch = originalFetch;
1131
+ restoreFetch();
1450
1132
  }
1451
1133
  });
1452
1134
  });
1453
1135
 
1454
1136
  // ---------------------------------------------------------------------------
1455
- // Platform teleport org ID and reordered flow tests
1137
+ // Target-platform URL threading: signed URL must be requested from the same
1138
+ // platform instance the import will run against. Codex P2 regression guard.
1456
1139
  // ---------------------------------------------------------------------------
1457
1140
 
1458
- describe("platform teleport org ID and reordered flow", () => {
1459
- test("hatchAssistant is called without orgId (authHeaders fetches it internally)", async () => {
1460
- setArgv("--from", "my-local", "--platform");
1461
-
1462
- const localEntry = makeEntry("my-local", { cloud: "local" });
1463
-
1464
- findAssistantByNameMock.mockImplementation((name: string) => {
1465
- if (name === "my-local") return localEntry;
1466
- return null;
1467
- });
1468
-
1469
- const originalFetch = globalThis.fetch;
1470
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1471
-
1472
- try {
1473
- await teleport();
1474
-
1475
- // hatchAssistant should be called with just the token (orgId is resolved internally by authHeaders)
1476
- expect(hatchAssistantMock).toHaveBeenCalledWith("platform-token");
1477
- } finally {
1478
- globalThis.fetch = originalFetch;
1479
- }
1480
- });
1481
-
1482
- test("upload to GCS happens before hatchAssistant for platform targets", async () => {
1483
- setArgv("--from", "my-local", "--platform");
1484
-
1485
- const localEntry = makeEntry("my-local", { cloud: "local" });
1486
-
1487
- findAssistantByNameMock.mockImplementation((name: string) => {
1488
- if (name === "my-local") return localEntry;
1489
- return null;
1490
- });
1491
-
1492
- const callOrder: string[] = [];
1493
-
1494
- platformRequestUploadUrlMock.mockImplementation(async () => {
1495
- callOrder.push("platformRequestUploadUrl");
1496
- return {
1497
- uploadUrl: "https://storage.googleapis.com/bucket/signed-upload-url",
1498
- bundleKey: "bundle-key-123",
1499
- expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1500
- };
1501
- });
1502
-
1503
- platformUploadToSignedUrlMock.mockImplementation(async () => {
1504
- callOrder.push("platformUploadToSignedUrl");
1505
- });
1506
-
1507
- hatchAssistantMock.mockImplementation(async () => {
1508
- callOrder.push("hatchAssistant");
1509
- return {
1510
- assistant: {
1511
- id: "platform-new-id",
1512
- name: "platform-new",
1513
- status: "active",
1514
- },
1515
- reusedExisting: false,
1516
- };
1517
- });
1518
-
1519
- const originalFetch = globalThis.fetch;
1520
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1521
-
1522
- try {
1523
- await teleport();
1524
-
1525
- // Verify ordering: upload steps come before hatch
1526
- const uploadUrlIdx = callOrder.indexOf("platformRequestUploadUrl");
1527
- const uploadIdx = callOrder.indexOf("platformUploadToSignedUrl");
1528
- const hatchIdx = callOrder.indexOf("hatchAssistant");
1529
-
1530
- expect(uploadUrlIdx).toBeGreaterThanOrEqual(0);
1531
- expect(uploadIdx).toBeGreaterThanOrEqual(0);
1532
- expect(hatchIdx).toBeGreaterThanOrEqual(0);
1533
- expect(uploadUrlIdx).toBeLessThan(hatchIdx);
1534
- expect(uploadIdx).toBeLessThan(hatchIdx);
1535
- } finally {
1536
- globalThis.fetch = originalFetch;
1537
- }
1538
- });
1539
-
1540
- test("signed-URL fallback: when platformRequestUploadUrl throws 'not available', falls back to inline upload via importToAssistant", async () => {
1541
- setArgv("--from", "my-local", "--platform");
1141
+ describe("signed-URL request targets the bundle-owning platform", () => {
1142
+ test("local existing platform target with non-default runtimeUrl: upload URL pinned to target's runtimeUrl", async () => {
1143
+ setArgv("--from", "my-local", "--platform", "existing-platform");
1542
1144
 
1543
1145
  const localEntry = makeEntry("my-local", { cloud: "local" });
1146
+ // Crucially, the target's runtimeUrl is NOT the default getPlatformUrl()
1147
+ // return value — this is the regression case Codex flagged.
1148
+ const platformEntry = makeEntry("existing-platform", {
1149
+ cloud: "vellum",
1150
+ runtimeUrl: "https://staging-platform.vellum.ai",
1151
+ });
1544
1152
 
1545
1153
  findAssistantByNameMock.mockImplementation((name: string) => {
1546
1154
  if (name === "my-local") return localEntry;
1155
+ if (name === "existing-platform") return platformEntry;
1547
1156
  return null;
1548
1157
  });
1549
1158
 
1550
- // Simulate 503 — signed uploads not available
1551
- platformRequestUploadUrlMock.mockRejectedValue(
1552
- new Error("Signed uploads are not available on this platform instance"),
1553
- );
1554
-
1555
- const originalFetch = globalThis.fetch;
1556
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1557
-
1159
+ const restoreFetch = installTrackingFetch();
1558
1160
  try {
1559
1161
  await teleport();
1560
1162
 
1561
- // Upload URL was attempted but failed
1562
- expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1563
- // No signed URL upload should have happened
1564
- expect(platformUploadToSignedUrlMock).not.toHaveBeenCalled();
1565
- // Should NOT use GCS-based import
1566
- expect(platformImportBundleFromGcsMock).not.toHaveBeenCalled();
1567
- // Should fall back to inline import
1568
- expect(platformImportBundleMock).toHaveBeenCalled();
1569
- // Hatch should still succeed
1570
- expect(hatchAssistantMock).toHaveBeenCalled();
1571
- expect(consoleLogSpy).toHaveBeenCalledWith(
1572
- expect.stringContaining("Teleport complete"),
1163
+ // The signed-URL request for upload MUST target the existing
1164
+ // platform assistant's runtimeUrl, not the default platform URL.
1165
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1166
+ expect.objectContaining({ operation: "upload" }),
1167
+ "platform-token",
1168
+ "https://staging-platform.vellum.ai",
1169
+ );
1170
+
1171
+ // And the import must run against the same platform.
1172
+ expect(platformImportBundleFromGcsMock).toHaveBeenCalledWith(
1173
+ "bundle-key-123",
1174
+ "platform-token",
1175
+ "https://staging-platform.vellum.ai",
1573
1176
  );
1177
+
1178
+ // Assert none of the signed-URL calls used the default URL — if any
1179
+ // did, upload and download would hit different platforms.
1180
+ for (const call of platformRequestSignedUrlMock.mock.calls) {
1181
+ expect(call[2]).toBe("https://staging-platform.vellum.ai");
1182
+ }
1574
1183
  } finally {
1575
- globalThis.fetch = originalFetch;
1184
+ restoreFetch();
1576
1185
  }
1577
1186
  });
1578
1187
 
1579
- test("bundleKey from pre-upload is forwarded to platformImportBundleFromGcs", async () => {
1580
- setArgv("--from", "my-local", "--platform");
1188
+ test("platform local with non-default source runtimeUrl: download URL pinned to source's runtimeUrl", async () => {
1189
+ setArgv("--from", "my-platform", "--local", "my-local");
1581
1190
 
1191
+ const platformEntry = makeEntry("my-platform", {
1192
+ cloud: "vellum",
1193
+ runtimeUrl: "https://dev-platform.vellum.ai",
1194
+ });
1582
1195
  const localEntry = makeEntry("my-local", { cloud: "local" });
1583
1196
 
1584
1197
  findAssistantByNameMock.mockImplementation((name: string) => {
1198
+ if (name === "my-platform") return platformEntry;
1585
1199
  if (name === "my-local") return localEntry;
1586
1200
  return null;
1587
1201
  });
1588
1202
 
1589
- // Return a specific bundle key from the pre-upload step
1590
- platformRequestUploadUrlMock.mockResolvedValue({
1591
- uploadUrl: "https://storage.googleapis.com/bucket/signed-upload-url",
1592
- bundleKey: "pre-uploaded-key-789",
1593
- expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1203
+ platformPollJobStatusMock.mockResolvedValue({
1204
+ jobId: "platform-export-job-1",
1205
+ type: "export",
1206
+ status: "complete",
1207
+ bundleKey: "dev-bundle-key",
1594
1208
  });
1595
1209
 
1596
- const originalFetch = globalThis.fetch;
1597
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1210
+ // Bundle key flows from the upload signed-URL request now; pin it so the
1211
+ // download-URL assertion below uses the same key.
1212
+ platformRequestSignedUrlMock.mockImplementation(async (params) => ({
1213
+ url:
1214
+ params.operation === "upload"
1215
+ ? "https://storage.googleapis.com/bucket/signed-upload"
1216
+ : "https://storage.googleapis.com/bucket/signed-download",
1217
+ bundleKey: params.bundleKey ?? "dev-bundle-key",
1218
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1219
+ }));
1598
1220
 
1221
+ const restoreFetch = installTrackingFetch();
1599
1222
  try {
1600
1223
  await teleport();
1601
1224
 
1602
- // The bundle key from the pre-upload step should be forwarded to GCS import
1603
- expect(platformImportBundleFromGcsMock).toHaveBeenCalledWith(
1604
- "pre-uploaded-key-789",
1225
+ // The download URL must be requested from the SOURCE platform (where
1226
+ // the bundle was written by the runtime export), not the default.
1227
+ expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
1228
+ { operation: "download", bundleKey: "dev-bundle-key" },
1605
1229
  "platform-token",
1606
- expect.any(String),
1230
+ "https://dev-platform.vellum.ai",
1607
1231
  );
1608
- // Inline import should NOT be used since signed upload succeeded
1609
- expect(platformImportBundleMock).not.toHaveBeenCalled();
1610
1232
  } finally {
1611
- globalThis.fetch = originalFetch;
1233
+ restoreFetch();
1612
1234
  }
1613
1235
  });
1614
1236
  });
1615
1237
 
1616
1238
  // ---------------------------------------------------------------------------
1617
- // Pre-check: block teleport to platform when existing assistant detected
1239
+ // Invariants: CLI never buffers bundle bytes
1618
1240
  // ---------------------------------------------------------------------------
1619
1241
 
1620
- describe("pre-check: block teleport to platform when existing assistant detected", () => {
1621
- test("blocks BEFORE GCS upload when pre-check finds existing assistant", async () => {
1622
- setArgv("--from", "my-local", "--platform");
1623
-
1624
- const localEntry = makeEntry("my-local", { cloud: "local" });
1242
+ describe("CLI never buffers bundle bytes", () => {
1243
+ // A teleport bundle is always > 1 KiB in practice; anything near that size
1244
+ // would mean the CLI is shuttling bytes when it shouldn't.
1245
+ const BUNDLE_BODY_THRESHOLD_BYTES = 1024;
1625
1246
 
1626
- findAssistantByNameMock.mockImplementation((name: string) => {
1627
- if (name === "my-local") return localEntry;
1628
- return null;
1629
- });
1247
+ function bodySize(body: unknown): number {
1248
+ if (typeof body === "string") return body.length;
1249
+ if (body instanceof Uint8Array) return body.byteLength;
1250
+ if (body instanceof ArrayBuffer) return body.byteLength;
1251
+ if (body instanceof Blob) return body.size;
1252
+ return 0;
1253
+ }
1630
1254
 
1631
- // Pre-check returns an existing assistant
1632
- checkExistingPlatformAssistantMock.mockResolvedValue({
1633
- id: "existing-platform-id",
1634
- name: "existing-platform",
1635
- status: "active",
1636
- });
1255
+ test("local → platform: no fetch call carries a bundle-sized body", async () => {
1256
+ setArgv("--from", "my-local", "--platform");
1637
1257
 
1638
- const originalFetch = globalThis.fetch;
1639
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1258
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1259
+ findAssistantByNameMock.mockImplementation((name: string) =>
1260
+ name === "my-local" ? localEntry : null,
1261
+ );
1640
1262
 
1263
+ const restoreFetch = installTrackingFetch();
1641
1264
  try {
1642
- await expect(teleport()).rejects.toThrow("process.exit:1");
1643
-
1644
- // Pre-check should have been called
1645
- expect(checkExistingPlatformAssistantMock).toHaveBeenCalledWith(
1646
- "platform-token",
1647
- undefined,
1648
- );
1649
-
1650
- // GCS upload should NOT have been attempted
1651
- expect(platformRequestUploadUrlMock).not.toHaveBeenCalled();
1652
- expect(platformUploadToSignedUrlMock).not.toHaveBeenCalled();
1653
-
1654
- // Hatch should NOT have been called
1655
- expect(hatchAssistantMock).not.toHaveBeenCalled();
1656
-
1657
- // Error message should be actionable
1658
- expect(consoleErrorSpy).toHaveBeenCalledWith(
1659
- expect.stringContaining("already have a platform assistant"),
1660
- );
1661
- expect(consoleErrorSpy).toHaveBeenCalledWith(
1662
- expect.stringContaining("Retire it first"),
1663
- );
1265
+ await teleport();
1664
1266
 
1665
- // The existing assistant is saved to the lockfile
1666
- expect(saveAssistantEntryMock).toHaveBeenCalledWith(
1667
- expect.objectContaining({
1668
- assistantId: "existing-platform-id",
1669
- cloud: "vellum",
1670
- }),
1671
- );
1267
+ for (const call of fetchCalls) {
1268
+ expect(bodySize(call.body)).toBeLessThan(BUNDLE_BODY_THRESHOLD_BYTES);
1269
+ }
1672
1270
  } finally {
1673
- globalThis.fetch = originalFetch;
1271
+ restoreFetch();
1674
1272
  }
1675
1273
  });
1676
1274
 
1677
- test("skips pre-check when targeting an existing named assistant", async () => {
1678
- setArgv("--from", "my-local", "--platform", "existing-platform");
1275
+ test("platform local: no fetch call carries a bundle-sized body", async () => {
1276
+ setArgv("--from", "my-platform", "--local", "my-local");
1679
1277
 
1680
- const localEntry = makeEntry("my-local", { cloud: "local" });
1681
- const platformEntry = makeEntry("existing-platform", {
1278
+ const platformEntry = makeEntry("my-platform", {
1682
1279
  cloud: "vellum",
1683
1280
  runtimeUrl: "https://platform.vellum.ai",
1684
1281
  });
1282
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1685
1283
 
1686
1284
  findAssistantByNameMock.mockImplementation((name: string) => {
1285
+ if (name === "my-platform") return platformEntry;
1687
1286
  if (name === "my-local") return localEntry;
1688
- if (name === "existing-platform") return platformEntry;
1689
1287
  return null;
1690
1288
  });
1691
1289
 
1692
- const originalFetch = globalThis.fetch;
1693
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1290
+ platformPollJobStatusMock.mockResolvedValue({
1291
+ jobId: "platform-export-job-1",
1292
+ type: "export",
1293
+ status: "complete",
1294
+ bundleKey: "platform-exports/org-1/bundle.vbundle",
1295
+ });
1694
1296
 
1297
+ const restoreFetch = installTrackingFetch();
1695
1298
  try {
1696
1299
  await teleport();
1697
1300
 
1698
- // Pre-check should NOT be called when targeting a known named assistant
1699
- expect(checkExistingPlatformAssistantMock).not.toHaveBeenCalled();
1700
-
1701
- // Upload should proceed normally
1702
- expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1703
- expect(consoleLogSpy).toHaveBeenCalledWith(
1704
- expect.stringContaining("Teleport complete"),
1705
- );
1301
+ for (const call of fetchCalls) {
1302
+ expect(bodySize(call.body)).toBeLessThan(BUNDLE_BODY_THRESHOLD_BYTES);
1303
+ }
1706
1304
  } finally {
1707
- globalThis.fetch = originalFetch;
1305
+ restoreFetch();
1708
1306
  }
1709
1307
  });
1308
+ });
1309
+
1310
+ // ---------------------------------------------------------------------------
1311
+ // Polling behavior
1312
+ // ---------------------------------------------------------------------------
1710
1313
 
1711
- test("pre-check failure is non-fatal — teleport proceeds", async () => {
1314
+ describe("polling", () => {
1315
+ test("local-runtime export poll continues until complete", async () => {
1712
1316
  setArgv("--from", "my-local", "--platform");
1713
1317
 
1714
1318
  const localEntry = makeEntry("my-local", { cloud: "local" });
1319
+ findAssistantByNameMock.mockImplementation((name: string) =>
1320
+ name === "my-local" ? localEntry : null,
1321
+ );
1715
1322
 
1716
- findAssistantByNameMock.mockImplementation((name: string) => {
1717
- if (name === "my-local") return localEntry;
1718
- return null;
1719
- });
1720
-
1721
- // Pre-check returns null (no existing assistant or API failed gracefully)
1722
- checkExistingPlatformAssistantMock.mockResolvedValue(null);
1723
-
1724
- const originalFetch = globalThis.fetch;
1725
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1323
+ let callIdx = 0;
1324
+ localRuntimePollJobStatusMock.mockImplementation(
1325
+ async (_runtimeUrl, _token, jobId) => {
1326
+ callIdx++;
1327
+ if (callIdx < 3) {
1328
+ return {
1329
+ jobId,
1330
+ type: "export" as const,
1331
+ status: "processing" as const,
1332
+ };
1333
+ }
1334
+ return {
1335
+ jobId,
1336
+ type: "export" as const,
1337
+ status: "complete" as const,
1338
+ result: undefined,
1339
+ };
1340
+ },
1341
+ );
1726
1342
 
1343
+ const restoreFetch = installTrackingFetch();
1727
1344
  try {
1728
1345
  await teleport();
1729
-
1730
- // Pre-check was called
1731
- expect(checkExistingPlatformAssistantMock).toHaveBeenCalled();
1732
-
1733
- // Upload and hatch should proceed normally
1734
- expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1735
- expect(hatchAssistantMock).toHaveBeenCalled();
1736
- expect(consoleLogSpy).toHaveBeenCalledWith(
1737
- expect.stringContaining("Teleport complete"),
1738
- );
1346
+ expect(callIdx).toBeGreaterThanOrEqual(3);
1739
1347
  } finally {
1740
- globalThis.fetch = originalFetch;
1348
+ restoreFetch();
1741
1349
  }
1742
1350
  });
1743
- });
1744
-
1745
- // ---------------------------------------------------------------------------
1746
- // Version guard: block platform→non-platform when target is behind
1747
- // ---------------------------------------------------------------------------
1748
1351
 
1749
- describe("version guard: block platformnon-platform when target is behind", () => {
1750
- test("blocks platform→local when local version is behind platform", async () => {
1751
- setArgv("--from", "my-platform", "--local", "my-local");
1352
+ test("local-runtime export failureteleport exits 1", async () => {
1353
+ setArgv("--from", "my-local", "--platform");
1752
1354
 
1753
- const platformEntry = makeEntry("my-platform", {
1754
- cloud: "vellum",
1755
- runtimeUrl: "https://platform.vellum.ai",
1756
- });
1757
1355
  const localEntry = makeEntry("my-local", { cloud: "local" });
1356
+ findAssistantByNameMock.mockImplementation((name: string) =>
1357
+ name === "my-local" ? localEntry : null,
1358
+ );
1758
1359
 
1759
- findAssistantByNameMock.mockImplementation((name: string) => {
1760
- if (name === "my-platform") return platformEntry;
1761
- if (name === "my-local") return localEntry;
1762
- return null;
1763
- });
1764
-
1765
- // Source (platform) is on 0.7.0, target (local) is on 0.6.0
1766
- fetchCurrentVersionMock.mockImplementation((url: string) => {
1767
- if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
1768
- return Promise.resolve("0.6.0");
1360
+ localRuntimePollJobStatusMock.mockResolvedValue({
1361
+ jobId: "local-export-job-1",
1362
+ type: "export",
1363
+ status: "failed",
1364
+ error: "simulated failure",
1769
1365
  });
1770
1366
 
1771
- const originalFetch = globalThis.fetch;
1772
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1773
-
1367
+ const restoreFetch = installTrackingFetch();
1774
1368
  try {
1775
1369
  await expect(teleport()).rejects.toThrow("process.exit:1");
1776
1370
  expect(consoleErrorSpy).toHaveBeenCalledWith(
1777
- expect.stringContaining("is running 0.6.0"),
1778
- );
1779
- expect(consoleErrorSpy).toHaveBeenCalledWith(
1780
- expect.stringContaining("Upgrade your local assistant first"),
1371
+ expect.stringContaining("simulated failure"),
1781
1372
  );
1782
1373
  } finally {
1783
- globalThis.fetch = originalFetch;
1374
+ restoreFetch();
1784
1375
  }
1785
1376
  });
1786
1377
 
1787
- test("allows platformlocal when versions are equal", async () => {
1378
+ test("local-runtime import failure teleport exits 1", async () => {
1788
1379
  setArgv("--from", "my-platform", "--local", "my-local");
1789
1380
 
1790
1381
  const platformEntry = makeEntry("my-platform", {
@@ -1799,57 +1390,89 @@ describe("version guard: block platform→non-platform when target is behind", (
1799
1390
  return null;
1800
1391
  });
1801
1392
 
1802
- // Both on 0.7.0
1803
- fetchCurrentVersionMock.mockResolvedValue("0.7.0");
1804
-
1805
- const originalFetch = globalThis.fetch;
1806
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1393
+ platformPollJobStatusMock.mockResolvedValue({
1394
+ jobId: "platform-export-job-1",
1395
+ type: "export",
1396
+ status: "complete",
1397
+ bundleKey: "key-1",
1398
+ });
1399
+
1400
+ localRuntimePollJobStatusMock.mockImplementation(
1401
+ async (_runtimeUrl, _token, jobId) => {
1402
+ if (jobId.includes("import")) {
1403
+ return {
1404
+ jobId,
1405
+ type: "import" as const,
1406
+ status: "failed" as const,
1407
+ error: "import blew up",
1408
+ };
1409
+ }
1410
+ return {
1411
+ jobId,
1412
+ type: "export" as const,
1413
+ status: "complete" as const,
1414
+ result: undefined,
1415
+ };
1416
+ },
1417
+ );
1807
1418
 
1419
+ const restoreFetch = installTrackingFetch();
1808
1420
  try {
1809
- await teleport();
1810
- expect(consoleLogSpy).toHaveBeenCalledWith(
1811
- expect.stringContaining("Teleport complete"),
1421
+ await expect(teleport()).rejects.toThrow("process.exit:1");
1422
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1423
+ expect.stringContaining("import blew up"),
1812
1424
  );
1813
1425
  } finally {
1814
- globalThis.fetch = originalFetch;
1426
+ restoreFetch();
1815
1427
  }
1816
1428
  });
1429
+ });
1817
1430
 
1818
- test("allows platform→local when local is ahead of platform", async () => {
1819
- setArgv("--from", "my-platform", "--local", "my-local");
1431
+ // ---------------------------------------------------------------------------
1432
+ // MigrationInProgressError handling
1433
+ // ---------------------------------------------------------------------------
1434
+
1435
+ describe("MigrationInProgressError handling", () => {
1436
+ test("local-runtime export already in flight → fail fast with existing job id", async () => {
1437
+ setArgv("--from", "my-local", "--platform");
1820
1438
 
1821
- const platformEntry = makeEntry("my-platform", {
1822
- cloud: "vellum",
1823
- runtimeUrl: "https://platform.vellum.ai",
1824
- });
1825
1439
  const localEntry = makeEntry("my-local", { cloud: "local" });
1440
+ findAssistantByNameMock.mockImplementation((name: string) =>
1441
+ name === "my-local" ? localEntry : null,
1442
+ );
1826
1443
 
1827
- findAssistantByNameMock.mockImplementation((name: string) => {
1828
- if (name === "my-platform") return platformEntry;
1829
- if (name === "my-local") return localEntry;
1830
- return null;
1831
- });
1444
+ localRuntimeExportToGcsMock.mockRejectedValue(
1445
+ new localRuntimeClient.MigrationInProgressError(
1446
+ "export_in_progress",
1447
+ "existing-job-42",
1448
+ ),
1449
+ );
1832
1450
 
1833
- // Source (platform) is on 0.7.0, target (local) is on 0.8.0
1834
- fetchCurrentVersionMock.mockImplementation((url: string) => {
1835
- if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
1836
- return Promise.resolve("0.8.0");
1837
- });
1451
+ const restoreFetch = installTrackingFetch();
1452
+ try {
1453
+ await expect(teleport()).rejects.toThrow("process.exit:1");
1838
1454
 
1839
- const originalFetch = globalThis.fetch;
1840
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1455
+ // Must not have polled the existing job — the existing job's bundle
1456
+ // lives at a different GCS key (its caller's signed URL), so polling
1457
+ // it would leave the teleport pointing at an empty/unrelated bundle.
1458
+ const polledIds = localRuntimePollJobStatusMock.mock.calls.map(
1459
+ (call: unknown[]) => call[2],
1460
+ );
1461
+ expect(polledIds).not.toContain("existing-job-42");
1841
1462
 
1842
- try {
1843
- await teleport();
1844
- expect(consoleLogSpy).toHaveBeenCalledWith(
1845
- expect.stringContaining("Teleport complete"),
1463
+ // Error must mention the existing job id so the user can act on it.
1464
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1465
+ expect.stringContaining("existing-job-42"),
1466
+ );
1467
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1468
+ expect.stringContaining("already in progress"),
1846
1469
  );
1847
1470
  } finally {
1848
- globalThis.fetch = originalFetch;
1471
+ restoreFetch();
1849
1472
  }
1850
1473
  });
1851
1474
 
1852
- test("allows teleport when source version cannot be fetched (best-effort)", async () => {
1475
+ test("local-runtime import already in flight fail fast with existing job id", async () => {
1853
1476
  setArgv("--from", "my-platform", "--local", "my-local");
1854
1477
 
1855
1478
  const platformEntry = makeEntry("my-platform", {
@@ -1864,62 +1487,104 @@ describe("version guard: block platform→non-platform when target is behind", (
1864
1487
  return null;
1865
1488
  });
1866
1489
 
1867
- // Source (platform) is unreachable, target (local) is on 0.6.0
1868
- fetchCurrentVersionMock.mockImplementation((url: string) => {
1869
- if (url === "https://platform.vellum.ai")
1870
- return Promise.resolve(undefined);
1871
- return Promise.resolve("0.6.0");
1490
+ platformPollJobStatusMock.mockResolvedValue({
1491
+ jobId: "platform-export-job-1",
1492
+ type: "export",
1493
+ status: "complete",
1494
+ bundleKey: "bundle-key-from-platform",
1872
1495
  });
1873
1496
 
1874
- const originalFetch = globalThis.fetch;
1875
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1497
+ localRuntimeImportFromGcsMock.mockRejectedValue(
1498
+ new localRuntimeClient.MigrationInProgressError(
1499
+ "import_in_progress",
1500
+ "existing-import-99",
1501
+ ),
1502
+ );
1876
1503
 
1504
+ const restoreFetch = installTrackingFetch();
1877
1505
  try {
1878
- await teleport();
1879
- expect(consoleLogSpy).toHaveBeenCalledWith(
1880
- expect.stringContaining("Teleport complete"),
1506
+ await expect(teleport()).rejects.toThrow("process.exit:1");
1507
+
1508
+ // Must not poll the existing import — it's importing somebody else's
1509
+ // bundle, not ours, so reporting on it would be misleading.
1510
+ const polledIds = localRuntimePollJobStatusMock.mock.calls.map(
1511
+ (call: unknown[]) => call[2],
1512
+ );
1513
+ expect(polledIds).not.toContain("existing-import-99");
1514
+
1515
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1516
+ expect.stringContaining("existing-import-99"),
1517
+ );
1518
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1519
+ expect.stringContaining("already in progress"),
1881
1520
  );
1882
1521
  } finally {
1883
- globalThis.fetch = originalFetch;
1522
+ restoreFetch();
1884
1523
  }
1885
1524
  });
1525
+ });
1886
1526
 
1887
- test("allows teleport when target version cannot be fetched (best-effort)", async () => {
1888
- setArgv("--from", "my-platform", "--local", "my-local");
1527
+ // ---------------------------------------------------------------------------
1528
+ // Dry-run behavior
1529
+ // ---------------------------------------------------------------------------
1889
1530
 
1890
- const platformEntry = makeEntry("my-platform", {
1531
+ describe("dry-run", () => {
1532
+ test("dry-run without existing target does not hatch or export", async () => {
1533
+ setArgv("--from", "my-local", "--docker", "--dry-run");
1534
+
1535
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1536
+ findAssistantByNameMock.mockImplementation((name: string) =>
1537
+ name === "my-local" ? localEntry : null,
1538
+ );
1539
+
1540
+ await teleport();
1541
+
1542
+ expect(hatchDockerMock).not.toHaveBeenCalled();
1543
+ expect(hatchLocalMock).not.toHaveBeenCalled();
1544
+ expect(hatchAssistantMock).not.toHaveBeenCalled();
1545
+ expect(retireLocalMock).not.toHaveBeenCalled();
1546
+ expect(retireDockerMock).not.toHaveBeenCalled();
1547
+ expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
1548
+ expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
1549
+ });
1550
+
1551
+ test("dry-run with existing platform target runs preflight-from-gcs", async () => {
1552
+ setArgv(
1553
+ "--from",
1554
+ "my-local",
1555
+ "--platform",
1556
+ "existing-platform",
1557
+ "--dry-run",
1558
+ );
1559
+
1560
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1561
+ const platformEntry = makeEntry("existing-platform", {
1891
1562
  cloud: "vellum",
1892
1563
  runtimeUrl: "https://platform.vellum.ai",
1893
1564
  });
1894
- const localEntry = makeEntry("my-local", { cloud: "local" });
1895
1565
 
1896
1566
  findAssistantByNameMock.mockImplementation((name: string) => {
1897
- if (name === "my-platform") return platformEntry;
1898
1567
  if (name === "my-local") return localEntry;
1568
+ if (name === "existing-platform") return platformEntry;
1899
1569
  return null;
1900
1570
  });
1901
1571
 
1902
- // Source (platform) is on 0.7.0, target (local) is unreachable
1903
- fetchCurrentVersionMock.mockImplementation((url: string) => {
1904
- if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
1905
- return Promise.resolve(undefined);
1906
- });
1907
-
1908
- const originalFetch = globalThis.fetch;
1909
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1910
-
1572
+ const restoreFetch = installTrackingFetch();
1911
1573
  try {
1912
1574
  await teleport();
1913
- expect(consoleLogSpy).toHaveBeenCalledWith(
1914
- expect.stringContaining("Teleport complete"),
1575
+ expect(platformImportPreflightFromGcsMock).toHaveBeenCalledWith(
1576
+ "bundle-key-123",
1577
+ "platform-token",
1578
+ "https://platform.vellum.ai",
1915
1579
  );
1580
+ expect(hatchAssistantMock).not.toHaveBeenCalled();
1916
1581
  } finally {
1917
- globalThis.fetch = originalFetch;
1582
+ restoreFetch();
1918
1583
  }
1919
1584
  });
1920
1585
 
1921
- test("pre-release target is behind release source: 0.7.0-local.xxx < 0.7.0", async () => {
1922
- setArgv("--from", "my-platform", "--local", "my-local");
1586
+ test("dry-run against local target fails fast (no preflight-from-gcs runtime endpoint yet)", async () => {
1587
+ setArgv("--from", "my-platform", "--local", "my-local", "--dry-run");
1923
1588
 
1924
1589
  const platformEntry = makeEntry("my-platform", {
1925
1590
  cloud: "vellum",
@@ -1933,64 +1598,106 @@ describe("version guard: block platform→non-platform when target is behind", (
1933
1598
  return null;
1934
1599
  });
1935
1600
 
1936
- // Per semver, pre-release < release for same core version
1937
- fetchCurrentVersionMock.mockImplementation((url: string) => {
1938
- if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
1939
- return Promise.resolve("0.7.0-local.20260411.abc123");
1601
+ platformPollJobStatusMock.mockResolvedValue({
1602
+ jobId: "platform-export-job-1",
1603
+ type: "export",
1604
+ status: "complete",
1605
+ bundleKey: "bundle-key-from-platform",
1940
1606
  });
1941
1607
 
1942
- const originalFetch = globalThis.fetch;
1943
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1944
-
1608
+ const restoreFetch = installTrackingFetch();
1945
1609
  try {
1946
1610
  await expect(teleport()).rejects.toThrow("process.exit:1");
1947
1611
  expect(consoleErrorSpy).toHaveBeenCalledWith(
1948
- expect.stringContaining("is running 0.7.0-local.20260411.abc123"),
1612
+ expect.stringContaining(
1613
+ "--dry-run is not yet supported for local or docker targets",
1614
+ ),
1949
1615
  );
1616
+
1617
+ // Must fail BEFORE any export work — no signed URL request, no runtime
1618
+ // export kickoff, nothing that costs time or bandwidth.
1619
+ expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
1620
+ expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
1950
1621
  } finally {
1951
- globalThis.fetch = originalFetch;
1622
+ restoreFetch();
1952
1623
  }
1953
1624
  });
1625
+ });
1626
+
1627
+ // ---------------------------------------------------------------------------
1628
+ // Pre-check: block teleport to platform when existing assistant detected
1629
+ // ---------------------------------------------------------------------------
1630
+
1631
+ describe("pre-check: existing platform assistant", () => {
1632
+ test("blocks before any work when pre-check finds existing assistant", async () => {
1633
+ setArgv("--from", "my-local", "--platform");
1634
+
1635
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1636
+ findAssistantByNameMock.mockImplementation((name: string) =>
1637
+ name === "my-local" ? localEntry : null,
1638
+ );
1639
+
1640
+ checkExistingPlatformAssistantMock.mockResolvedValue({
1641
+ id: "existing-platform-id",
1642
+ name: "existing-platform",
1643
+ status: "active",
1644
+ });
1645
+
1646
+ await expect(teleport()).rejects.toThrow("process.exit:1");
1647
+
1648
+ expect(checkExistingPlatformAssistantMock).toHaveBeenCalledWith(
1649
+ "platform-token",
1650
+ undefined,
1651
+ );
1652
+ // No signed-URL or runtime calls
1653
+ expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
1654
+ expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
1655
+ expect(hatchAssistantMock).not.toHaveBeenCalled();
1656
+
1657
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
1658
+ expect.stringContaining("already have a platform assistant"),
1659
+ );
1660
+ });
1661
+ });
1662
+
1663
+ // ---------------------------------------------------------------------------
1664
+ // Version guard
1665
+ // ---------------------------------------------------------------------------
1954
1666
 
1955
- test("blocks platform→docker when docker version is behind platform", async () => {
1956
- setArgv("--from", "my-platform", "--docker", "my-docker");
1667
+ describe("version guard", () => {
1668
+ test("blocks platform→local when local version is behind", async () => {
1669
+ setArgv("--from", "my-platform", "--local", "my-local");
1957
1670
 
1958
1671
  const platformEntry = makeEntry("my-platform", {
1959
1672
  cloud: "vellum",
1960
1673
  runtimeUrl: "https://platform.vellum.ai",
1961
1674
  });
1962
- const dockerEntry = makeEntry("my-docker", { cloud: "docker" });
1675
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1963
1676
 
1964
1677
  findAssistantByNameMock.mockImplementation((name: string) => {
1965
1678
  if (name === "my-platform") return platformEntry;
1966
- if (name === "my-docker") return dockerEntry;
1679
+ if (name === "my-local") return localEntry;
1967
1680
  return null;
1968
1681
  });
1969
1682
 
1970
- // Source (platform) is on 0.7.0, target (docker) is on 0.5.0
1971
1683
  fetchCurrentVersionMock.mockImplementation((url: string) => {
1972
1684
  if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
1973
- return Promise.resolve("0.5.0");
1685
+ return Promise.resolve("0.6.0");
1974
1686
  });
1975
1687
 
1976
- const originalFetch = globalThis.fetch;
1977
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1978
-
1688
+ const restoreFetch = installTrackingFetch();
1979
1689
  try {
1980
1690
  await expect(teleport()).rejects.toThrow("process.exit:1");
1981
1691
  expect(consoleErrorSpy).toHaveBeenCalledWith(
1982
- expect.stringContaining("is running 0.5.0"),
1983
- );
1984
- expect(consoleErrorSpy).toHaveBeenCalledWith(
1985
- expect.stringContaining("Upgrade your docker assistant first"),
1692
+ expect.stringContaining("is running 0.6.0"),
1986
1693
  );
1987
1694
  } finally {
1988
- globalThis.fetch = originalFetch;
1695
+ restoreFetch();
1989
1696
  }
1990
1697
  });
1991
1698
 
1992
- test("dry-run: blocks platform→local when local version is behind", async () => {
1993
- setArgv("--from", "my-platform", "--local", "my-local", "--dry-run");
1699
+ test("allows equal versions", async () => {
1700
+ setArgv("--from", "my-platform", "--local", "my-local");
1994
1701
 
1995
1702
  const platformEntry = makeEntry("my-platform", {
1996
1703
  cloud: "vellum",
@@ -2004,28 +1711,26 @@ describe("version guard: block platform→non-platform when target is behind", (
2004
1711
  return null;
2005
1712
  });
2006
1713
 
2007
- // Source (platform) is on 0.7.0, target (local) is on 0.6.0
2008
- fetchCurrentVersionMock.mockImplementation((url: string) => {
2009
- if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
2010
- return Promise.resolve("0.6.0");
1714
+ fetchCurrentVersionMock.mockResolvedValue("0.7.0");
1715
+ platformPollJobStatusMock.mockResolvedValue({
1716
+ jobId: "platform-export-job-1",
1717
+ type: "export",
1718
+ status: "complete",
1719
+ bundleKey: "b",
2011
1720
  });
2012
1721
 
2013
- const originalFetch = globalThis.fetch;
2014
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
2015
-
1722
+ const restoreFetch = installTrackingFetch();
2016
1723
  try {
2017
- await expect(teleport()).rejects.toThrow("process.exit:1");
2018
- expect(consoleErrorSpy).toHaveBeenCalledWith(
2019
- expect.stringContaining("is running 0.6.0"),
1724
+ await teleport();
1725
+ expect(consoleLogSpy).toHaveBeenCalledWith(
1726
+ expect.stringContaining("Teleport complete"),
2020
1727
  );
2021
1728
  } finally {
2022
- globalThis.fetch = originalFetch;
1729
+ restoreFetch();
2023
1730
  }
2024
1731
  });
2025
1732
 
2026
1733
  test("newly hatched target is cleaned up when version check fails", async () => {
2027
- // No existing local target — teleport will hatch a new one, then
2028
- // the version guard should retire it to avoid orphans.
2029
1734
  setArgv("--from", "my-platform", "--local");
2030
1735
 
2031
1736
  const platformEntry = makeEntry("my-platform", {
@@ -2034,12 +1739,10 @@ describe("version guard: block platform→non-platform when target is behind", (
2034
1739
  });
2035
1740
  const newLocalEntry = makeEntry("new-local", { cloud: "local" });
2036
1741
 
2037
- findAssistantByNameMock.mockImplementation((name: string) => {
2038
- if (name === "my-platform") return platformEntry;
2039
- return null;
2040
- });
1742
+ findAssistantByNameMock.mockImplementation((name: string) =>
1743
+ name === "my-platform" ? platformEntry : null,
1744
+ );
2041
1745
 
2042
- // Simulate hatch creating a new local entry
2043
1746
  loadAllAssistantsMock.mockImplementation(() => {
2044
1747
  if (hatchLocalMock.mock.calls.length > 0) {
2045
1748
  return [platformEntry, newLocalEntry];
@@ -2047,70 +1750,42 @@ describe("version guard: block platform→non-platform when target is behind", (
2047
1750
  return [platformEntry];
2048
1751
  });
2049
1752
 
2050
- // Source (platform) is on 0.7.0, newly hatched local is on 0.6.0
1753
+ platformPollJobStatusMock.mockResolvedValue({
1754
+ jobId: "platform-export-job-1",
1755
+ type: "export",
1756
+ status: "complete",
1757
+ bundleKey: "b",
1758
+ });
1759
+
2051
1760
  fetchCurrentVersionMock.mockImplementation((url: string) => {
2052
1761
  if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
2053
1762
  return Promise.resolve("0.6.0");
2054
1763
  });
2055
1764
 
2056
- const originalFetch = globalThis.fetch;
2057
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
2058
-
1765
+ const restoreFetch = installTrackingFetch();
2059
1766
  try {
2060
1767
  await expect(teleport()).rejects.toThrow("process.exit:1");
2061
- // Should have hatched a new local assistant
2062
1768
  expect(hatchLocalMock).toHaveBeenCalled();
2063
- // Should retire the orphaned assistant
2064
1769
  expect(retireLocalMock).toHaveBeenCalledWith("new-local", newLocalEntry);
2065
1770
  expect(removeAssistantEntryMock).toHaveBeenCalledWith("new-local");
2066
- expect(consoleErrorSpy).toHaveBeenCalledWith(
2067
- expect.stringContaining("Cleaning up newly hatched assistant"),
2068
- );
2069
- } finally {
2070
- globalThis.fetch = originalFetch;
2071
- }
2072
- });
2073
-
2074
- test("does not check versions for local→platform direction", async () => {
2075
- setArgv("--from", "my-local", "--platform");
2076
-
2077
- const localEntry = makeEntry("my-local", { cloud: "local" });
2078
-
2079
- findAssistantByNameMock.mockImplementation((name: string) => {
2080
- if (name === "my-local") return localEntry;
2081
- return null;
2082
- });
2083
-
2084
- const originalFetch = globalThis.fetch;
2085
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
2086
-
2087
- try {
2088
- await teleport();
2089
- // fetchCurrentVersion should NOT be called for local→platform
2090
- expect(fetchCurrentVersionMock).not.toHaveBeenCalled();
2091
- expect(consoleLogSpy).toHaveBeenCalledWith(
2092
- expect.stringContaining("Teleport complete"),
2093
- );
2094
1771
  } finally {
2095
- globalThis.fetch = originalFetch;
1772
+ restoreFetch();
2096
1773
  }
2097
1774
  });
2098
1775
  });
2099
1776
 
2100
1777
  // ---------------------------------------------------------------------------
2101
- // Credential import display tests
1778
+ // Credential import display
2102
1779
  // ---------------------------------------------------------------------------
2103
1780
 
2104
1781
  describe("credential import display", () => {
2105
- test("prints credential counts when credentialsImported is present", async () => {
1782
+ test("prints credential counts when credentialsImported is present (platform target)", async () => {
2106
1783
  setArgv("--from", "my-local", "--platform");
2107
1784
 
2108
1785
  const localEntry = makeEntry("my-local", { cloud: "local" });
2109
-
2110
- findAssistantByNameMock.mockImplementation((name: string) => {
2111
- if (name === "my-local") return localEntry;
2112
- return null;
2113
- });
1786
+ findAssistantByNameMock.mockImplementation((name: string) =>
1787
+ name === "my-local" ? localEntry : null,
1788
+ );
2114
1789
 
2115
1790
  platformImportBundleFromGcsMock.mockResolvedValue({
2116
1791
  statusCode: 200,
@@ -2132,47 +1807,24 @@ describe("credential import display", () => {
2132
1807
  },
2133
1808
  });
2134
1809
 
2135
- const originalFetch = globalThis.fetch;
2136
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
2137
-
1810
+ const restoreFetch = installTrackingFetch();
2138
1811
  try {
2139
1812
  await teleport();
2140
1813
  expect(consoleLogSpy).toHaveBeenCalledWith(" Credentials imported: 5/5");
2141
1814
  } finally {
2142
- globalThis.fetch = originalFetch;
1815
+ restoreFetch();
2143
1816
  }
2144
1817
  });
2145
1818
 
2146
- test("does not print credential line when credentialsImported is absent (old server)", async () => {
1819
+ test("does not print credential line when absent", async () => {
2147
1820
  setArgv("--from", "my-local", "--platform");
2148
1821
 
2149
1822
  const localEntry = makeEntry("my-local", { cloud: "local" });
1823
+ findAssistantByNameMock.mockImplementation((name: string) =>
1824
+ name === "my-local" ? localEntry : null,
1825
+ );
2150
1826
 
2151
- findAssistantByNameMock.mockImplementation((name: string) => {
2152
- if (name === "my-local") return localEntry;
2153
- return null;
2154
- });
2155
-
2156
- // Mock the GCS import path (used by default since platformRequestUploadUrl
2157
- // succeeds) with a response that has no credentialsImported field, simulating
2158
- // an older server.
2159
- platformImportBundleFromGcsMock.mockResolvedValue({
2160
- statusCode: 200,
2161
- body: {
2162
- success: true,
2163
- summary: {
2164
- total_files: 3,
2165
- files_created: 2,
2166
- files_overwritten: 1,
2167
- files_skipped: 0,
2168
- backups_created: 1,
2169
- },
2170
- },
2171
- });
2172
-
2173
- const originalFetch = globalThis.fetch;
2174
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
2175
-
1827
+ const restoreFetch = installTrackingFetch();
2176
1828
  try {
2177
1829
  await teleport();
2178
1830
  const allLogCalls = consoleLogSpy.mock.calls.map((c: unknown[]) => c[0]);
@@ -2182,98 +1834,7 @@ describe("credential import display", () => {
2182
1834
  );
2183
1835
  expect(credentialLines).toHaveLength(0);
2184
1836
  } finally {
2185
- globalThis.fetch = originalFetch;
2186
- }
2187
- });
2188
-
2189
- test("lists failed credential accounts individually", async () => {
2190
- setArgv("--from", "my-local", "--platform");
2191
-
2192
- const localEntry = makeEntry("my-local", { cloud: "local" });
2193
-
2194
- findAssistantByNameMock.mockImplementation((name: string) => {
2195
- if (name === "my-local") return localEntry;
2196
- return null;
2197
- });
2198
-
2199
- platformImportBundleFromGcsMock.mockResolvedValue({
2200
- statusCode: 200,
2201
- body: {
2202
- success: true,
2203
- summary: {
2204
- total_files: 3,
2205
- files_created: 2,
2206
- files_overwritten: 1,
2207
- files_skipped: 0,
2208
- backups_created: 1,
2209
- },
2210
- credentialsImported: {
2211
- total: 5,
2212
- succeeded: 3,
2213
- failed: 2,
2214
- failedAccounts: ["google:user@example.com", "github:octocat"],
2215
- },
2216
- },
2217
- });
2218
-
2219
- const originalFetch = globalThis.fetch;
2220
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
2221
-
2222
- try {
2223
- await teleport();
2224
- expect(consoleLogSpy).toHaveBeenCalledWith(" Credentials imported: 3/5");
2225
- expect(consoleLogSpy).toHaveBeenCalledWith(" Credentials failed: 2");
2226
- expect(consoleLogSpy).toHaveBeenCalledWith(
2227
- " - google:user@example.com",
2228
- );
2229
- expect(consoleLogSpy).toHaveBeenCalledWith(" - github:octocat");
2230
- } finally {
2231
- globalThis.fetch = originalFetch;
2232
- }
2233
- });
2234
-
2235
- test("shows platform credentials skipped count", async () => {
2236
- setArgv("--from", "my-local", "--platform");
2237
-
2238
- const localEntry = makeEntry("my-local", { cloud: "local" });
2239
-
2240
- findAssistantByNameMock.mockImplementation((name: string) => {
2241
- if (name === "my-local") return localEntry;
2242
- return null;
2243
- });
2244
-
2245
- platformImportBundleFromGcsMock.mockResolvedValue({
2246
- statusCode: 200,
2247
- body: {
2248
- success: true,
2249
- summary: {
2250
- total_files: 3,
2251
- files_created: 2,
2252
- files_overwritten: 1,
2253
- files_skipped: 0,
2254
- backups_created: 1,
2255
- },
2256
- credentialsImported: {
2257
- total: 5,
2258
- succeeded: 5,
2259
- failed: 0,
2260
- failedAccounts: [],
2261
- skippedPlatform: 3,
2262
- },
2263
- },
2264
- });
2265
-
2266
- const originalFetch = globalThis.fetch;
2267
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
2268
-
2269
- try {
2270
- await teleport();
2271
- expect(consoleLogSpy).toHaveBeenCalledWith(" Credentials imported: 5/5");
2272
- expect(consoleLogSpy).toHaveBeenCalledWith(
2273
- " Platform credentials skipped: 3",
2274
- );
2275
- } finally {
2276
- globalThis.fetch = originalFetch;
1837
+ restoreFetch();
2277
1838
  }
2278
1839
  });
2279
1840
  });
@@ -2282,7 +1843,7 @@ describe("credential import display", () => {
2282
1843
  // Platform credential injection after teleport
2283
1844
  // ---------------------------------------------------------------------------
2284
1845
 
2285
- describe("teleport platform credential injection", () => {
1846
+ describe("platform credential injection", () => {
2286
1847
  test("platform→local teleport calls ensureSelfHostedLocalRegistration and injectCredentialsIntoAssistant", async () => {
2287
1848
  setArgv("--from", "my-platform", "--local", "my-local");
2288
1849
 
@@ -2301,9 +1862,14 @@ describe("teleport platform credential injection", () => {
2301
1862
  return null;
2302
1863
  });
2303
1864
 
2304
- const originalFetch = globalThis.fetch;
2305
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1865
+ platformPollJobStatusMock.mockResolvedValue({
1866
+ jobId: "platform-export-job-1",
1867
+ type: "export",
1868
+ status: "complete",
1869
+ bundleKey: "bundle-xyz",
1870
+ });
2306
1871
 
1872
+ const restoreFetch = installTrackingFetch();
2307
1873
  try {
2308
1874
  await teleport();
2309
1875
  expect(ensureSelfHostedLocalRegistrationMock).toHaveBeenCalledWith(
@@ -2323,76 +1889,87 @@ describe("teleport platform credential injection", () => {
2323
1889
  userId: "user-1",
2324
1890
  webhookSecret: "webhook-secret-123",
2325
1891
  });
2326
- expect(consoleLogSpy).toHaveBeenCalledWith(
2327
- " Platform credentials injected.",
2328
- );
2329
1892
  } finally {
2330
- globalThis.fetch = originalFetch;
1893
+ restoreFetch();
2331
1894
  }
2332
1895
  });
2333
1896
 
2334
- test("platform→docker teleport also calls credential injection", async () => {
2335
- setArgv("--from", "my-platform", "--docker", "my-docker");
1897
+ test("local→docker teleport does NOT call credential injection", async () => {
1898
+ setArgv("--from", "my-local", "--docker", "my-docker");
2336
1899
 
2337
- const platformEntry = makeEntry("my-platform", {
2338
- cloud: "vellum",
2339
- runtimeUrl: "https://platform.vellum.ai",
2340
- });
1900
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2341
1901
  const dockerEntry = makeEntry("my-docker", {
2342
1902
  cloud: "docker",
2343
1903
  runtimeUrl: "http://localhost:8821",
2344
- bearerToken: "docker-bearer",
2345
1904
  });
2346
1905
 
2347
1906
  findAssistantByNameMock.mockImplementation((name: string) => {
2348
- if (name === "my-platform") return platformEntry;
1907
+ if (name === "my-local") return localEntry;
2349
1908
  if (name === "my-docker") return dockerEntry;
2350
1909
  return null;
2351
1910
  });
2352
1911
 
2353
- const originalFetch = globalThis.fetch;
2354
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
2355
-
1912
+ const restoreFetch = installTrackingFetch();
2356
1913
  try {
2357
1914
  await teleport();
2358
- expect(ensureSelfHostedLocalRegistrationMock).toHaveBeenCalled();
2359
- expect(injectCredentialsIntoAssistantMock).toHaveBeenCalled();
2360
- expect(consoleLogSpy).toHaveBeenCalledWith(
2361
- " Platform credentials injected.",
2362
- );
1915
+ expect(ensureSelfHostedLocalRegistrationMock).not.toHaveBeenCalled();
1916
+ expect(injectCredentialsIntoAssistantMock).not.toHaveBeenCalled();
2363
1917
  } finally {
2364
- globalThis.fetch = originalFetch;
1918
+ restoreFetch();
2365
1919
  }
2366
1920
  });
1921
+ });
2367
1922
 
2368
- test("local→local teleport does NOT call credential injection", async () => {
2369
- setArgv("--from", "my-local", "--docker", "my-docker");
1923
+ // ---------------------------------------------------------------------------
1924
+ // Auth / transient-error resilience (Codex P1/P2 regression guards)
1925
+ // ---------------------------------------------------------------------------
1926
+
1927
+ describe("auth + transient-error resilience", () => {
1928
+ test("runtime 401 on export kickoff triggers token refresh and retry", async () => {
1929
+ setArgv("--from", "my-local", "--platform");
2370
1930
 
2371
1931
  const localEntry = makeEntry("my-local", { cloud: "local" });
2372
- const dockerEntry = makeEntry("my-docker", {
2373
- cloud: "docker",
2374
- runtimeUrl: "http://localhost:8821",
2375
- });
1932
+ findAssistantByNameMock.mockImplementation((name: string) =>
1933
+ name === "my-local" ? localEntry : null,
1934
+ );
2376
1935
 
2377
- findAssistantByNameMock.mockImplementation((name: string) => {
2378
- if (name === "my-local") return localEntry;
2379
- if (name === "my-docker") return dockerEntry;
2380
- return null;
1936
+ // First kickoff call fails with 401, second succeeds.
1937
+ localRuntimeExportToGcsMock.mockImplementationOnce(async () => {
1938
+ throw new Error("Local runtime export-to-gcs failed (401): stale token");
2381
1939
  });
1940
+ localRuntimeExportToGcsMock.mockImplementationOnce(async () => ({
1941
+ jobId: "local-export-job-after-refresh",
1942
+ }));
2382
1943
 
2383
- const originalFetch = globalThis.fetch;
2384
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1944
+ // Ensure the refresh path returns a distinguishable token.
1945
+ leaseGuardianTokenMock.mockResolvedValueOnce({
1946
+ accessToken: "refreshed-token",
1947
+ accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
1948
+ } as unknown as Awaited<
1949
+ ReturnType<typeof guardianToken.leaseGuardianToken>
1950
+ >);
2385
1951
 
1952
+ const restoreFetch = installTrackingFetch();
2386
1953
  try {
2387
1954
  await teleport();
2388
- expect(ensureSelfHostedLocalRegistrationMock).not.toHaveBeenCalled();
2389
- expect(injectCredentialsIntoAssistantMock).not.toHaveBeenCalled();
2390
1955
  } finally {
2391
- globalThis.fetch = originalFetch;
1956
+ restoreFetch();
2392
1957
  }
1958
+
1959
+ // Kickoff was attempted twice: once with the cached token, once after
1960
+ // a forced refresh lease.
1961
+ expect(localRuntimeExportToGcsMock).toHaveBeenCalledTimes(2);
1962
+
1963
+ const firstTokenArg = localRuntimeExportToGcsMock.mock.calls[0][1];
1964
+ const secondTokenArg = localRuntimeExportToGcsMock.mock.calls[1][1];
1965
+ expect(firstTokenArg).toBe("local-token");
1966
+ expect(secondTokenArg).toBe("refreshed-token");
1967
+
1968
+ // A fresh lease was requested exactly once (the forceRefresh path).
1969
+ expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
2393
1970
  });
2394
1971
 
2395
- test("if user is not logged in (readPlatformToken returns null), injection is skipped gracefully", async () => {
1972
+ test("runtime 401 on import kickoff triggers token refresh and retry", async () => {
2396
1973
  setArgv("--from", "my-platform", "--local", "my-local");
2397
1974
 
2398
1975
  const platformEntry = makeEntry("my-platform", {
@@ -2407,36 +1984,181 @@ describe("teleport platform credential injection", () => {
2407
1984
  return null;
2408
1985
  });
2409
1986
 
2410
- // The readPlatformToken mock is called twice during teleport:
2411
- // once during the platform export flow and once during credential injection.
2412
- // We need the first call to return a token (for the export to work)
2413
- // and the second call to return null (to test the skip path).
2414
- let callCount = 0;
2415
- readPlatformTokenMock.mockImplementation(() => {
2416
- callCount++;
2417
- // The platform export path calls readPlatformToken first;
2418
- // the credential injection helper calls it after import.
2419
- // Return null only on the last call (the injection helper).
2420
- if (callCount <= 1) return "platform-token";
2421
- return null;
1987
+ platformPollJobStatusMock.mockResolvedValue({
1988
+ jobId: "platform-export-job-1",
1989
+ type: "export",
1990
+ status: "complete",
1991
+ bundleKey: "b-key",
2422
1992
  });
2423
1993
 
2424
- const originalFetch = globalThis.fetch;
2425
- globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1994
+ localRuntimeImportFromGcsMock.mockImplementationOnce(async () => {
1995
+ throw new Error(
1996
+ "Local runtime import-from-gcs failed (401): stale token",
1997
+ );
1998
+ });
1999
+ localRuntimeImportFromGcsMock.mockImplementationOnce(async () => ({
2000
+ jobId: "local-import-after-refresh",
2001
+ }));
2426
2002
 
2003
+ leaseGuardianTokenMock.mockResolvedValueOnce({
2004
+ accessToken: "refreshed-import-token",
2005
+ accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
2006
+ } as unknown as Awaited<
2007
+ ReturnType<typeof guardianToken.leaseGuardianToken>
2008
+ >);
2009
+
2010
+ const restoreFetch = installTrackingFetch();
2427
2011
  try {
2428
2012
  await teleport();
2429
- expect(ensureSelfHostedLocalRegistrationMock).not.toHaveBeenCalled();
2430
- expect(injectCredentialsIntoAssistantMock).not.toHaveBeenCalled();
2431
- expect(consoleLogSpy).toHaveBeenCalledWith(
2432
- " Skipped platform credential injection (not logged in).",
2013
+ } finally {
2014
+ restoreFetch();
2015
+ }
2016
+
2017
+ expect(localRuntimeImportFromGcsMock).toHaveBeenCalledTimes(2);
2018
+ expect(localRuntimeImportFromGcsMock.mock.calls[0][1]).toBe("local-token");
2019
+ expect(localRuntimeImportFromGcsMock.mock.calls[1][1]).toBe(
2020
+ "refreshed-import-token",
2021
+ );
2022
+ });
2023
+
2024
+ test("runtime non-401 errors do NOT trigger token refresh", async () => {
2025
+ setArgv("--from", "my-local", "--platform");
2026
+
2027
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2028
+ findAssistantByNameMock.mockImplementation((name: string) =>
2029
+ name === "my-local" ? localEntry : null,
2030
+ );
2031
+
2032
+ localRuntimeExportToGcsMock.mockRejectedValue(
2033
+ new Error("Local runtime export-to-gcs failed (500): boom"),
2034
+ );
2035
+
2036
+ const restoreFetch = installTrackingFetch();
2037
+ try {
2038
+ await expect(teleport()).rejects.toThrow(/500.*boom/);
2039
+ } finally {
2040
+ restoreFetch();
2041
+ }
2042
+
2043
+ // One attempt, no forced-refresh lease.
2044
+ expect(localRuntimeExportToGcsMock).toHaveBeenCalledTimes(1);
2045
+ expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
2046
+ });
2047
+
2048
+ test("runtime poll 401 mid-migration triggers forceRefresh lease and completes", async () => {
2049
+ setArgv("--from", "my-local", "--platform");
2050
+
2051
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2052
+ findAssistantByNameMock.mockImplementation((name: string) =>
2053
+ name === "my-local" ? localEntry : null,
2054
+ );
2055
+
2056
+ // Export kickoff succeeds with the cached "local-token".
2057
+ // During polling, the first status check fails with 401 (token expired
2058
+ // mid-migration), the poll loop calls refreshOn401 → leaseGuardianToken,
2059
+ // then the next poll succeeds with the new token.
2060
+ const tokensSeenByPoll: string[] = [];
2061
+ localRuntimePollJobStatusMock.mockImplementation(
2062
+ async (_runtimeUrl, token, jobId) => {
2063
+ tokensSeenByPoll.push(token);
2064
+ if (tokensSeenByPoll.length === 1) {
2065
+ throw new Error("Local job status check failed: 401 Unauthorized");
2066
+ }
2067
+ return {
2068
+ jobId,
2069
+ type: "export" as const,
2070
+ status: "complete" as const,
2071
+ result: undefined,
2072
+ };
2073
+ },
2074
+ );
2075
+
2076
+ leaseGuardianTokenMock.mockResolvedValueOnce({
2077
+ accessToken: "poll-refreshed-token",
2078
+ accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
2079
+ } as unknown as Awaited<
2080
+ ReturnType<typeof guardianToken.leaseGuardianToken>
2081
+ >);
2082
+
2083
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
2084
+ const restoreFetch = installTrackingFetch();
2085
+ try {
2086
+ await teleport();
2087
+
2088
+ // The first poll used the cached token; the second (post-refresh) poll
2089
+ // used the freshly leased one.
2090
+ expect(tokensSeenByPoll.length).toBeGreaterThanOrEqual(2);
2091
+ expect(tokensSeenByPoll[0]).toBe("local-token");
2092
+ expect(tokensSeenByPoll[1]).toBe("poll-refreshed-token");
2093
+
2094
+ // leaseGuardianToken was invoked for the forceRefresh path.
2095
+ expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
2096
+
2097
+ // The 401 branch emits its own warning — distinct from the generic
2098
+ // transient-error warning — so this asserts the refresh path fired.
2099
+ expect(warnSpy).toHaveBeenCalledWith(
2100
+ expect.stringContaining("refreshing auth"),
2433
2101
  );
2434
- // Teleport still succeeds
2435
- expect(consoleLogSpy).toHaveBeenCalledWith(
2436
- expect.stringContaining("Teleport complete"),
2102
+ } finally {
2103
+ restoreFetch();
2104
+ warnSpy.mockRestore();
2105
+ }
2106
+ });
2107
+
2108
+ test("transient poll error does not abort teleport (job completes after retry)", async () => {
2109
+ setArgv("--from", "my-local", "--platform");
2110
+
2111
+ const localEntry = makeEntry("my-local", { cloud: "local" });
2112
+ findAssistantByNameMock.mockImplementation((name: string) =>
2113
+ name === "my-local" ? localEntry : null,
2114
+ );
2115
+
2116
+ // Throw once with a 503 (transient), then succeed with terminal complete.
2117
+ let pollCalls = 0;
2118
+ localRuntimePollJobStatusMock.mockImplementation(
2119
+ async (_runtimeUrl, _token, jobId) => {
2120
+ pollCalls += 1;
2121
+ if (pollCalls === 1) {
2122
+ throw new Error(
2123
+ "Local job status check failed: 503 Service Unavailable",
2124
+ );
2125
+ }
2126
+ return {
2127
+ jobId,
2128
+ type: "export" as const,
2129
+ status: "complete" as const,
2130
+ result: undefined,
2131
+ };
2132
+ },
2133
+ );
2134
+
2135
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
2136
+ const restoreFetch = installTrackingFetch();
2137
+ try {
2138
+ // Should NOT reject — a single transient 503 is retried, not fatal.
2139
+ await teleport();
2140
+ expect(pollCalls).toBeGreaterThanOrEqual(2);
2141
+ expect(warnSpy).toHaveBeenCalledWith(
2142
+ expect.stringContaining("polling failed, retrying"),
2437
2143
  );
2438
2144
  } finally {
2439
- globalThis.fetch = originalFetch;
2145
+ restoreFetch();
2146
+ warnSpy.mockRestore();
2440
2147
  }
2441
2148
  });
2442
2149
  });
2150
+
2151
+ // ---------------------------------------------------------------------------
2152
+ // Misc: legacy --to deprecation
2153
+ // ---------------------------------------------------------------------------
2154
+
2155
+ describe("misc", () => {
2156
+ test("legacy --to flag shows deprecation message", async () => {
2157
+ setArgv("--from", "source", "--to", "target");
2158
+
2159
+ await expect(teleport()).rejects.toThrow("process.exit:1");
2160
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
2161
+ expect.stringContaining("--to is deprecated"),
2162
+ );
2163
+ });
2164
+ });