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