@vellumai/cli 0.6.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -2
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/config-utils.test.ts +159 -0
- package/src/__tests__/env-drift.test.ts +10 -32
- package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
- package/src/__tests__/multi-local.test.ts +0 -5
- package/src/__tests__/sleep.test.ts +1 -2
- package/src/__tests__/teleport.test.ts +919 -1255
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +40 -8
- package/src/commands/hatch.ts +6 -2
- package/src/commands/login.ts +89 -6
- package/src/commands/ps.ts +104 -20
- package/src/commands/retire.ts +23 -0
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +15 -2
- package/src/commands/teleport.ts +447 -583
- package/src/commands/terminal.ts +225 -0
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +304 -152
- package/src/index.ts +6 -0
- package/src/lib/__tests__/docker.test.ts +50 -74
- package/src/lib/__tests__/job-polling.test.ts +278 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +383 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/assistant-config.ts +12 -8
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/config-utils.ts +97 -1
- package/src/lib/docker.ts +73 -75
- package/src/lib/environments/__tests__/paths.test.ts +2 -0
- package/src/lib/environments/resolve.ts +89 -7
- package/src/lib/environments/seeds.ts +8 -5
- package/src/lib/environments/types.ts +10 -0
- package/src/lib/hatch-local.ts +15 -120
- package/src/lib/health-check.ts +98 -0
- package/src/lib/job-polling.ts +195 -0
- package/src/lib/local-runtime-client.ts +178 -0
- package/src/lib/local.ts +139 -15
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +215 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/terminal-client.ts +177 -0
- package/src/lib/terminal-session.ts +457 -0
- package/src/shared/provider-env-vars.ts +2 -3
- 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
|
|
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,
|
|
@@ -101,45 +95,35 @@ const platformInitiateExportMock = spyOn(
|
|
|
101
95
|
platformClient,
|
|
102
96
|
"platformInitiateExport",
|
|
103
97
|
).mockResolvedValue({
|
|
104
|
-
jobId: "job-1",
|
|
98
|
+
jobId: "platform-export-job-1",
|
|
105
99
|
status: "pending",
|
|
106
100
|
});
|
|
107
101
|
|
|
108
|
-
const
|
|
102
|
+
const platformPollJobStatusMock = spyOn(
|
|
109
103
|
platformClient,
|
|
110
|
-
"
|
|
104
|
+
"platformPollJobStatus",
|
|
111
105
|
).mockResolvedValue({
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
jobId: "platform-job-1",
|
|
107
|
+
type: "export",
|
|
108
|
+
status: "complete",
|
|
109
|
+
bundleKey: "platform-bundle-key-abc",
|
|
114
110
|
});
|
|
115
111
|
|
|
116
|
-
const
|
|
112
|
+
const platformRequestSignedUrlMock = spyOn(
|
|
117
113
|
platformClient,
|
|
118
|
-
"
|
|
119
|
-
).mockImplementation(async () => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
});
|
|
114
|
+
"platformRequestSignedUrl",
|
|
115
|
+
).mockImplementation(async (params) => ({
|
|
116
|
+
url:
|
|
117
|
+
params.operation === "upload"
|
|
118
|
+
? "https://storage.googleapis.com/bucket/signed-upload"
|
|
119
|
+
: "https://storage.googleapis.com/bucket/signed-download",
|
|
120
|
+
bundleKey: params.bundleKey ?? "bundle-key-123",
|
|
121
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
122
|
+
}));
|
|
139
123
|
|
|
140
|
-
const
|
|
124
|
+
const platformImportBundleFromGcsMock = spyOn(
|
|
141
125
|
platformClient,
|
|
142
|
-
"
|
|
126
|
+
"platformImportBundleFromGcs",
|
|
143
127
|
).mockResolvedValue({
|
|
144
128
|
statusCode: 200,
|
|
145
129
|
body: {
|
|
@@ -154,20 +138,6 @@ const platformImportBundleMock = spyOn(
|
|
|
154
138
|
} as Record<string, unknown>,
|
|
155
139
|
});
|
|
156
140
|
|
|
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
141
|
const platformImportPreflightFromGcsMock = spyOn(
|
|
172
142
|
platformClient,
|
|
173
143
|
"platformImportPreflightFromGcs",
|
|
@@ -184,23 +154,6 @@ const platformImportPreflightFromGcsMock = spyOn(
|
|
|
184
154
|
} as Record<string, unknown>,
|
|
185
155
|
});
|
|
186
156
|
|
|
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
157
|
const checkExistingPlatformAssistantMock = spyOn(
|
|
205
158
|
platformClient,
|
|
206
159
|
"checkExistingPlatformAssistant",
|
|
@@ -241,6 +194,35 @@ const fetchOrganizationIdMock = spyOn(
|
|
|
241
194
|
"fetchOrganizationId",
|
|
242
195
|
).mockResolvedValue("org-1");
|
|
243
196
|
|
|
197
|
+
const localRuntimeExportToGcsMock = spyOn(
|
|
198
|
+
localRuntimeClient,
|
|
199
|
+
"localRuntimeExportToGcs",
|
|
200
|
+
).mockResolvedValue({ jobId: "local-export-job-1" });
|
|
201
|
+
|
|
202
|
+
const localRuntimeImportFromGcsMock = spyOn(
|
|
203
|
+
localRuntimeClient,
|
|
204
|
+
"localRuntimeImportFromGcs",
|
|
205
|
+
).mockResolvedValue({ jobId: "local-import-job-1" });
|
|
206
|
+
|
|
207
|
+
const localRuntimePollJobStatusMock = spyOn(
|
|
208
|
+
localRuntimeClient,
|
|
209
|
+
"localRuntimePollJobStatus",
|
|
210
|
+
).mockImplementation(async (_runtimeUrl, _token, jobId) => ({
|
|
211
|
+
jobId,
|
|
212
|
+
type: jobId.includes("import") ? "import" : "export",
|
|
213
|
+
status: "complete",
|
|
214
|
+
result: {
|
|
215
|
+
success: true,
|
|
216
|
+
summary: {
|
|
217
|
+
total_files: 3,
|
|
218
|
+
files_created: 2,
|
|
219
|
+
files_overwritten: 1,
|
|
220
|
+
files_skipped: 0,
|
|
221
|
+
backups_created: 1,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
|
|
244
226
|
const hatchLocalMock = mock(async () => {});
|
|
245
227
|
|
|
246
228
|
mock.module("../lib/hatch-local.js", () => ({
|
|
@@ -313,19 +295,18 @@ afterAll(() => {
|
|
|
313
295
|
hatchAssistantMock.mockRestore();
|
|
314
296
|
checkExistingPlatformAssistantMock.mockRestore();
|
|
315
297
|
platformInitiateExportMock.mockRestore();
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
platformImportPreflightMock.mockRestore();
|
|
319
|
-
platformImportBundleMock.mockRestore();
|
|
320
|
-
platformRequestUploadUrlMock.mockRestore();
|
|
321
|
-
platformUploadToSignedUrlMock.mockRestore();
|
|
322
|
-
platformImportPreflightFromGcsMock.mockRestore();
|
|
298
|
+
platformPollJobStatusMock.mockRestore();
|
|
299
|
+
platformRequestSignedUrlMock.mockRestore();
|
|
323
300
|
platformImportBundleFromGcsMock.mockRestore();
|
|
301
|
+
platformImportPreflightFromGcsMock.mockRestore();
|
|
324
302
|
ensureSelfHostedLocalRegistrationMock.mockRestore();
|
|
325
303
|
injectCredentialsIntoAssistantMock.mockRestore();
|
|
326
304
|
fetchCurrentUserMock.mockRestore();
|
|
327
305
|
fetchOrganizationIdMock.mockRestore();
|
|
328
306
|
computeDeviceIdMock.mockRestore();
|
|
307
|
+
localRuntimeExportToGcsMock.mockRestore();
|
|
308
|
+
localRuntimeImportFromGcsMock.mockRestore();
|
|
309
|
+
localRuntimePollJobStatusMock.mockRestore();
|
|
329
310
|
rmSync(testDir, { recursive: true, force: true });
|
|
330
311
|
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
331
312
|
});
|
|
@@ -335,11 +316,39 @@ let exitMock: ReturnType<typeof mock>;
|
|
|
335
316
|
let originalExit: typeof process.exit;
|
|
336
317
|
let consoleLogSpy: ReturnType<typeof spyOn>;
|
|
337
318
|
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
|
319
|
+
let fetchCalls: Array<{ url: string; body: unknown }>;
|
|
320
|
+
|
|
321
|
+
function defaultLocalRuntimePollImpl(
|
|
322
|
+
_runtimeUrl: string,
|
|
323
|
+
_token: string,
|
|
324
|
+
jobId: string,
|
|
325
|
+
): Promise<{
|
|
326
|
+
jobId: string;
|
|
327
|
+
type: "export" | "import";
|
|
328
|
+
status: "complete";
|
|
329
|
+
result: Record<string, unknown>;
|
|
330
|
+
}> {
|
|
331
|
+
return Promise.resolve({
|
|
332
|
+
jobId,
|
|
333
|
+
type: jobId.includes("import") ? "import" : "export",
|
|
334
|
+
status: "complete",
|
|
335
|
+
result: {
|
|
336
|
+
success: true,
|
|
337
|
+
summary: {
|
|
338
|
+
total_files: 3,
|
|
339
|
+
files_created: 2,
|
|
340
|
+
files_overwritten: 1,
|
|
341
|
+
files_skipped: 0,
|
|
342
|
+
backups_created: 1,
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
338
347
|
|
|
339
348
|
beforeEach(() => {
|
|
340
349
|
originalArgv = [...process.argv];
|
|
350
|
+
fetchCalls = [];
|
|
341
351
|
|
|
342
|
-
// Reset all mocks
|
|
343
352
|
findAssistantByNameMock.mockReset();
|
|
344
353
|
findAssistantByNameMock.mockReturnValue(null);
|
|
345
354
|
saveAssistantEntryMock.mockReset();
|
|
@@ -371,33 +380,27 @@ beforeEach(() => {
|
|
|
371
380
|
});
|
|
372
381
|
platformInitiateExportMock.mockReset();
|
|
373
382
|
platformInitiateExportMock.mockResolvedValue({
|
|
374
|
-
jobId: "job-1",
|
|
383
|
+
jobId: "platform-export-job-1",
|
|
375
384
|
status: "pending",
|
|
376
385
|
});
|
|
377
|
-
|
|
378
|
-
|
|
386
|
+
platformPollJobStatusMock.mockReset();
|
|
387
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
388
|
+
jobId: "platform-job-1",
|
|
389
|
+
type: "export",
|
|
379
390
|
status: "complete",
|
|
380
|
-
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
files_to_overwrite: 1,
|
|
394
|
-
files_unchanged: 0,
|
|
395
|
-
total_files: 3,
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
platformImportBundleMock.mockReset();
|
|
400
|
-
platformImportBundleMock.mockResolvedValue({
|
|
391
|
+
bundleKey: "platform-bundle-key-abc",
|
|
392
|
+
});
|
|
393
|
+
platformRequestSignedUrlMock.mockReset();
|
|
394
|
+
platformRequestSignedUrlMock.mockImplementation(async (params) => ({
|
|
395
|
+
url:
|
|
396
|
+
params.operation === "upload"
|
|
397
|
+
? "https://storage.googleapis.com/bucket/signed-upload"
|
|
398
|
+
: "https://storage.googleapis.com/bucket/signed-download",
|
|
399
|
+
bundleKey: params.bundleKey ?? "bundle-key-123",
|
|
400
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
401
|
+
}));
|
|
402
|
+
platformImportBundleFromGcsMock.mockReset();
|
|
403
|
+
platformImportBundleFromGcsMock.mockResolvedValue({
|
|
401
404
|
statusCode: 200,
|
|
402
405
|
body: {
|
|
403
406
|
success: true,
|
|
@@ -410,14 +413,6 @@ beforeEach(() => {
|
|
|
410
413
|
},
|
|
411
414
|
},
|
|
412
415
|
});
|
|
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
416
|
platformImportPreflightFromGcsMock.mockReset();
|
|
422
417
|
platformImportPreflightFromGcsMock.mockResolvedValue({
|
|
423
418
|
statusCode: 200,
|
|
@@ -431,20 +426,6 @@ beforeEach(() => {
|
|
|
431
426
|
},
|
|
432
427
|
},
|
|
433
428
|
});
|
|
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
429
|
checkExistingPlatformAssistantMock.mockReset();
|
|
449
430
|
checkExistingPlatformAssistantMock.mockResolvedValue(null);
|
|
450
431
|
ensureSelfHostedLocalRegistrationMock.mockReset();
|
|
@@ -471,6 +452,17 @@ beforeEach(() => {
|
|
|
471
452
|
computeDeviceIdMock.mockReset();
|
|
472
453
|
computeDeviceIdMock.mockReturnValue("device-id-123");
|
|
473
454
|
|
|
455
|
+
localRuntimeExportToGcsMock.mockReset();
|
|
456
|
+
localRuntimeExportToGcsMock.mockResolvedValue({
|
|
457
|
+
jobId: "local-export-job-1",
|
|
458
|
+
});
|
|
459
|
+
localRuntimeImportFromGcsMock.mockReset();
|
|
460
|
+
localRuntimeImportFromGcsMock.mockResolvedValue({
|
|
461
|
+
jobId: "local-import-job-1",
|
|
462
|
+
});
|
|
463
|
+
localRuntimePollJobStatusMock.mockReset();
|
|
464
|
+
localRuntimePollJobStatusMock.mockImplementation(defaultLocalRuntimePollImpl);
|
|
465
|
+
|
|
474
466
|
hatchLocalMock.mockReset();
|
|
475
467
|
hatchLocalMock.mockResolvedValue(undefined);
|
|
476
468
|
hatchDockerMock.mockReset();
|
|
@@ -505,7 +497,6 @@ afterEach(() => {
|
|
|
505
497
|
});
|
|
506
498
|
|
|
507
499
|
function setArgv(...args: string[]): void {
|
|
508
|
-
// teleport reads process.argv.slice(3)
|
|
509
500
|
process.argv = ["bun", "vellum", "teleport", ...args];
|
|
510
501
|
}
|
|
511
502
|
|
|
@@ -521,44 +512,24 @@ function makeEntry(
|
|
|
521
512
|
};
|
|
522
513
|
}
|
|
523
514
|
|
|
524
|
-
/**
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
});
|
|
515
|
+
/**
|
|
516
|
+
* Tracking fetch mock — records every call so tests can verify that the CLI
|
|
517
|
+
* never sends a bundle-sized request body. With the new GCS-unified flow
|
|
518
|
+
* all bundle bytes travel between the runtime and GCS directly, so the CLI
|
|
519
|
+
* should make zero fetch calls carrying binary payloads.
|
|
520
|
+
*/
|
|
521
|
+
function installTrackingFetch(): () => void {
|
|
522
|
+
const originalFetch = globalThis.fetch;
|
|
523
|
+
globalThis.fetch = mock(
|
|
524
|
+
async (url: string | URL | Request, init?: RequestInit) => {
|
|
525
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
526
|
+
fetchCalls.push({ url: urlStr, body: init?.body });
|
|
527
|
+
return new Response("not found", { status: 404 });
|
|
528
|
+
},
|
|
529
|
+
) as unknown as typeof globalThis.fetch;
|
|
530
|
+
return () => {
|
|
531
|
+
globalThis.fetch = originalFetch;
|
|
532
|
+
};
|
|
562
533
|
}
|
|
563
534
|
|
|
564
535
|
// ---------------------------------------------------------------------------
|
|
@@ -663,20 +634,13 @@ describe("same-environment rejection", () => {
|
|
|
663
634
|
return null;
|
|
664
635
|
});
|
|
665
636
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
}
|
|
637
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
638
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
639
|
+
expect.stringContaining("Cannot teleport between two local assistants"),
|
|
640
|
+
);
|
|
677
641
|
});
|
|
678
642
|
|
|
679
|
-
test("source docker, target docker -> error
|
|
643
|
+
test("source docker, target docker -> error", async () => {
|
|
680
644
|
setArgv("--from", "src", "--docker", "dst");
|
|
681
645
|
|
|
682
646
|
const srcEntry = makeEntry("src", { cloud: "docker" });
|
|
@@ -688,22 +652,13 @@ describe("same-environment rejection", () => {
|
|
|
688
652
|
return null;
|
|
689
653
|
});
|
|
690
654
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
}
|
|
655
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
656
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
657
|
+
expect.stringContaining("Cannot teleport between two docker assistants"),
|
|
658
|
+
);
|
|
704
659
|
});
|
|
705
660
|
|
|
706
|
-
test("source vellum, target platform -> error
|
|
661
|
+
test("source vellum, target platform -> error", async () => {
|
|
707
662
|
setArgv("--from", "src", "--platform", "dst");
|
|
708
663
|
|
|
709
664
|
const srcEntry = makeEntry("src", {
|
|
@@ -721,19 +676,12 @@ describe("same-environment rejection", () => {
|
|
|
721
676
|
return null;
|
|
722
677
|
});
|
|
723
678
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
expect.stringContaining(
|
|
731
|
-
"Cannot teleport between two platform assistants",
|
|
732
|
-
),
|
|
733
|
-
);
|
|
734
|
-
} finally {
|
|
735
|
-
globalThis.fetch = originalFetch;
|
|
736
|
-
}
|
|
679
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
680
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
681
|
+
expect.stringContaining(
|
|
682
|
+
"Cannot teleport between two platform assistants",
|
|
683
|
+
),
|
|
684
|
+
);
|
|
737
685
|
});
|
|
738
686
|
|
|
739
687
|
test("same-env rejection happens before hatching (no orphaned assistants)", async () => {
|
|
@@ -749,59 +697,15 @@ describe("same-environment rejection", () => {
|
|
|
749
697
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
750
698
|
expect.stringContaining("Cannot teleport between two local assistants"),
|
|
751
699
|
);
|
|
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
700
|
expect(hatchLocalMock).not.toHaveBeenCalled();
|
|
795
701
|
expect(hatchDockerMock).not.toHaveBeenCalled();
|
|
796
702
|
expect(hatchAssistantMock).not.toHaveBeenCalled();
|
|
797
703
|
});
|
|
798
704
|
|
|
799
705
|
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
706
|
setArgv("--from", "src", "--docker", "misidentified");
|
|
802
707
|
|
|
803
708
|
const srcEntry = makeEntry("src", { cloud: "vellum" });
|
|
804
|
-
// Target is actually local despite the --docker flag
|
|
805
709
|
const dstEntry = makeEntry("misidentified", { cloud: "local" });
|
|
806
710
|
|
|
807
711
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
@@ -832,16 +736,11 @@ describe("resolveOrHatchTarget", () => {
|
|
|
832
736
|
const result = await resolveOrHatchTarget("docker", "my-docker");
|
|
833
737
|
expect(result).toBe(dockerEntry);
|
|
834
738
|
expect(hatchDockerMock).not.toHaveBeenCalled();
|
|
835
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
836
|
-
expect.stringContaining("Target: my-docker (docker)"),
|
|
837
|
-
);
|
|
838
739
|
});
|
|
839
740
|
|
|
840
741
|
test("name not found -> hatch docker", async () => {
|
|
841
742
|
const newEntry = makeEntry("new-one", { cloud: "docker" });
|
|
842
743
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
843
|
-
// First call: lookup by name -> not found
|
|
844
|
-
// Second call: after hatch -> found
|
|
845
744
|
if (name === "new-one" && hatchDockerMock.mock.calls.length > 0) {
|
|
846
745
|
return newEntry;
|
|
847
746
|
}
|
|
@@ -859,12 +758,10 @@ describe("resolveOrHatchTarget", () => {
|
|
|
859
758
|
expect(result).toBe(newEntry);
|
|
860
759
|
});
|
|
861
760
|
|
|
862
|
-
test("no name -> hatch local
|
|
761
|
+
test("no name -> hatch local, discovers via diff", async () => {
|
|
863
762
|
const existingEntry = makeEntry("existing-local", { cloud: "local" });
|
|
864
763
|
const newEntry = makeEntry("auto-generated", { cloud: "local" });
|
|
865
764
|
|
|
866
|
-
// Before hatch: only the existing entry
|
|
867
|
-
// After hatch: existing + new entry
|
|
868
765
|
loadAllAssistantsMock.mockImplementation(() => {
|
|
869
766
|
if (hatchLocalMock.mock.calls.length > 0) {
|
|
870
767
|
return [existingEntry, newEntry];
|
|
@@ -873,13 +770,7 @@ describe("resolveOrHatchTarget", () => {
|
|
|
873
770
|
});
|
|
874
771
|
|
|
875
772
|
const result = await resolveOrHatchTarget("local");
|
|
876
|
-
expect(hatchLocalMock).
|
|
877
|
-
"vellum",
|
|
878
|
-
null,
|
|
879
|
-
false,
|
|
880
|
-
false,
|
|
881
|
-
{},
|
|
882
|
-
);
|
|
773
|
+
expect(hatchLocalMock).toHaveBeenCalled();
|
|
883
774
|
expect(result).toBe(newEntry);
|
|
884
775
|
});
|
|
885
776
|
|
|
@@ -903,16 +794,10 @@ describe("resolveOrHatchTarget", () => {
|
|
|
903
794
|
|
|
904
795
|
const result = await resolveOrHatchTarget("platform", "nonexistent");
|
|
905
796
|
expect(hatchAssistantMock).toHaveBeenCalledWith("platform-token");
|
|
906
|
-
expect(saveAssistantEntryMock).toHaveBeenCalledWith(
|
|
907
|
-
expect.objectContaining({
|
|
908
|
-
assistantId: "platform-new-id",
|
|
909
|
-
cloud: "vellum",
|
|
910
|
-
}),
|
|
911
|
-
);
|
|
912
797
|
expect(result.assistantId).toBe("platform-new-id");
|
|
913
798
|
});
|
|
914
799
|
|
|
915
|
-
test("platform with no name -> blocks when hatch returns reusedExisting
|
|
800
|
+
test("platform with no name -> blocks when hatch returns reusedExisting", async () => {
|
|
916
801
|
findAssistantByNameMock.mockReturnValue(null);
|
|
917
802
|
hatchAssistantMock.mockResolvedValue({
|
|
918
803
|
assistant: {
|
|
@@ -923,25 +808,12 @@ describe("resolveOrHatchTarget", () => {
|
|
|
923
808
|
reusedExisting: true,
|
|
924
809
|
});
|
|
925
810
|
|
|
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
811
|
await expect(resolveOrHatchTarget("platform", undefined)).rejects.toThrow(
|
|
930
812
|
"process.exit:1",
|
|
931
813
|
);
|
|
932
814
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
933
815
|
expect.stringContaining("already have a platform assistant"),
|
|
934
816
|
);
|
|
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
817
|
});
|
|
946
818
|
|
|
947
819
|
test("existing assistant with wrong cloud -> rejects", async () => {
|
|
@@ -973,212 +845,139 @@ describe("resolveOrHatchTarget", () => {
|
|
|
973
845
|
});
|
|
974
846
|
|
|
975
847
|
// ---------------------------------------------------------------------------
|
|
976
|
-
//
|
|
848
|
+
// Unified GCS teleport flow — the four directions
|
|
977
849
|
// ---------------------------------------------------------------------------
|
|
978
850
|
|
|
979
|
-
describe("
|
|
980
|
-
test("local
|
|
981
|
-
setArgv("--from", "my-local", "--
|
|
851
|
+
describe("unified GCS flow — four directions", () => {
|
|
852
|
+
test("local → platform: requests upload URL, drives local runtime export, imports from GCS", async () => {
|
|
853
|
+
setArgv("--from", "my-local", "--platform");
|
|
982
854
|
|
|
983
|
-
const localEntry = makeEntry("my-local", {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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" });
|
|
855
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
856
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
857
|
+
name === "my-local" ? localEntry : null,
|
|
858
|
+
);
|
|
996
859
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
});
|
|
860
|
+
const restoreFetch = installTrackingFetch();
|
|
861
|
+
try {
|
|
862
|
+
await teleport();
|
|
1001
863
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
864
|
+
// Signed-URL request for upload — pinned to the platform target's URL
|
|
865
|
+
// so upload and download land on the same platform.
|
|
866
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
867
|
+
expect.objectContaining({ operation: "upload" }),
|
|
868
|
+
"platform-token",
|
|
869
|
+
"https://platform.vellum.ai",
|
|
870
|
+
);
|
|
1009
871
|
|
|
1010
|
-
|
|
1011
|
-
|
|
872
|
+
// Runtime export-to-gcs kicked off with the signed upload URL
|
|
873
|
+
expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
|
|
874
|
+
"http://localhost:7821",
|
|
875
|
+
"local-token",
|
|
876
|
+
expect.objectContaining({
|
|
877
|
+
uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
|
|
878
|
+
}),
|
|
879
|
+
);
|
|
1012
880
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
881
|
+
// Poll continued until complete
|
|
882
|
+
expect(localRuntimePollJobStatusMock).toHaveBeenCalled();
|
|
883
|
+
|
|
884
|
+
// Import via GCS with the bundleKey returned from signed-URL request
|
|
885
|
+
expect(platformImportBundleFromGcsMock).toHaveBeenCalledWith(
|
|
886
|
+
"bundle-key-123",
|
|
887
|
+
"platform-token",
|
|
888
|
+
expect.any(String),
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
// No download-URL request on the import side (platform target pulls
|
|
892
|
+
// directly from GCS).
|
|
893
|
+
const downloadOps = platformRequestSignedUrlMock.mock.calls.filter(
|
|
894
|
+
(call: unknown[]) =>
|
|
895
|
+
(call[0] as { operation: string }).operation === "download",
|
|
896
|
+
);
|
|
897
|
+
expect(downloadOps.length).toBe(0);
|
|
1020
898
|
} finally {
|
|
1021
|
-
|
|
899
|
+
restoreFetch();
|
|
1022
900
|
}
|
|
1023
901
|
});
|
|
1024
902
|
|
|
1025
|
-
test("
|
|
1026
|
-
setArgv("--from", "my-
|
|
903
|
+
test("platform → local: drives platform export, reads bundle_key, requests download URL for runtime import", async () => {
|
|
904
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1027
905
|
|
|
1028
|
-
const
|
|
1029
|
-
|
|
906
|
+
const platformEntry = makeEntry("my-platform", {
|
|
907
|
+
cloud: "vellum",
|
|
908
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
909
|
+
});
|
|
910
|
+
const localEntry = makeEntry("my-local", {
|
|
911
|
+
cloud: "local",
|
|
912
|
+
bearerToken: "local-bearer",
|
|
913
|
+
});
|
|
1030
914
|
|
|
1031
915
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1032
|
-
if (name === "my-
|
|
916
|
+
if (name === "my-platform") return platformEntry;
|
|
917
|
+
if (name === "my-local") return localEntry;
|
|
1033
918
|
return null;
|
|
1034
919
|
});
|
|
1035
920
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
921
|
+
// Platform poll returns export-complete with a bundle_key.
|
|
922
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
923
|
+
jobId: "platform-export-job-1",
|
|
924
|
+
type: "export",
|
|
925
|
+
status: "complete",
|
|
926
|
+
bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
|
|
1041
927
|
});
|
|
1042
928
|
|
|
1043
|
-
const
|
|
1044
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1045
|
-
|
|
929
|
+
const restoreFetch = installTrackingFetch();
|
|
1046
930
|
try {
|
|
1047
931
|
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
|
-
|
|
1076
|
-
const originalFetch = globalThis.fetch;
|
|
1077
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1078
932
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
expect(
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
933
|
+
// Platform side: initiated server export and polled the unified status.
|
|
934
|
+
expect(platformInitiateExportMock).toHaveBeenCalled();
|
|
935
|
+
expect(platformPollJobStatusMock).toHaveBeenCalled();
|
|
936
|
+
|
|
937
|
+
// For the local target we request a download URL keyed by the
|
|
938
|
+
// platform's bundle_key. The URL must target the SOURCE platform
|
|
939
|
+
// (where the bundle was written) — pinned so a lockfile change
|
|
940
|
+
// can't split upload and download across instances.
|
|
941
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
942
|
+
{
|
|
943
|
+
operation: "download",
|
|
944
|
+
bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
|
|
945
|
+
},
|
|
946
|
+
"platform-token",
|
|
947
|
+
"https://platform.vellum.ai",
|
|
1086
948
|
);
|
|
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
949
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1148
|
-
const originalFetch = globalThis.fetch;
|
|
1149
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1150
|
-
|
|
1151
|
-
try {
|
|
1152
|
-
await teleport();
|
|
950
|
+
// Runtime import-from-gcs was kicked off with that URL.
|
|
951
|
+
expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
|
|
952
|
+
"http://localhost:7821",
|
|
953
|
+
"local-token",
|
|
954
|
+
expect.objectContaining({
|
|
955
|
+
bundleUrl: "https://storage.googleapis.com/bucket/signed-download",
|
|
956
|
+
}),
|
|
957
|
+
);
|
|
958
|
+
expect(localRuntimePollJobStatusMock).toHaveBeenCalled();
|
|
1153
959
|
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
expect(retireLocalMock).not.toHaveBeenCalled();
|
|
1158
|
-
expect(retireDockerMock).not.toHaveBeenCalled();
|
|
1159
|
-
expect(removeAssistantEntryMock).not.toHaveBeenCalled();
|
|
960
|
+
// No legacy inline-import helpers were touched.
|
|
961
|
+
// (Verified by the absence of fetch calls carrying bundle bodies —
|
|
962
|
+
// see "never buffers bundle bytes" assertion below.)
|
|
1160
963
|
} finally {
|
|
1161
|
-
|
|
964
|
+
restoreFetch();
|
|
1162
965
|
}
|
|
1163
966
|
});
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
// ---------------------------------------------------------------------------
|
|
1167
|
-
// Full flow tests
|
|
1168
|
-
// ---------------------------------------------------------------------------
|
|
1169
967
|
|
|
1170
|
-
|
|
1171
|
-
test("hatch and import: --from my-local --docker", async () => {
|
|
968
|
+
test("local → docker: export via upload URL, import via download URL", async () => {
|
|
1172
969
|
setArgv("--from", "my-local", "--docker");
|
|
1173
970
|
|
|
1174
971
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1175
|
-
const dockerEntry = makeEntry("new-docker", {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
if (name === "my-local") return localEntry;
|
|
1179
|
-
return null;
|
|
972
|
+
const dockerEntry = makeEntry("new-docker", {
|
|
973
|
+
cloud: "docker",
|
|
974
|
+
runtimeUrl: "http://localhost:7822",
|
|
1180
975
|
});
|
|
1181
976
|
|
|
977
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
978
|
+
name === "my-local" ? localEntry : null,
|
|
979
|
+
);
|
|
980
|
+
|
|
1182
981
|
loadAllAssistantsMock.mockImplementation(() => {
|
|
1183
982
|
if (hatchDockerMock.mock.calls.length > 0) {
|
|
1184
983
|
return [localEntry, dockerEntry];
|
|
@@ -1186,605 +985,338 @@ describe("teleport full flow", () => {
|
|
|
1186
985
|
return [localEntry];
|
|
1187
986
|
});
|
|
1188
987
|
|
|
1189
|
-
const
|
|
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
|
-
|
|
1224
|
-
try {
|
|
1225
|
-
await teleport();
|
|
1226
|
-
|
|
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
|
-
|
|
988
|
+
const restoreFetch = installTrackingFetch();
|
|
1267
989
|
try {
|
|
1268
990
|
await teleport();
|
|
1269
991
|
|
|
1270
|
-
//
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
expect(
|
|
1274
|
-
"
|
|
992
|
+
// Export and import must pin the same platform URL so the bundle
|
|
993
|
+
// lives in one place end-to-end. For local→docker neither side is
|
|
994
|
+
// platform, so we default to getPlatformUrl() (resolved once).
|
|
995
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
996
|
+
expect.objectContaining({ operation: "upload" }),
|
|
1275
997
|
"platform-token",
|
|
1276
998
|
"https://platform.vellum.ai",
|
|
1277
999
|
);
|
|
1278
|
-
|
|
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
|
-
);
|
|
1296
|
-
|
|
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;
|
|
1000
|
+
expect(localRuntimeExportToGcsMock).toHaveBeenCalled();
|
|
1311
1001
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
expect(platformImportPreflightFromGcsMock).toHaveBeenCalledWith(
|
|
1319
|
-
"bundle-key-123",
|
|
1002
|
+
// Import: download-URL for the docker target, then runtime import.
|
|
1003
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1004
|
+
{
|
|
1005
|
+
operation: "download",
|
|
1006
|
+
bundleKey: "bundle-key-123",
|
|
1007
|
+
},
|
|
1320
1008
|
"platform-token",
|
|
1321
1009
|
"https://platform.vellum.ai",
|
|
1322
1010
|
);
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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"),
|
|
1011
|
+
expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
|
|
1012
|
+
"http://localhost:7822",
|
|
1013
|
+
"local-token",
|
|
1014
|
+
expect.objectContaining({
|
|
1015
|
+
bundleUrl: "https://storage.googleapis.com/bucket/signed-download",
|
|
1016
|
+
}),
|
|
1358
1017
|
);
|
|
1018
|
+
|
|
1019
|
+
// Source retirement still happens on success for local↔docker.
|
|
1020
|
+
expect(retireLocalMock).toHaveBeenCalledWith("my-local", localEntry);
|
|
1021
|
+
expect(removeAssistantEntryMock).toHaveBeenCalledWith("my-local");
|
|
1359
1022
|
} finally {
|
|
1360
|
-
|
|
1023
|
+
restoreFetch();
|
|
1361
1024
|
}
|
|
1362
1025
|
});
|
|
1363
1026
|
|
|
1364
|
-
test("
|
|
1365
|
-
setArgv("--from", "my-
|
|
1366
|
-
|
|
1367
|
-
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1027
|
+
test("docker → local: export via upload URL, import via download URL", async () => {
|
|
1028
|
+
setArgv("--from", "my-docker", "--local");
|
|
1368
1029
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1030
|
+
const dockerEntry = makeEntry("my-docker", {
|
|
1031
|
+
cloud: "docker",
|
|
1032
|
+
runtimeUrl: "http://localhost:7822",
|
|
1033
|
+
});
|
|
1034
|
+
const localEntry = makeEntry("new-local", {
|
|
1035
|
+
cloud: "local",
|
|
1036
|
+
runtimeUrl: "http://localhost:7823",
|
|
1372
1037
|
});
|
|
1373
1038
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
new Error("Signed uploads are not available on this platform instance"),
|
|
1039
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1040
|
+
name === "my-docker" ? dockerEntry : null,
|
|
1377
1041
|
);
|
|
1378
1042
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1043
|
+
loadAllAssistantsMock.mockImplementation(() => {
|
|
1044
|
+
if (hatchLocalMock.mock.calls.length > 0) {
|
|
1045
|
+
return [dockerEntry, localEntry];
|
|
1046
|
+
}
|
|
1047
|
+
return [dockerEntry];
|
|
1048
|
+
});
|
|
1381
1049
|
|
|
1050
|
+
const restoreFetch = installTrackingFetch();
|
|
1382
1051
|
try {
|
|
1383
1052
|
await teleport();
|
|
1384
1053
|
|
|
1385
|
-
//
|
|
1386
|
-
expect(
|
|
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;
|
|
1054
|
+
// Docker source should be put to sleep first.
|
|
1055
|
+
expect(sleepContainersMock).toHaveBeenCalled();
|
|
1415
1056
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1057
|
+
// Export leg: upload-URL (pinned to the same platform as import),
|
|
1058
|
+
// then runtime export.
|
|
1059
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1060
|
+
expect.objectContaining({ operation: "upload" }),
|
|
1061
|
+
"platform-token",
|
|
1062
|
+
"https://platform.vellum.ai",
|
|
1063
|
+
);
|
|
1064
|
+
expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
|
|
1065
|
+
"http://localhost:7822",
|
|
1066
|
+
"local-token",
|
|
1067
|
+
expect.objectContaining({
|
|
1068
|
+
uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
|
|
1069
|
+
}),
|
|
1419
1070
|
);
|
|
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
1071
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1072
|
+
// Import leg: download-URL targets the new local runtime
|
|
1073
|
+
expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
|
|
1074
|
+
"http://localhost:7823",
|
|
1075
|
+
"local-token",
|
|
1076
|
+
expect.anything(),
|
|
1077
|
+
);
|
|
1445
1078
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1079
|
+
// Source retirement
|
|
1080
|
+
expect(retireDockerMock).toHaveBeenCalledWith("my-docker");
|
|
1081
|
+
expect(removeAssistantEntryMock).toHaveBeenCalledWith("my-docker");
|
|
1448
1082
|
} finally {
|
|
1449
|
-
|
|
1083
|
+
restoreFetch();
|
|
1450
1084
|
}
|
|
1451
1085
|
});
|
|
1452
1086
|
});
|
|
1453
1087
|
|
|
1454
1088
|
// ---------------------------------------------------------------------------
|
|
1455
|
-
//
|
|
1089
|
+
// Target-platform URL threading: signed URL must be requested from the same
|
|
1090
|
+
// platform instance the import will run against. Codex P2 regression guard.
|
|
1456
1091
|
// ---------------------------------------------------------------------------
|
|
1457
1092
|
|
|
1458
|
-
describe("
|
|
1459
|
-
test("
|
|
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");
|
|
1093
|
+
describe("signed-URL request targets the bundle-owning platform", () => {
|
|
1094
|
+
test("local → existing platform target with non-default runtimeUrl: upload URL pinned to target's runtimeUrl", async () => {
|
|
1095
|
+
setArgv("--from", "my-local", "--platform", "existing-platform");
|
|
1542
1096
|
|
|
1543
1097
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1098
|
+
// Crucially, the target's runtimeUrl is NOT the default getPlatformUrl()
|
|
1099
|
+
// return value — this is the regression case Codex flagged.
|
|
1100
|
+
const platformEntry = makeEntry("existing-platform", {
|
|
1101
|
+
cloud: "vellum",
|
|
1102
|
+
runtimeUrl: "https://staging-platform.vellum.ai",
|
|
1103
|
+
});
|
|
1544
1104
|
|
|
1545
1105
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1546
1106
|
if (name === "my-local") return localEntry;
|
|
1107
|
+
if (name === "existing-platform") return platformEntry;
|
|
1547
1108
|
return null;
|
|
1548
1109
|
});
|
|
1549
1110
|
|
|
1550
|
-
|
|
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
|
-
|
|
1111
|
+
const restoreFetch = installTrackingFetch();
|
|
1558
1112
|
try {
|
|
1559
1113
|
await teleport();
|
|
1560
1114
|
|
|
1561
|
-
//
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
//
|
|
1570
|
-
expect(
|
|
1571
|
-
|
|
1572
|
-
|
|
1115
|
+
// The signed-URL request for upload MUST target the existing
|
|
1116
|
+
// platform assistant's runtimeUrl, not the default platform URL.
|
|
1117
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1118
|
+
expect.objectContaining({ operation: "upload" }),
|
|
1119
|
+
"platform-token",
|
|
1120
|
+
"https://staging-platform.vellum.ai",
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
// And the import must run against the same platform.
|
|
1124
|
+
expect(platformImportBundleFromGcsMock).toHaveBeenCalledWith(
|
|
1125
|
+
"bundle-key-123",
|
|
1126
|
+
"platform-token",
|
|
1127
|
+
"https://staging-platform.vellum.ai",
|
|
1573
1128
|
);
|
|
1129
|
+
|
|
1130
|
+
// Assert none of the signed-URL calls used the default URL — if any
|
|
1131
|
+
// did, upload and download would hit different platforms.
|
|
1132
|
+
for (const call of platformRequestSignedUrlMock.mock.calls) {
|
|
1133
|
+
expect(call[2]).toBe("https://staging-platform.vellum.ai");
|
|
1134
|
+
}
|
|
1574
1135
|
} finally {
|
|
1575
|
-
|
|
1136
|
+
restoreFetch();
|
|
1576
1137
|
}
|
|
1577
1138
|
});
|
|
1578
1139
|
|
|
1579
|
-
test("
|
|
1580
|
-
setArgv("--from", "my-
|
|
1140
|
+
test("platform → local with non-default source runtimeUrl: download URL pinned to source's runtimeUrl", async () => {
|
|
1141
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1581
1142
|
|
|
1143
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1144
|
+
cloud: "vellum",
|
|
1145
|
+
runtimeUrl: "https://dev-platform.vellum.ai",
|
|
1146
|
+
});
|
|
1582
1147
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1583
1148
|
|
|
1584
1149
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1150
|
+
if (name === "my-platform") return platformEntry;
|
|
1585
1151
|
if (name === "my-local") return localEntry;
|
|
1586
1152
|
return null;
|
|
1587
1153
|
});
|
|
1588
1154
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1155
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1156
|
+
jobId: "platform-export-job-1",
|
|
1157
|
+
type: "export",
|
|
1158
|
+
status: "complete",
|
|
1159
|
+
bundleKey: "dev-bundle-key",
|
|
1594
1160
|
});
|
|
1595
1161
|
|
|
1596
|
-
const
|
|
1597
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1598
|
-
|
|
1162
|
+
const restoreFetch = installTrackingFetch();
|
|
1599
1163
|
try {
|
|
1600
1164
|
await teleport();
|
|
1601
1165
|
|
|
1602
|
-
// The
|
|
1603
|
-
|
|
1604
|
-
|
|
1166
|
+
// The download URL must be requested from the SOURCE platform (where
|
|
1167
|
+
// the bundle was written by the server-side export), not the default.
|
|
1168
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1169
|
+
{ operation: "download", bundleKey: "dev-bundle-key" },
|
|
1605
1170
|
"platform-token",
|
|
1606
|
-
|
|
1171
|
+
"https://dev-platform.vellum.ai",
|
|
1607
1172
|
);
|
|
1608
|
-
// Inline import should NOT be used since signed upload succeeded
|
|
1609
|
-
expect(platformImportBundleMock).not.toHaveBeenCalled();
|
|
1610
1173
|
} finally {
|
|
1611
|
-
|
|
1174
|
+
restoreFetch();
|
|
1612
1175
|
}
|
|
1613
1176
|
});
|
|
1614
1177
|
});
|
|
1615
1178
|
|
|
1616
1179
|
// ---------------------------------------------------------------------------
|
|
1617
|
-
//
|
|
1180
|
+
// Invariants: CLI never buffers bundle bytes
|
|
1618
1181
|
// ---------------------------------------------------------------------------
|
|
1619
1182
|
|
|
1620
|
-
describe("
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1183
|
+
describe("CLI never buffers bundle bytes", () => {
|
|
1184
|
+
// A teleport bundle is always > 1 KiB in practice; anything near that size
|
|
1185
|
+
// would mean the CLI is shuttling bytes when it shouldn't.
|
|
1186
|
+
const BUNDLE_BODY_THRESHOLD_BYTES = 1024;
|
|
1625
1187
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1188
|
+
function bodySize(body: unknown): number {
|
|
1189
|
+
if (typeof body === "string") return body.length;
|
|
1190
|
+
if (body instanceof Uint8Array) return body.byteLength;
|
|
1191
|
+
if (body instanceof ArrayBuffer) return body.byteLength;
|
|
1192
|
+
if (body instanceof Blob) return body.size;
|
|
1193
|
+
return 0;
|
|
1194
|
+
}
|
|
1630
1195
|
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
id: "existing-platform-id",
|
|
1634
|
-
name: "existing-platform",
|
|
1635
|
-
status: "active",
|
|
1636
|
-
});
|
|
1196
|
+
test("local → platform: no fetch call carries a bundle-sized body", async () => {
|
|
1197
|
+
setArgv("--from", "my-local", "--platform");
|
|
1637
1198
|
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1199
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1200
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1201
|
+
name === "my-local" ? localEntry : null,
|
|
1202
|
+
);
|
|
1640
1203
|
|
|
1204
|
+
const restoreFetch = installTrackingFetch();
|
|
1641
1205
|
try {
|
|
1642
|
-
await
|
|
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
|
-
);
|
|
1206
|
+
await teleport();
|
|
1664
1207
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
assistantId: "existing-platform-id",
|
|
1669
|
-
cloud: "vellum",
|
|
1670
|
-
}),
|
|
1671
|
-
);
|
|
1208
|
+
for (const call of fetchCalls) {
|
|
1209
|
+
expect(bodySize(call.body)).toBeLessThan(BUNDLE_BODY_THRESHOLD_BYTES);
|
|
1210
|
+
}
|
|
1672
1211
|
} finally {
|
|
1673
|
-
|
|
1212
|
+
restoreFetch();
|
|
1674
1213
|
}
|
|
1675
1214
|
});
|
|
1676
1215
|
|
|
1677
|
-
test("
|
|
1678
|
-
setArgv("--from", "my-
|
|
1216
|
+
test("platform → local: no fetch call carries a bundle-sized body", async () => {
|
|
1217
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1679
1218
|
|
|
1680
|
-
const
|
|
1681
|
-
const platformEntry = makeEntry("existing-platform", {
|
|
1219
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1682
1220
|
cloud: "vellum",
|
|
1683
1221
|
runtimeUrl: "https://platform.vellum.ai",
|
|
1684
1222
|
});
|
|
1223
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1685
1224
|
|
|
1686
1225
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1226
|
+
if (name === "my-platform") return platformEntry;
|
|
1687
1227
|
if (name === "my-local") return localEntry;
|
|
1688
|
-
if (name === "existing-platform") return platformEntry;
|
|
1689
1228
|
return null;
|
|
1690
1229
|
});
|
|
1691
1230
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1231
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1232
|
+
jobId: "platform-export-job-1",
|
|
1233
|
+
type: "export",
|
|
1234
|
+
status: "complete",
|
|
1235
|
+
bundleKey: "platform-exports/org-1/bundle.vbundle",
|
|
1236
|
+
});
|
|
1694
1237
|
|
|
1238
|
+
const restoreFetch = installTrackingFetch();
|
|
1695
1239
|
try {
|
|
1696
1240
|
await teleport();
|
|
1697
1241
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
// Upload should proceed normally
|
|
1702
|
-
expect(platformRequestUploadUrlMock).toHaveBeenCalled();
|
|
1703
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
1704
|
-
expect.stringContaining("Teleport complete"),
|
|
1705
|
-
);
|
|
1242
|
+
for (const call of fetchCalls) {
|
|
1243
|
+
expect(bodySize(call.body)).toBeLessThan(BUNDLE_BODY_THRESHOLD_BYTES);
|
|
1244
|
+
}
|
|
1706
1245
|
} finally {
|
|
1707
|
-
|
|
1246
|
+
restoreFetch();
|
|
1708
1247
|
}
|
|
1709
1248
|
});
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// ---------------------------------------------------------------------------
|
|
1252
|
+
// Polling behavior
|
|
1253
|
+
// ---------------------------------------------------------------------------
|
|
1710
1254
|
|
|
1711
|
-
|
|
1255
|
+
describe("polling", () => {
|
|
1256
|
+
test("local-runtime export poll continues until complete", async () => {
|
|
1712
1257
|
setArgv("--from", "my-local", "--platform");
|
|
1713
1258
|
|
|
1714
1259
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1260
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1261
|
+
name === "my-local" ? localEntry : null,
|
|
1262
|
+
);
|
|
1715
1263
|
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1264
|
+
let callIdx = 0;
|
|
1265
|
+
localRuntimePollJobStatusMock.mockImplementation(
|
|
1266
|
+
async (_runtimeUrl, _token, jobId) => {
|
|
1267
|
+
callIdx++;
|
|
1268
|
+
if (callIdx < 3) {
|
|
1269
|
+
return {
|
|
1270
|
+
jobId,
|
|
1271
|
+
type: "export" as const,
|
|
1272
|
+
status: "processing" as const,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
jobId,
|
|
1277
|
+
type: "export" as const,
|
|
1278
|
+
status: "complete" as const,
|
|
1279
|
+
result: undefined,
|
|
1280
|
+
};
|
|
1281
|
+
},
|
|
1282
|
+
);
|
|
1726
1283
|
|
|
1284
|
+
const restoreFetch = installTrackingFetch();
|
|
1727
1285
|
try {
|
|
1728
1286
|
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
|
-
);
|
|
1287
|
+
expect(callIdx).toBeGreaterThanOrEqual(3);
|
|
1739
1288
|
} finally {
|
|
1740
|
-
|
|
1289
|
+
restoreFetch();
|
|
1741
1290
|
}
|
|
1742
1291
|
});
|
|
1743
|
-
});
|
|
1744
|
-
|
|
1745
|
-
// ---------------------------------------------------------------------------
|
|
1746
|
-
// Version guard: block platform→non-platform when target is behind
|
|
1747
|
-
// ---------------------------------------------------------------------------
|
|
1748
1292
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1293
|
+
test("local-runtime export failure → teleport exits 1", async () => {
|
|
1294
|
+
setArgv("--from", "my-local", "--platform");
|
|
1752
1295
|
|
|
1753
|
-
const platformEntry = makeEntry("my-platform", {
|
|
1754
|
-
cloud: "vellum",
|
|
1755
|
-
runtimeUrl: "https://platform.vellum.ai",
|
|
1756
|
-
});
|
|
1757
1296
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1297
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1298
|
+
name === "my-local" ? localEntry : null,
|
|
1299
|
+
);
|
|
1758
1300
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
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");
|
|
1301
|
+
localRuntimePollJobStatusMock.mockResolvedValue({
|
|
1302
|
+
jobId: "local-export-job-1",
|
|
1303
|
+
type: "export",
|
|
1304
|
+
status: "failed",
|
|
1305
|
+
error: "simulated failure",
|
|
1769
1306
|
});
|
|
1770
1307
|
|
|
1771
|
-
const
|
|
1772
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1773
|
-
|
|
1308
|
+
const restoreFetch = installTrackingFetch();
|
|
1774
1309
|
try {
|
|
1775
1310
|
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1776
1311
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1777
|
-
expect.stringContaining("
|
|
1778
|
-
);
|
|
1779
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1780
|
-
expect.stringContaining("Upgrade your local assistant first"),
|
|
1312
|
+
expect.stringContaining("simulated failure"),
|
|
1781
1313
|
);
|
|
1782
1314
|
} finally {
|
|
1783
|
-
|
|
1315
|
+
restoreFetch();
|
|
1784
1316
|
}
|
|
1785
1317
|
});
|
|
1786
1318
|
|
|
1787
|
-
test("
|
|
1319
|
+
test("local-runtime import failure → teleport exits 1", async () => {
|
|
1788
1320
|
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1789
1321
|
|
|
1790
1322
|
const platformEntry = makeEntry("my-platform", {
|
|
@@ -1799,57 +1331,89 @@ describe("version guard: block platform→non-platform when target is behind", (
|
|
|
1799
1331
|
return null;
|
|
1800
1332
|
});
|
|
1801
1333
|
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1334
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1335
|
+
jobId: "platform-export-job-1",
|
|
1336
|
+
type: "export",
|
|
1337
|
+
status: "complete",
|
|
1338
|
+
bundleKey: "key-1",
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
localRuntimePollJobStatusMock.mockImplementation(
|
|
1342
|
+
async (_runtimeUrl, _token, jobId) => {
|
|
1343
|
+
if (jobId.includes("import")) {
|
|
1344
|
+
return {
|
|
1345
|
+
jobId,
|
|
1346
|
+
type: "import" as const,
|
|
1347
|
+
status: "failed" as const,
|
|
1348
|
+
error: "import blew up",
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
jobId,
|
|
1353
|
+
type: "export" as const,
|
|
1354
|
+
status: "complete" as const,
|
|
1355
|
+
result: undefined,
|
|
1356
|
+
};
|
|
1357
|
+
},
|
|
1358
|
+
);
|
|
1807
1359
|
|
|
1360
|
+
const restoreFetch = installTrackingFetch();
|
|
1808
1361
|
try {
|
|
1809
|
-
await teleport();
|
|
1810
|
-
expect(
|
|
1811
|
-
expect.stringContaining("
|
|
1362
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1363
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1364
|
+
expect.stringContaining("import blew up"),
|
|
1812
1365
|
);
|
|
1813
1366
|
} finally {
|
|
1814
|
-
|
|
1367
|
+
restoreFetch();
|
|
1815
1368
|
}
|
|
1816
1369
|
});
|
|
1370
|
+
});
|
|
1817
1371
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1372
|
+
// ---------------------------------------------------------------------------
|
|
1373
|
+
// MigrationInProgressError handling
|
|
1374
|
+
// ---------------------------------------------------------------------------
|
|
1375
|
+
|
|
1376
|
+
describe("MigrationInProgressError handling", () => {
|
|
1377
|
+
test("local-runtime export already in flight → fail fast with existing job id", async () => {
|
|
1378
|
+
setArgv("--from", "my-local", "--platform");
|
|
1820
1379
|
|
|
1821
|
-
const platformEntry = makeEntry("my-platform", {
|
|
1822
|
-
cloud: "vellum",
|
|
1823
|
-
runtimeUrl: "https://platform.vellum.ai",
|
|
1824
|
-
});
|
|
1825
1380
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1381
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1382
|
+
name === "my-local" ? localEntry : null,
|
|
1383
|
+
);
|
|
1826
1384
|
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1385
|
+
localRuntimeExportToGcsMock.mockRejectedValue(
|
|
1386
|
+
new localRuntimeClient.MigrationInProgressError(
|
|
1387
|
+
"export_in_progress",
|
|
1388
|
+
"existing-job-42",
|
|
1389
|
+
),
|
|
1390
|
+
);
|
|
1832
1391
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
return Promise.resolve("0.8.0");
|
|
1837
|
-
});
|
|
1392
|
+
const restoreFetch = installTrackingFetch();
|
|
1393
|
+
try {
|
|
1394
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1838
1395
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1396
|
+
// Must not have polled the existing job — the existing job's bundle
|
|
1397
|
+
// lives at a different GCS key (its caller's signed URL), so polling
|
|
1398
|
+
// it would leave the teleport pointing at an empty/unrelated bundle.
|
|
1399
|
+
const polledIds = localRuntimePollJobStatusMock.mock.calls.map(
|
|
1400
|
+
(call: unknown[]) => call[2],
|
|
1401
|
+
);
|
|
1402
|
+
expect(polledIds).not.toContain("existing-job-42");
|
|
1841
1403
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1404
|
+
// Error must mention the existing job id so the user can act on it.
|
|
1405
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1406
|
+
expect.stringContaining("existing-job-42"),
|
|
1407
|
+
);
|
|
1408
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1409
|
+
expect.stringContaining("already in progress"),
|
|
1846
1410
|
);
|
|
1847
1411
|
} finally {
|
|
1848
|
-
|
|
1412
|
+
restoreFetch();
|
|
1849
1413
|
}
|
|
1850
1414
|
});
|
|
1851
1415
|
|
|
1852
|
-
test("
|
|
1416
|
+
test("local-runtime import already in flight → fail fast with existing job id", async () => {
|
|
1853
1417
|
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1854
1418
|
|
|
1855
1419
|
const platformEntry = makeEntry("my-platform", {
|
|
@@ -1864,62 +1428,104 @@ describe("version guard: block platform→non-platform when target is behind", (
|
|
|
1864
1428
|
return null;
|
|
1865
1429
|
});
|
|
1866
1430
|
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1431
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1432
|
+
jobId: "platform-export-job-1",
|
|
1433
|
+
type: "export",
|
|
1434
|
+
status: "complete",
|
|
1435
|
+
bundleKey: "bundle-key-from-platform",
|
|
1872
1436
|
});
|
|
1873
1437
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1438
|
+
localRuntimeImportFromGcsMock.mockRejectedValue(
|
|
1439
|
+
new localRuntimeClient.MigrationInProgressError(
|
|
1440
|
+
"import_in_progress",
|
|
1441
|
+
"existing-import-99",
|
|
1442
|
+
),
|
|
1443
|
+
);
|
|
1876
1444
|
|
|
1445
|
+
const restoreFetch = installTrackingFetch();
|
|
1877
1446
|
try {
|
|
1878
|
-
await teleport();
|
|
1879
|
-
|
|
1880
|
-
|
|
1447
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1448
|
+
|
|
1449
|
+
// Must not poll the existing import — it's importing somebody else's
|
|
1450
|
+
// bundle, not ours, so reporting on it would be misleading.
|
|
1451
|
+
const polledIds = localRuntimePollJobStatusMock.mock.calls.map(
|
|
1452
|
+
(call: unknown[]) => call[2],
|
|
1453
|
+
);
|
|
1454
|
+
expect(polledIds).not.toContain("existing-import-99");
|
|
1455
|
+
|
|
1456
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1457
|
+
expect.stringContaining("existing-import-99"),
|
|
1458
|
+
);
|
|
1459
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1460
|
+
expect.stringContaining("already in progress"),
|
|
1881
1461
|
);
|
|
1882
1462
|
} finally {
|
|
1883
|
-
|
|
1463
|
+
restoreFetch();
|
|
1884
1464
|
}
|
|
1885
1465
|
});
|
|
1466
|
+
});
|
|
1886
1467
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1468
|
+
// ---------------------------------------------------------------------------
|
|
1469
|
+
// Dry-run behavior
|
|
1470
|
+
// ---------------------------------------------------------------------------
|
|
1889
1471
|
|
|
1890
|
-
|
|
1472
|
+
describe("dry-run", () => {
|
|
1473
|
+
test("dry-run without existing target does not hatch or export", async () => {
|
|
1474
|
+
setArgv("--from", "my-local", "--docker", "--dry-run");
|
|
1475
|
+
|
|
1476
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1477
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1478
|
+
name === "my-local" ? localEntry : null,
|
|
1479
|
+
);
|
|
1480
|
+
|
|
1481
|
+
await teleport();
|
|
1482
|
+
|
|
1483
|
+
expect(hatchDockerMock).not.toHaveBeenCalled();
|
|
1484
|
+
expect(hatchLocalMock).not.toHaveBeenCalled();
|
|
1485
|
+
expect(hatchAssistantMock).not.toHaveBeenCalled();
|
|
1486
|
+
expect(retireLocalMock).not.toHaveBeenCalled();
|
|
1487
|
+
expect(retireDockerMock).not.toHaveBeenCalled();
|
|
1488
|
+
expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
|
|
1489
|
+
expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
test("dry-run with existing platform target runs preflight-from-gcs", async () => {
|
|
1493
|
+
setArgv(
|
|
1494
|
+
"--from",
|
|
1495
|
+
"my-local",
|
|
1496
|
+
"--platform",
|
|
1497
|
+
"existing-platform",
|
|
1498
|
+
"--dry-run",
|
|
1499
|
+
);
|
|
1500
|
+
|
|
1501
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1502
|
+
const platformEntry = makeEntry("existing-platform", {
|
|
1891
1503
|
cloud: "vellum",
|
|
1892
1504
|
runtimeUrl: "https://platform.vellum.ai",
|
|
1893
1505
|
});
|
|
1894
|
-
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1895
1506
|
|
|
1896
1507
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1897
|
-
if (name === "my-platform") return platformEntry;
|
|
1898
1508
|
if (name === "my-local") return localEntry;
|
|
1509
|
+
if (name === "existing-platform") return platformEntry;
|
|
1899
1510
|
return null;
|
|
1900
1511
|
});
|
|
1901
1512
|
|
|
1902
|
-
|
|
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
|
-
|
|
1513
|
+
const restoreFetch = installTrackingFetch();
|
|
1911
1514
|
try {
|
|
1912
1515
|
await teleport();
|
|
1913
|
-
expect(
|
|
1914
|
-
|
|
1516
|
+
expect(platformImportPreflightFromGcsMock).toHaveBeenCalledWith(
|
|
1517
|
+
"bundle-key-123",
|
|
1518
|
+
"platform-token",
|
|
1519
|
+
"https://platform.vellum.ai",
|
|
1915
1520
|
);
|
|
1521
|
+
expect(hatchAssistantMock).not.toHaveBeenCalled();
|
|
1916
1522
|
} finally {
|
|
1917
|
-
|
|
1523
|
+
restoreFetch();
|
|
1918
1524
|
}
|
|
1919
1525
|
});
|
|
1920
1526
|
|
|
1921
|
-
test("
|
|
1922
|
-
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1527
|
+
test("dry-run against local target fails fast (no preflight-from-gcs runtime endpoint yet)", async () => {
|
|
1528
|
+
setArgv("--from", "my-platform", "--local", "my-local", "--dry-run");
|
|
1923
1529
|
|
|
1924
1530
|
const platformEntry = makeEntry("my-platform", {
|
|
1925
1531
|
cloud: "vellum",
|
|
@@ -1933,64 +1539,107 @@ describe("version guard: block platform→non-platform when target is behind", (
|
|
|
1933
1539
|
return null;
|
|
1934
1540
|
});
|
|
1935
1541
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1542
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1543
|
+
jobId: "platform-export-job-1",
|
|
1544
|
+
type: "export",
|
|
1545
|
+
status: "complete",
|
|
1546
|
+
bundleKey: "bundle-key-from-platform",
|
|
1940
1547
|
});
|
|
1941
1548
|
|
|
1942
|
-
const
|
|
1943
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1944
|
-
|
|
1549
|
+
const restoreFetch = installTrackingFetch();
|
|
1945
1550
|
try {
|
|
1946
1551
|
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1947
1552
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1948
|
-
expect.stringContaining(
|
|
1553
|
+
expect.stringContaining(
|
|
1554
|
+
"--dry-run is not yet supported for local or docker targets",
|
|
1555
|
+
),
|
|
1949
1556
|
);
|
|
1557
|
+
|
|
1558
|
+
// Must fail BEFORE any export work — no signed URL request, no platform
|
|
1559
|
+
// export initiation, nothing that costs time or bandwidth.
|
|
1560
|
+
expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
|
|
1561
|
+
expect(platformInitiateExportMock).not.toHaveBeenCalled();
|
|
1562
|
+
expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
|
|
1950
1563
|
} finally {
|
|
1951
|
-
|
|
1564
|
+
restoreFetch();
|
|
1952
1565
|
}
|
|
1953
1566
|
});
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
// ---------------------------------------------------------------------------
|
|
1570
|
+
// Pre-check: block teleport to platform when existing assistant detected
|
|
1571
|
+
// ---------------------------------------------------------------------------
|
|
1572
|
+
|
|
1573
|
+
describe("pre-check: existing platform assistant", () => {
|
|
1574
|
+
test("blocks before any work when pre-check finds existing assistant", async () => {
|
|
1575
|
+
setArgv("--from", "my-local", "--platform");
|
|
1576
|
+
|
|
1577
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1578
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1579
|
+
name === "my-local" ? localEntry : null,
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1582
|
+
checkExistingPlatformAssistantMock.mockResolvedValue({
|
|
1583
|
+
id: "existing-platform-id",
|
|
1584
|
+
name: "existing-platform",
|
|
1585
|
+
status: "active",
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1589
|
+
|
|
1590
|
+
expect(checkExistingPlatformAssistantMock).toHaveBeenCalledWith(
|
|
1591
|
+
"platform-token",
|
|
1592
|
+
undefined,
|
|
1593
|
+
);
|
|
1594
|
+
// No signed-URL or runtime calls
|
|
1595
|
+
expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
|
|
1596
|
+
expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
|
|
1597
|
+
expect(hatchAssistantMock).not.toHaveBeenCalled();
|
|
1598
|
+
|
|
1599
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1600
|
+
expect.stringContaining("already have a platform assistant"),
|
|
1601
|
+
);
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
// ---------------------------------------------------------------------------
|
|
1606
|
+
// Version guard
|
|
1607
|
+
// ---------------------------------------------------------------------------
|
|
1954
1608
|
|
|
1955
|
-
|
|
1956
|
-
|
|
1609
|
+
describe("version guard", () => {
|
|
1610
|
+
test("blocks platform→local when local version is behind", async () => {
|
|
1611
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1957
1612
|
|
|
1958
1613
|
const platformEntry = makeEntry("my-platform", {
|
|
1959
1614
|
cloud: "vellum",
|
|
1960
1615
|
runtimeUrl: "https://platform.vellum.ai",
|
|
1961
1616
|
});
|
|
1962
|
-
const
|
|
1617
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1963
1618
|
|
|
1964
1619
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1965
1620
|
if (name === "my-platform") return platformEntry;
|
|
1966
|
-
if (name === "my-
|
|
1621
|
+
if (name === "my-local") return localEntry;
|
|
1967
1622
|
return null;
|
|
1968
1623
|
});
|
|
1969
1624
|
|
|
1970
|
-
// Source (platform) is on 0.7.0, target (docker) is on 0.5.0
|
|
1971
1625
|
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
1972
1626
|
if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
|
|
1973
|
-
return Promise.resolve("0.
|
|
1627
|
+
return Promise.resolve("0.6.0");
|
|
1974
1628
|
});
|
|
1975
1629
|
|
|
1976
|
-
const
|
|
1977
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1978
|
-
|
|
1630
|
+
const restoreFetch = installTrackingFetch();
|
|
1979
1631
|
try {
|
|
1980
1632
|
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1981
1633
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1982
|
-
expect.stringContaining("is running 0.
|
|
1983
|
-
);
|
|
1984
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1985
|
-
expect.stringContaining("Upgrade your docker assistant first"),
|
|
1634
|
+
expect.stringContaining("is running 0.6.0"),
|
|
1986
1635
|
);
|
|
1987
1636
|
} finally {
|
|
1988
|
-
|
|
1637
|
+
restoreFetch();
|
|
1989
1638
|
}
|
|
1990
1639
|
});
|
|
1991
1640
|
|
|
1992
|
-
test("
|
|
1993
|
-
setArgv("--from", "my-platform", "--local", "my-local"
|
|
1641
|
+
test("allows equal versions", async () => {
|
|
1642
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1994
1643
|
|
|
1995
1644
|
const platformEntry = makeEntry("my-platform", {
|
|
1996
1645
|
cloud: "vellum",
|
|
@@ -2004,28 +1653,26 @@ describe("version guard: block platform→non-platform when target is behind", (
|
|
|
2004
1653
|
return null;
|
|
2005
1654
|
});
|
|
2006
1655
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
1656
|
+
fetchCurrentVersionMock.mockResolvedValue("0.7.0");
|
|
1657
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1658
|
+
jobId: "platform-export-job-1",
|
|
1659
|
+
type: "export",
|
|
1660
|
+
status: "complete",
|
|
1661
|
+
bundleKey: "b",
|
|
2011
1662
|
});
|
|
2012
1663
|
|
|
2013
|
-
const
|
|
2014
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2015
|
-
|
|
1664
|
+
const restoreFetch = installTrackingFetch();
|
|
2016
1665
|
try {
|
|
2017
|
-
await
|
|
2018
|
-
expect(
|
|
2019
|
-
expect.stringContaining("
|
|
1666
|
+
await teleport();
|
|
1667
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
1668
|
+
expect.stringContaining("Teleport complete"),
|
|
2020
1669
|
);
|
|
2021
1670
|
} finally {
|
|
2022
|
-
|
|
1671
|
+
restoreFetch();
|
|
2023
1672
|
}
|
|
2024
1673
|
});
|
|
2025
1674
|
|
|
2026
1675
|
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
1676
|
setArgv("--from", "my-platform", "--local");
|
|
2030
1677
|
|
|
2031
1678
|
const platformEntry = makeEntry("my-platform", {
|
|
@@ -2034,12 +1681,10 @@ describe("version guard: block platform→non-platform when target is behind", (
|
|
|
2034
1681
|
});
|
|
2035
1682
|
const newLocalEntry = makeEntry("new-local", { cloud: "local" });
|
|
2036
1683
|
|
|
2037
|
-
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
});
|
|
1684
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1685
|
+
name === "my-platform" ? platformEntry : null,
|
|
1686
|
+
);
|
|
2041
1687
|
|
|
2042
|
-
// Simulate hatch creating a new local entry
|
|
2043
1688
|
loadAllAssistantsMock.mockImplementation(() => {
|
|
2044
1689
|
if (hatchLocalMock.mock.calls.length > 0) {
|
|
2045
1690
|
return [platformEntry, newLocalEntry];
|
|
@@ -2047,70 +1692,42 @@ describe("version guard: block platform→non-platform when target is behind", (
|
|
|
2047
1692
|
return [platformEntry];
|
|
2048
1693
|
});
|
|
2049
1694
|
|
|
2050
|
-
|
|
1695
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1696
|
+
jobId: "platform-export-job-1",
|
|
1697
|
+
type: "export",
|
|
1698
|
+
status: "complete",
|
|
1699
|
+
bundleKey: "b",
|
|
1700
|
+
});
|
|
1701
|
+
|
|
2051
1702
|
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
2052
1703
|
if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
|
|
2053
1704
|
return Promise.resolve("0.6.0");
|
|
2054
1705
|
});
|
|
2055
1706
|
|
|
2056
|
-
const
|
|
2057
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2058
|
-
|
|
1707
|
+
const restoreFetch = installTrackingFetch();
|
|
2059
1708
|
try {
|
|
2060
1709
|
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
2061
|
-
// Should have hatched a new local assistant
|
|
2062
1710
|
expect(hatchLocalMock).toHaveBeenCalled();
|
|
2063
|
-
// Should retire the orphaned assistant
|
|
2064
1711
|
expect(retireLocalMock).toHaveBeenCalledWith("new-local", newLocalEntry);
|
|
2065
1712
|
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
1713
|
} finally {
|
|
2095
|
-
|
|
1714
|
+
restoreFetch();
|
|
2096
1715
|
}
|
|
2097
1716
|
});
|
|
2098
1717
|
});
|
|
2099
1718
|
|
|
2100
1719
|
// ---------------------------------------------------------------------------
|
|
2101
|
-
// Credential import display
|
|
1720
|
+
// Credential import display
|
|
2102
1721
|
// ---------------------------------------------------------------------------
|
|
2103
1722
|
|
|
2104
1723
|
describe("credential import display", () => {
|
|
2105
|
-
test("prints credential counts when credentialsImported is present", async () => {
|
|
1724
|
+
test("prints credential counts when credentialsImported is present (platform target)", async () => {
|
|
2106
1725
|
setArgv("--from", "my-local", "--platform");
|
|
2107
1726
|
|
|
2108
1727
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
return null;
|
|
2113
|
-
});
|
|
1728
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1729
|
+
name === "my-local" ? localEntry : null,
|
|
1730
|
+
);
|
|
2114
1731
|
|
|
2115
1732
|
platformImportBundleFromGcsMock.mockResolvedValue({
|
|
2116
1733
|
statusCode: 200,
|
|
@@ -2132,47 +1749,24 @@ describe("credential import display", () => {
|
|
|
2132
1749
|
},
|
|
2133
1750
|
});
|
|
2134
1751
|
|
|
2135
|
-
const
|
|
2136
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2137
|
-
|
|
1752
|
+
const restoreFetch = installTrackingFetch();
|
|
2138
1753
|
try {
|
|
2139
1754
|
await teleport();
|
|
2140
1755
|
expect(consoleLogSpy).toHaveBeenCalledWith(" Credentials imported: 5/5");
|
|
2141
1756
|
} finally {
|
|
2142
|
-
|
|
1757
|
+
restoreFetch();
|
|
2143
1758
|
}
|
|
2144
1759
|
});
|
|
2145
1760
|
|
|
2146
|
-
test("does not print credential line when
|
|
1761
|
+
test("does not print credential line when absent", async () => {
|
|
2147
1762
|
setArgv("--from", "my-local", "--platform");
|
|
2148
1763
|
|
|
2149
1764
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1765
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1766
|
+
name === "my-local" ? localEntry : null,
|
|
1767
|
+
);
|
|
2150
1768
|
|
|
2151
|
-
|
|
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
|
-
|
|
1769
|
+
const restoreFetch = installTrackingFetch();
|
|
2176
1770
|
try {
|
|
2177
1771
|
await teleport();
|
|
2178
1772
|
const allLogCalls = consoleLogSpy.mock.calls.map((c: unknown[]) => c[0]);
|
|
@@ -2182,98 +1776,7 @@ describe("credential import display", () => {
|
|
|
2182
1776
|
);
|
|
2183
1777
|
expect(credentialLines).toHaveLength(0);
|
|
2184
1778
|
} finally {
|
|
2185
|
-
|
|
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;
|
|
1779
|
+
restoreFetch();
|
|
2277
1780
|
}
|
|
2278
1781
|
});
|
|
2279
1782
|
});
|
|
@@ -2282,7 +1785,7 @@ describe("credential import display", () => {
|
|
|
2282
1785
|
// Platform credential injection after teleport
|
|
2283
1786
|
// ---------------------------------------------------------------------------
|
|
2284
1787
|
|
|
2285
|
-
describe("
|
|
1788
|
+
describe("platform credential injection", () => {
|
|
2286
1789
|
test("platform→local teleport calls ensureSelfHostedLocalRegistration and injectCredentialsIntoAssistant", async () => {
|
|
2287
1790
|
setArgv("--from", "my-platform", "--local", "my-local");
|
|
2288
1791
|
|
|
@@ -2301,9 +1804,14 @@ describe("teleport platform credential injection", () => {
|
|
|
2301
1804
|
return null;
|
|
2302
1805
|
});
|
|
2303
1806
|
|
|
2304
|
-
|
|
2305
|
-
|
|
1807
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1808
|
+
jobId: "platform-export-job-1",
|
|
1809
|
+
type: "export",
|
|
1810
|
+
status: "complete",
|
|
1811
|
+
bundleKey: "bundle-xyz",
|
|
1812
|
+
});
|
|
2306
1813
|
|
|
1814
|
+
const restoreFetch = installTrackingFetch();
|
|
2307
1815
|
try {
|
|
2308
1816
|
await teleport();
|
|
2309
1817
|
expect(ensureSelfHostedLocalRegistrationMock).toHaveBeenCalledWith(
|
|
@@ -2323,76 +1831,87 @@ describe("teleport platform credential injection", () => {
|
|
|
2323
1831
|
userId: "user-1",
|
|
2324
1832
|
webhookSecret: "webhook-secret-123",
|
|
2325
1833
|
});
|
|
2326
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
2327
|
-
" Platform credentials injected.",
|
|
2328
|
-
);
|
|
2329
1834
|
} finally {
|
|
2330
|
-
|
|
1835
|
+
restoreFetch();
|
|
2331
1836
|
}
|
|
2332
1837
|
});
|
|
2333
1838
|
|
|
2334
|
-
test("
|
|
2335
|
-
setArgv("--from", "my-
|
|
1839
|
+
test("local→docker teleport does NOT call credential injection", async () => {
|
|
1840
|
+
setArgv("--from", "my-local", "--docker", "my-docker");
|
|
2336
1841
|
|
|
2337
|
-
const
|
|
2338
|
-
cloud: "vellum",
|
|
2339
|
-
runtimeUrl: "https://platform.vellum.ai",
|
|
2340
|
-
});
|
|
1842
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2341
1843
|
const dockerEntry = makeEntry("my-docker", {
|
|
2342
1844
|
cloud: "docker",
|
|
2343
1845
|
runtimeUrl: "http://localhost:8821",
|
|
2344
|
-
bearerToken: "docker-bearer",
|
|
2345
1846
|
});
|
|
2346
1847
|
|
|
2347
1848
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2348
|
-
if (name === "my-
|
|
1849
|
+
if (name === "my-local") return localEntry;
|
|
2349
1850
|
if (name === "my-docker") return dockerEntry;
|
|
2350
1851
|
return null;
|
|
2351
1852
|
});
|
|
2352
1853
|
|
|
2353
|
-
const
|
|
2354
|
-
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2355
|
-
|
|
1854
|
+
const restoreFetch = installTrackingFetch();
|
|
2356
1855
|
try {
|
|
2357
1856
|
await teleport();
|
|
2358
|
-
expect(ensureSelfHostedLocalRegistrationMock).toHaveBeenCalled();
|
|
2359
|
-
expect(injectCredentialsIntoAssistantMock).toHaveBeenCalled();
|
|
2360
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
2361
|
-
" Platform credentials injected.",
|
|
2362
|
-
);
|
|
1857
|
+
expect(ensureSelfHostedLocalRegistrationMock).not.toHaveBeenCalled();
|
|
1858
|
+
expect(injectCredentialsIntoAssistantMock).not.toHaveBeenCalled();
|
|
2363
1859
|
} finally {
|
|
2364
|
-
|
|
1860
|
+
restoreFetch();
|
|
2365
1861
|
}
|
|
2366
1862
|
});
|
|
1863
|
+
});
|
|
2367
1864
|
|
|
2368
|
-
|
|
2369
|
-
|
|
1865
|
+
// ---------------------------------------------------------------------------
|
|
1866
|
+
// Auth / transient-error resilience (Codex P1/P2 regression guards)
|
|
1867
|
+
// ---------------------------------------------------------------------------
|
|
1868
|
+
|
|
1869
|
+
describe("auth + transient-error resilience", () => {
|
|
1870
|
+
test("runtime 401 on export kickoff triggers token refresh and retry", async () => {
|
|
1871
|
+
setArgv("--from", "my-local", "--platform");
|
|
2370
1872
|
|
|
2371
1873
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
});
|
|
1874
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1875
|
+
name === "my-local" ? localEntry : null,
|
|
1876
|
+
);
|
|
2376
1877
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
return null;
|
|
1878
|
+
// First kickoff call fails with 401, second succeeds.
|
|
1879
|
+
localRuntimeExportToGcsMock.mockImplementationOnce(async () => {
|
|
1880
|
+
throw new Error("Local runtime export-to-gcs failed (401): stale token");
|
|
2381
1881
|
});
|
|
1882
|
+
localRuntimeExportToGcsMock.mockImplementationOnce(async () => ({
|
|
1883
|
+
jobId: "local-export-job-after-refresh",
|
|
1884
|
+
}));
|
|
2382
1885
|
|
|
2383
|
-
|
|
2384
|
-
|
|
1886
|
+
// Ensure the refresh path returns a distinguishable token.
|
|
1887
|
+
leaseGuardianTokenMock.mockResolvedValueOnce({
|
|
1888
|
+
accessToken: "refreshed-token",
|
|
1889
|
+
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1890
|
+
} as unknown as Awaited<
|
|
1891
|
+
ReturnType<typeof guardianToken.leaseGuardianToken>
|
|
1892
|
+
>);
|
|
2385
1893
|
|
|
1894
|
+
const restoreFetch = installTrackingFetch();
|
|
2386
1895
|
try {
|
|
2387
1896
|
await teleport();
|
|
2388
|
-
expect(ensureSelfHostedLocalRegistrationMock).not.toHaveBeenCalled();
|
|
2389
|
-
expect(injectCredentialsIntoAssistantMock).not.toHaveBeenCalled();
|
|
2390
1897
|
} finally {
|
|
2391
|
-
|
|
1898
|
+
restoreFetch();
|
|
2392
1899
|
}
|
|
1900
|
+
|
|
1901
|
+
// Kickoff was attempted twice: once with the cached token, once after
|
|
1902
|
+
// a forced refresh lease.
|
|
1903
|
+
expect(localRuntimeExportToGcsMock).toHaveBeenCalledTimes(2);
|
|
1904
|
+
|
|
1905
|
+
const firstTokenArg = localRuntimeExportToGcsMock.mock.calls[0][1];
|
|
1906
|
+
const secondTokenArg = localRuntimeExportToGcsMock.mock.calls[1][1];
|
|
1907
|
+
expect(firstTokenArg).toBe("local-token");
|
|
1908
|
+
expect(secondTokenArg).toBe("refreshed-token");
|
|
1909
|
+
|
|
1910
|
+
// A fresh lease was requested exactly once (the forceRefresh path).
|
|
1911
|
+
expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
|
|
2393
1912
|
});
|
|
2394
1913
|
|
|
2395
|
-
test("
|
|
1914
|
+
test("runtime 401 on import kickoff triggers token refresh and retry", async () => {
|
|
2396
1915
|
setArgv("--from", "my-platform", "--local", "my-local");
|
|
2397
1916
|
|
|
2398
1917
|
const platformEntry = makeEntry("my-platform", {
|
|
@@ -2407,36 +1926,181 @@ describe("teleport platform credential injection", () => {
|
|
|
2407
1926
|
return null;
|
|
2408
1927
|
});
|
|
2409
1928
|
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
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;
|
|
1929
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1930
|
+
jobId: "platform-export-job-1",
|
|
1931
|
+
type: "export",
|
|
1932
|
+
status: "complete",
|
|
1933
|
+
bundleKey: "b-key",
|
|
2422
1934
|
});
|
|
2423
1935
|
|
|
2424
|
-
|
|
2425
|
-
|
|
1936
|
+
localRuntimeImportFromGcsMock.mockImplementationOnce(async () => {
|
|
1937
|
+
throw new Error(
|
|
1938
|
+
"Local runtime import-from-gcs failed (401): stale token",
|
|
1939
|
+
);
|
|
1940
|
+
});
|
|
1941
|
+
localRuntimeImportFromGcsMock.mockImplementationOnce(async () => ({
|
|
1942
|
+
jobId: "local-import-after-refresh",
|
|
1943
|
+
}));
|
|
2426
1944
|
|
|
1945
|
+
leaseGuardianTokenMock.mockResolvedValueOnce({
|
|
1946
|
+
accessToken: "refreshed-import-token",
|
|
1947
|
+
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1948
|
+
} as unknown as Awaited<
|
|
1949
|
+
ReturnType<typeof guardianToken.leaseGuardianToken>
|
|
1950
|
+
>);
|
|
1951
|
+
|
|
1952
|
+
const restoreFetch = installTrackingFetch();
|
|
2427
1953
|
try {
|
|
2428
1954
|
await teleport();
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
1955
|
+
} finally {
|
|
1956
|
+
restoreFetch();
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
expect(localRuntimeImportFromGcsMock).toHaveBeenCalledTimes(2);
|
|
1960
|
+
expect(localRuntimeImportFromGcsMock.mock.calls[0][1]).toBe("local-token");
|
|
1961
|
+
expect(localRuntimeImportFromGcsMock.mock.calls[1][1]).toBe(
|
|
1962
|
+
"refreshed-import-token",
|
|
1963
|
+
);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
test("runtime non-401 errors do NOT trigger token refresh", async () => {
|
|
1967
|
+
setArgv("--from", "my-local", "--platform");
|
|
1968
|
+
|
|
1969
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1970
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1971
|
+
name === "my-local" ? localEntry : null,
|
|
1972
|
+
);
|
|
1973
|
+
|
|
1974
|
+
localRuntimeExportToGcsMock.mockRejectedValue(
|
|
1975
|
+
new Error("Local runtime export-to-gcs failed (500): boom"),
|
|
1976
|
+
);
|
|
1977
|
+
|
|
1978
|
+
const restoreFetch = installTrackingFetch();
|
|
1979
|
+
try {
|
|
1980
|
+
await expect(teleport()).rejects.toThrow(/500.*boom/);
|
|
1981
|
+
} finally {
|
|
1982
|
+
restoreFetch();
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// One attempt, no forced-refresh lease.
|
|
1986
|
+
expect(localRuntimeExportToGcsMock).toHaveBeenCalledTimes(1);
|
|
1987
|
+
expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
test("runtime poll 401 mid-migration triggers forceRefresh lease and completes", async () => {
|
|
1991
|
+
setArgv("--from", "my-local", "--platform");
|
|
1992
|
+
|
|
1993
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1994
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1995
|
+
name === "my-local" ? localEntry : null,
|
|
1996
|
+
);
|
|
1997
|
+
|
|
1998
|
+
// Export kickoff succeeds with the cached "local-token".
|
|
1999
|
+
// During polling, the first status check fails with 401 (token expired
|
|
2000
|
+
// mid-migration), the poll loop calls refreshOn401 → leaseGuardianToken,
|
|
2001
|
+
// then the next poll succeeds with the new token.
|
|
2002
|
+
const tokensSeenByPoll: string[] = [];
|
|
2003
|
+
localRuntimePollJobStatusMock.mockImplementation(
|
|
2004
|
+
async (_runtimeUrl, token, jobId) => {
|
|
2005
|
+
tokensSeenByPoll.push(token);
|
|
2006
|
+
if (tokensSeenByPoll.length === 1) {
|
|
2007
|
+
throw new Error("Local job status check failed: 401 Unauthorized");
|
|
2008
|
+
}
|
|
2009
|
+
return {
|
|
2010
|
+
jobId,
|
|
2011
|
+
type: "export" as const,
|
|
2012
|
+
status: "complete" as const,
|
|
2013
|
+
result: undefined,
|
|
2014
|
+
};
|
|
2015
|
+
},
|
|
2016
|
+
);
|
|
2017
|
+
|
|
2018
|
+
leaseGuardianTokenMock.mockResolvedValueOnce({
|
|
2019
|
+
accessToken: "poll-refreshed-token",
|
|
2020
|
+
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
2021
|
+
} as unknown as Awaited<
|
|
2022
|
+
ReturnType<typeof guardianToken.leaseGuardianToken>
|
|
2023
|
+
>);
|
|
2024
|
+
|
|
2025
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
2026
|
+
const restoreFetch = installTrackingFetch();
|
|
2027
|
+
try {
|
|
2028
|
+
await teleport();
|
|
2029
|
+
|
|
2030
|
+
// The first poll used the cached token; the second (post-refresh) poll
|
|
2031
|
+
// used the freshly leased one.
|
|
2032
|
+
expect(tokensSeenByPoll.length).toBeGreaterThanOrEqual(2);
|
|
2033
|
+
expect(tokensSeenByPoll[0]).toBe("local-token");
|
|
2034
|
+
expect(tokensSeenByPoll[1]).toBe("poll-refreshed-token");
|
|
2035
|
+
|
|
2036
|
+
// leaseGuardianToken was invoked for the forceRefresh path.
|
|
2037
|
+
expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
|
|
2038
|
+
|
|
2039
|
+
// The 401 branch emits its own warning — distinct from the generic
|
|
2040
|
+
// transient-error warning — so this asserts the refresh path fired.
|
|
2041
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
2042
|
+
expect.stringContaining("refreshing auth"),
|
|
2433
2043
|
);
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2044
|
+
} finally {
|
|
2045
|
+
restoreFetch();
|
|
2046
|
+
warnSpy.mockRestore();
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
test("transient poll error does not abort teleport (job completes after retry)", async () => {
|
|
2051
|
+
setArgv("--from", "my-local", "--platform");
|
|
2052
|
+
|
|
2053
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2054
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
2055
|
+
name === "my-local" ? localEntry : null,
|
|
2056
|
+
);
|
|
2057
|
+
|
|
2058
|
+
// Throw once with a 503 (transient), then succeed with terminal complete.
|
|
2059
|
+
let pollCalls = 0;
|
|
2060
|
+
localRuntimePollJobStatusMock.mockImplementation(
|
|
2061
|
+
async (_runtimeUrl, _token, jobId) => {
|
|
2062
|
+
pollCalls += 1;
|
|
2063
|
+
if (pollCalls === 1) {
|
|
2064
|
+
throw new Error(
|
|
2065
|
+
"Local job status check failed: 503 Service Unavailable",
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
return {
|
|
2069
|
+
jobId,
|
|
2070
|
+
type: "export" as const,
|
|
2071
|
+
status: "complete" as const,
|
|
2072
|
+
result: undefined,
|
|
2073
|
+
};
|
|
2074
|
+
},
|
|
2075
|
+
);
|
|
2076
|
+
|
|
2077
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
2078
|
+
const restoreFetch = installTrackingFetch();
|
|
2079
|
+
try {
|
|
2080
|
+
// Should NOT reject — a single transient 503 is retried, not fatal.
|
|
2081
|
+
await teleport();
|
|
2082
|
+
expect(pollCalls).toBeGreaterThanOrEqual(2);
|
|
2083
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
2084
|
+
expect.stringContaining("polling failed, retrying"),
|
|
2437
2085
|
);
|
|
2438
2086
|
} finally {
|
|
2439
|
-
|
|
2087
|
+
restoreFetch();
|
|
2088
|
+
warnSpy.mockRestore();
|
|
2440
2089
|
}
|
|
2441
2090
|
});
|
|
2442
2091
|
});
|
|
2092
|
+
|
|
2093
|
+
// ---------------------------------------------------------------------------
|
|
2094
|
+
// Misc: legacy --to deprecation
|
|
2095
|
+
// ---------------------------------------------------------------------------
|
|
2096
|
+
|
|
2097
|
+
describe("misc", () => {
|
|
2098
|
+
test("legacy --to flag shows deprecation message", async () => {
|
|
2099
|
+
setArgv("--from", "source", "--to", "target");
|
|
2100
|
+
|
|
2101
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
2102
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
2103
|
+
expect.stringContaining("--to is deprecated"),
|
|
2104
|
+
);
|
|
2105
|
+
});
|
|
2106
|
+
});
|