@vellumai/cli 0.6.2 → 0.6.4
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 +12 -2
- package/README.md +3 -3
- package/bunfig.toml +6 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +124 -0
- package/src/__tests__/env-drift.test.ts +87 -0
- package/src/__tests__/guardian-token.test.ts +172 -0
- package/src/__tests__/multi-local.test.ts +61 -14
- package/src/__tests__/orphan-detection.test.ts +214 -0
- package/src/__tests__/platform-client.test.ts +204 -0
- package/src/__tests__/preload.ts +27 -0
- package/src/__tests__/ssh-user-guard.test.ts +28 -0
- package/src/__tests__/teleport.test.ts +1073 -57
- package/src/commands/backup.ts +8 -0
- package/src/commands/hatch.ts +5 -28
- package/src/commands/login.ts +178 -9
- package/src/commands/logs.ts +652 -0
- package/src/commands/pair.ts +9 -1
- package/src/commands/ps.ts +37 -7
- package/src/commands/recover.ts +8 -4
- package/src/commands/restore.ts +124 -12
- package/src/commands/retire.ts +17 -3
- package/src/commands/rollback.ts +32 -33
- package/src/commands/sleep.ts +7 -0
- package/src/commands/ssh-apple-container.ts +162 -0
- package/src/commands/ssh.ts +7 -0
- package/src/commands/teleport.ts +307 -3
- package/src/commands/upgrade.ts +43 -52
- package/src/commands/wake.ts +21 -10
- package/src/components/DefaultMainScreen.tsx +7 -1
- package/src/index.ts +3 -0
- package/src/lib/__tests__/docker.test.ts +78 -0
- package/src/lib/assistant-config.ts +54 -87
- package/src/lib/aws.ts +12 -1
- package/src/lib/constants.ts +0 -10
- package/src/lib/docker.ts +73 -4
- package/src/lib/environments/__tests__/paths.test.ts +234 -0
- package/src/lib/environments/__tests__/resolve.test.ts +226 -0
- package/src/lib/environments/paths.ts +110 -0
- package/src/lib/environments/resolve.ts +96 -0
- package/src/lib/environments/seeds.ts +46 -0
- package/src/lib/environments/types.ts +60 -0
- package/src/lib/gcp.ts +12 -1
- package/src/lib/guardian-token.ts +8 -10
- package/src/lib/hatch-local.ts +30 -35
- package/src/lib/local.ts +46 -5
- package/src/lib/orphan-detection.ts +28 -12
- package/src/lib/platform-client.ts +261 -25
- package/src/lib/retire-apple-container.ts +102 -0
- package/src/lib/upgrade-lifecycle.ts +101 -28
|
@@ -23,11 +23,16 @@ 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
|
|
27
|
-
//
|
|
28
|
-
// other test files (e.g.
|
|
29
|
-
//
|
|
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.
|
|
30
33
|
import * as assistantConfig from "../lib/assistant-config.js";
|
|
34
|
+
import * as guardianToken from "../lib/guardian-token.js";
|
|
35
|
+
import * as platformClient from "../lib/platform-client.js";
|
|
31
36
|
|
|
32
37
|
const findAssistantByNameMock = spyOn(
|
|
33
38
|
assistantConfig,
|
|
@@ -49,40 +54,77 @@ const removeAssistantEntryMock = spyOn(
|
|
|
49
54
|
"removeAssistantEntry",
|
|
50
55
|
).mockImplementation(() => {});
|
|
51
56
|
|
|
52
|
-
const loadGuardianTokenMock =
|
|
57
|
+
const loadGuardianTokenMock = spyOn(
|
|
58
|
+
guardianToken,
|
|
59
|
+
"loadGuardianToken",
|
|
60
|
+
).mockReturnValue({
|
|
53
61
|
accessToken: "local-token",
|
|
54
62
|
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
55
|
-
})
|
|
56
|
-
|
|
63
|
+
} as unknown as ReturnType<typeof guardianToken.loadGuardianToken>);
|
|
64
|
+
|
|
65
|
+
const leaseGuardianTokenMock = spyOn(
|
|
66
|
+
guardianToken,
|
|
67
|
+
"leaseGuardianToken",
|
|
68
|
+
).mockResolvedValue({
|
|
57
69
|
accessToken: "leased-token",
|
|
58
70
|
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
} as unknown as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>);
|
|
72
|
+
|
|
73
|
+
const computeDeviceIdMock = spyOn(
|
|
74
|
+
guardianToken,
|
|
75
|
+
"computeDeviceId",
|
|
76
|
+
).mockReturnValue("device-id-123");
|
|
77
|
+
|
|
78
|
+
const readPlatformTokenMock = spyOn(
|
|
79
|
+
platformClient,
|
|
80
|
+
"readPlatformToken",
|
|
81
|
+
).mockReturnValue("platform-token");
|
|
82
|
+
|
|
83
|
+
const getPlatformUrlMock = spyOn(
|
|
84
|
+
platformClient,
|
|
85
|
+
"getPlatformUrl",
|
|
86
|
+
).mockReturnValue("https://platform.vellum.ai");
|
|
87
|
+
|
|
88
|
+
const hatchAssistantMock = spyOn(
|
|
89
|
+
platformClient,
|
|
90
|
+
"hatchAssistant",
|
|
91
|
+
).mockResolvedValue({
|
|
92
|
+
assistant: {
|
|
93
|
+
id: "platform-new-id",
|
|
94
|
+
name: "platform-new",
|
|
95
|
+
status: "active",
|
|
96
|
+
},
|
|
97
|
+
reusedExisting: false,
|
|
98
|
+
});
|
|
65
99
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
name: "platform-new",
|
|
71
|
-
status: "active",
|
|
72
|
-
}));
|
|
73
|
-
const platformInitiateExportMock = mock(async () => ({
|
|
100
|
+
const platformInitiateExportMock = spyOn(
|
|
101
|
+
platformClient,
|
|
102
|
+
"platformInitiateExport",
|
|
103
|
+
).mockResolvedValue({
|
|
74
104
|
jobId: "job-1",
|
|
75
105
|
status: "pending",
|
|
76
|
-
})
|
|
77
|
-
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const platformPollExportStatusMock = spyOn(
|
|
109
|
+
platformClient,
|
|
110
|
+
"platformPollExportStatus",
|
|
111
|
+
).mockResolvedValue({
|
|
78
112
|
status: "complete" as string,
|
|
79
113
|
downloadUrl: "https://cdn.example.com/bundle.tar.gz",
|
|
80
|
-
})
|
|
81
|
-
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const platformDownloadExportMock = spyOn(
|
|
117
|
+
platformClient,
|
|
118
|
+
"platformDownloadExport",
|
|
119
|
+
).mockImplementation(async () => {
|
|
82
120
|
const data = new Uint8Array([10, 20, 30]);
|
|
83
121
|
return new Response(data, { status: 200 });
|
|
84
122
|
});
|
|
85
|
-
|
|
123
|
+
|
|
124
|
+
const platformImportPreflightMock = spyOn(
|
|
125
|
+
platformClient,
|
|
126
|
+
"platformImportPreflight",
|
|
127
|
+
).mockResolvedValue({
|
|
86
128
|
statusCode: 200,
|
|
87
129
|
body: {
|
|
88
130
|
can_import: true,
|
|
@@ -93,8 +135,12 @@ const platformImportPreflightMock = mock(async () => ({
|
|
|
93
135
|
total_files: 3,
|
|
94
136
|
},
|
|
95
137
|
} as Record<string, unknown>,
|
|
96
|
-
})
|
|
97
|
-
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const platformImportBundleMock = spyOn(
|
|
141
|
+
platformClient,
|
|
142
|
+
"platformImportBundle",
|
|
143
|
+
).mockResolvedValue({
|
|
98
144
|
statusCode: 200,
|
|
99
145
|
body: {
|
|
100
146
|
success: true,
|
|
@@ -106,14 +152,26 @@ const platformImportBundleMock = mock(async () => ({
|
|
|
106
152
|
backups_created: 1,
|
|
107
153
|
},
|
|
108
154
|
} as Record<string, unknown>,
|
|
109
|
-
})
|
|
110
|
-
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const platformRequestUploadUrlMock = spyOn(
|
|
158
|
+
platformClient,
|
|
159
|
+
"platformRequestUploadUrl",
|
|
160
|
+
).mockResolvedValue({
|
|
111
161
|
uploadUrl: "https://storage.googleapis.com/bucket/signed-upload-url",
|
|
112
162
|
bundleKey: "bundle-key-123",
|
|
113
163
|
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
const
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const platformUploadToSignedUrlMock = spyOn(
|
|
167
|
+
platformClient,
|
|
168
|
+
"platformUploadToSignedUrl",
|
|
169
|
+
).mockResolvedValue(undefined);
|
|
170
|
+
|
|
171
|
+
const platformImportPreflightFromGcsMock = spyOn(
|
|
172
|
+
platformClient,
|
|
173
|
+
"platformImportPreflightFromGcs",
|
|
174
|
+
).mockResolvedValue({
|
|
117
175
|
statusCode: 200,
|
|
118
176
|
body: {
|
|
119
177
|
can_import: true,
|
|
@@ -124,8 +182,12 @@ const platformImportPreflightFromGcsMock = mock(async () => ({
|
|
|
124
182
|
total_files: 3,
|
|
125
183
|
},
|
|
126
184
|
} as Record<string, unknown>,
|
|
127
|
-
})
|
|
128
|
-
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const platformImportBundleFromGcsMock = spyOn(
|
|
188
|
+
platformClient,
|
|
189
|
+
"platformImportBundleFromGcs",
|
|
190
|
+
).mockResolvedValue({
|
|
129
191
|
statusCode: 200,
|
|
130
192
|
body: {
|
|
131
193
|
success: true,
|
|
@@ -137,22 +199,47 @@ const platformImportBundleFromGcsMock = mock(async () => ({
|
|
|
137
199
|
backups_created: 1,
|
|
138
200
|
},
|
|
139
201
|
} as Record<string, unknown>,
|
|
140
|
-
})
|
|
202
|
+
});
|
|
141
203
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
204
|
+
const checkExistingPlatformAssistantMock = spyOn(
|
|
205
|
+
platformClient,
|
|
206
|
+
"checkExistingPlatformAssistant",
|
|
207
|
+
).mockResolvedValue(null);
|
|
208
|
+
|
|
209
|
+
const ensureSelfHostedLocalRegistrationMock = spyOn(
|
|
210
|
+
platformClient,
|
|
211
|
+
"ensureSelfHostedLocalRegistration",
|
|
212
|
+
).mockResolvedValue({
|
|
213
|
+
assistant: { id: "platform-assistant-1", name: "my-assistant" },
|
|
214
|
+
registration: {
|
|
215
|
+
client_installation_id: "device-id-123",
|
|
216
|
+
runtime_assistant_id: "target-local",
|
|
217
|
+
client_platform: "cli",
|
|
218
|
+
},
|
|
219
|
+
assistant_api_key: "api-key-123",
|
|
220
|
+
webhook_secret: "webhook-secret-123",
|
|
221
|
+
} as unknown as Awaited<
|
|
222
|
+
ReturnType<typeof platformClient.ensureSelfHostedLocalRegistration>
|
|
223
|
+
>);
|
|
224
|
+
|
|
225
|
+
const injectCredentialsIntoAssistantMock = spyOn(
|
|
226
|
+
platformClient,
|
|
227
|
+
"injectCredentialsIntoAssistant",
|
|
228
|
+
).mockResolvedValue(true);
|
|
229
|
+
|
|
230
|
+
const fetchCurrentUserMock = spyOn(
|
|
231
|
+
platformClient,
|
|
232
|
+
"fetchCurrentUser",
|
|
233
|
+
).mockResolvedValue({
|
|
234
|
+
id: "user-1",
|
|
235
|
+
email: "test@example.com",
|
|
236
|
+
display: "Test",
|
|
237
|
+
} as unknown as Awaited<ReturnType<typeof platformClient.fetchCurrentUser>>);
|
|
238
|
+
|
|
239
|
+
const fetchOrganizationIdMock = spyOn(
|
|
240
|
+
platformClient,
|
|
241
|
+
"fetchOrganizationId",
|
|
242
|
+
).mockResolvedValue("org-1");
|
|
156
243
|
|
|
157
244
|
const hatchLocalMock = mock(async () => {});
|
|
158
245
|
|
|
@@ -166,9 +253,14 @@ const retireDockerMock = mock(async () => {});
|
|
|
166
253
|
const sleepContainersMock = mock(async () => {});
|
|
167
254
|
const dockerResourceNamesMock = mock((name: string) => ({
|
|
168
255
|
assistantContainer: `${name}-assistant`,
|
|
256
|
+
cesContainer: `${name}-credential-executor`,
|
|
257
|
+
cesSecurityVolume: `${name}-ces-sec`,
|
|
258
|
+
dockerdDataVolume: `${name}-dockerd-data`,
|
|
169
259
|
gatewayContainer: `${name}-gateway`,
|
|
170
|
-
|
|
260
|
+
gatewaySecurityVolume: `${name}-gateway-sec`,
|
|
171
261
|
network: `${name}-net`,
|
|
262
|
+
socketVolume: `${name}-socket`,
|
|
263
|
+
workspaceVolume: `${name}-workspace`,
|
|
172
264
|
}));
|
|
173
265
|
|
|
174
266
|
mock.module("../lib/docker.js", () => ({
|
|
@@ -190,6 +282,14 @@ mock.module("../lib/retire-local.js", () => ({
|
|
|
190
282
|
retireLocal: retireLocalMock,
|
|
191
283
|
}));
|
|
192
284
|
|
|
285
|
+
const fetchCurrentVersionMock = mock(
|
|
286
|
+
async (_runtimeUrl: string): Promise<string | undefined> => undefined,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
mock.module("../lib/upgrade-lifecycle.js", () => ({
|
|
290
|
+
fetchCurrentVersion: fetchCurrentVersionMock,
|
|
291
|
+
}));
|
|
292
|
+
|
|
193
293
|
import {
|
|
194
294
|
teleport,
|
|
195
295
|
parseArgs,
|
|
@@ -206,6 +306,26 @@ afterAll(() => {
|
|
|
206
306
|
saveAssistantEntryMock.mockRestore();
|
|
207
307
|
loadAllAssistantsMock.mockRestore();
|
|
208
308
|
removeAssistantEntryMock.mockRestore();
|
|
309
|
+
loadGuardianTokenMock.mockRestore();
|
|
310
|
+
leaseGuardianTokenMock.mockRestore();
|
|
311
|
+
readPlatformTokenMock.mockRestore();
|
|
312
|
+
getPlatformUrlMock.mockRestore();
|
|
313
|
+
hatchAssistantMock.mockRestore();
|
|
314
|
+
checkExistingPlatformAssistantMock.mockRestore();
|
|
315
|
+
platformInitiateExportMock.mockRestore();
|
|
316
|
+
platformPollExportStatusMock.mockRestore();
|
|
317
|
+
platformDownloadExportMock.mockRestore();
|
|
318
|
+
platformImportPreflightMock.mockRestore();
|
|
319
|
+
platformImportBundleMock.mockRestore();
|
|
320
|
+
platformRequestUploadUrlMock.mockRestore();
|
|
321
|
+
platformUploadToSignedUrlMock.mockRestore();
|
|
322
|
+
platformImportPreflightFromGcsMock.mockRestore();
|
|
323
|
+
platformImportBundleFromGcsMock.mockRestore();
|
|
324
|
+
ensureSelfHostedLocalRegistrationMock.mockRestore();
|
|
325
|
+
injectCredentialsIntoAssistantMock.mockRestore();
|
|
326
|
+
fetchCurrentUserMock.mockRestore();
|
|
327
|
+
fetchOrganizationIdMock.mockRestore();
|
|
328
|
+
computeDeviceIdMock.mockRestore();
|
|
209
329
|
rmSync(testDir, { recursive: true, force: true });
|
|
210
330
|
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
211
331
|
});
|
|
@@ -233,7 +353,7 @@ beforeEach(() => {
|
|
|
233
353
|
loadGuardianTokenMock.mockReturnValue({
|
|
234
354
|
accessToken: "local-token",
|
|
235
355
|
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
236
|
-
});
|
|
356
|
+
} as unknown as ReturnType<typeof guardianToken.loadGuardianToken>);
|
|
237
357
|
leaseGuardianTokenMock.mockReset();
|
|
238
358
|
|
|
239
359
|
readPlatformTokenMock.mockReset();
|
|
@@ -242,9 +362,12 @@ beforeEach(() => {
|
|
|
242
362
|
getPlatformUrlMock.mockReturnValue("https://platform.vellum.ai");
|
|
243
363
|
hatchAssistantMock.mockReset();
|
|
244
364
|
hatchAssistantMock.mockResolvedValue({
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
365
|
+
assistant: {
|
|
366
|
+
id: "platform-new-id",
|
|
367
|
+
name: "platform-new",
|
|
368
|
+
status: "active",
|
|
369
|
+
},
|
|
370
|
+
reusedExisting: false,
|
|
248
371
|
});
|
|
249
372
|
platformInitiateExportMock.mockReset();
|
|
250
373
|
platformInitiateExportMock.mockResolvedValue({
|
|
@@ -322,6 +445,31 @@ beforeEach(() => {
|
|
|
322
445
|
},
|
|
323
446
|
},
|
|
324
447
|
});
|
|
448
|
+
checkExistingPlatformAssistantMock.mockReset();
|
|
449
|
+
checkExistingPlatformAssistantMock.mockResolvedValue(null);
|
|
450
|
+
ensureSelfHostedLocalRegistrationMock.mockReset();
|
|
451
|
+
ensureSelfHostedLocalRegistrationMock.mockResolvedValue({
|
|
452
|
+
assistant: { id: "platform-assistant-1", name: "my-assistant" },
|
|
453
|
+
registration: {
|
|
454
|
+
client_installation_id: "device-id-123",
|
|
455
|
+
runtime_assistant_id: "target-local",
|
|
456
|
+
client_platform: "cli",
|
|
457
|
+
},
|
|
458
|
+
assistant_api_key: "api-key-123",
|
|
459
|
+
webhook_secret: "webhook-secret-123",
|
|
460
|
+
});
|
|
461
|
+
injectCredentialsIntoAssistantMock.mockReset();
|
|
462
|
+
injectCredentialsIntoAssistantMock.mockResolvedValue(true);
|
|
463
|
+
fetchCurrentUserMock.mockReset();
|
|
464
|
+
fetchCurrentUserMock.mockResolvedValue({
|
|
465
|
+
id: "user-1",
|
|
466
|
+
email: "test@example.com",
|
|
467
|
+
display: "Test",
|
|
468
|
+
});
|
|
469
|
+
fetchOrganizationIdMock.mockReset();
|
|
470
|
+
fetchOrganizationIdMock.mockResolvedValue("org-1");
|
|
471
|
+
computeDeviceIdMock.mockReset();
|
|
472
|
+
computeDeviceIdMock.mockReturnValue("device-id-123");
|
|
325
473
|
|
|
326
474
|
hatchLocalMock.mockReset();
|
|
327
475
|
hatchLocalMock.mockResolvedValue(undefined);
|
|
@@ -331,6 +479,8 @@ beforeEach(() => {
|
|
|
331
479
|
retireDockerMock.mockResolvedValue(undefined);
|
|
332
480
|
retireLocalMock.mockReset();
|
|
333
481
|
retireLocalMock.mockResolvedValue(undefined);
|
|
482
|
+
fetchCurrentVersionMock.mockReset();
|
|
483
|
+
fetchCurrentVersionMock.mockResolvedValue(undefined);
|
|
334
484
|
sleepContainersMock.mockReset();
|
|
335
485
|
sleepContainersMock.mockResolvedValue(undefined);
|
|
336
486
|
stopProcessByPidFileMock.mockReset();
|
|
@@ -728,7 +878,6 @@ describe("resolveOrHatchTarget", () => {
|
|
|
728
878
|
null,
|
|
729
879
|
false,
|
|
730
880
|
false,
|
|
731
|
-
false,
|
|
732
881
|
{},
|
|
733
882
|
);
|
|
734
883
|
expect(result).toBe(newEntry);
|
|
@@ -763,6 +912,38 @@ describe("resolveOrHatchTarget", () => {
|
|
|
763
912
|
expect(result.assistantId).toBe("platform-new-id");
|
|
764
913
|
});
|
|
765
914
|
|
|
915
|
+
test("platform with no name -> blocks when hatch returns reusedExisting (defensive safety net)", async () => {
|
|
916
|
+
findAssistantByNameMock.mockReturnValue(null);
|
|
917
|
+
hatchAssistantMock.mockResolvedValue({
|
|
918
|
+
assistant: {
|
|
919
|
+
id: "existing-platform-id",
|
|
920
|
+
name: "existing-platform",
|
|
921
|
+
status: "active",
|
|
922
|
+
},
|
|
923
|
+
reusedExisting: true,
|
|
924
|
+
});
|
|
925
|
+
|
|
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
|
+
await expect(resolveOrHatchTarget("platform", undefined)).rejects.toThrow(
|
|
930
|
+
"process.exit:1",
|
|
931
|
+
);
|
|
932
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
933
|
+
expect.stringContaining("already have a platform assistant"),
|
|
934
|
+
);
|
|
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
|
+
});
|
|
946
|
+
|
|
766
947
|
test("existing assistant with wrong cloud -> rejects", async () => {
|
|
767
948
|
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
768
949
|
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
@@ -1325,7 +1506,14 @@ describe("platform teleport org ID and reordered flow", () => {
|
|
|
1325
1506
|
|
|
1326
1507
|
hatchAssistantMock.mockImplementation(async () => {
|
|
1327
1508
|
callOrder.push("hatchAssistant");
|
|
1328
|
-
return {
|
|
1509
|
+
return {
|
|
1510
|
+
assistant: {
|
|
1511
|
+
id: "platform-new-id",
|
|
1512
|
+
name: "platform-new",
|
|
1513
|
+
status: "active",
|
|
1514
|
+
},
|
|
1515
|
+
reusedExisting: false,
|
|
1516
|
+
};
|
|
1329
1517
|
});
|
|
1330
1518
|
|
|
1331
1519
|
const originalFetch = globalThis.fetch;
|
|
@@ -1424,3 +1612,831 @@ describe("platform teleport org ID and reordered flow", () => {
|
|
|
1424
1612
|
}
|
|
1425
1613
|
});
|
|
1426
1614
|
});
|
|
1615
|
+
|
|
1616
|
+
// ---------------------------------------------------------------------------
|
|
1617
|
+
// Pre-check: block teleport to platform when existing assistant detected
|
|
1618
|
+
// ---------------------------------------------------------------------------
|
|
1619
|
+
|
|
1620
|
+
describe("pre-check: block teleport to platform when existing assistant detected", () => {
|
|
1621
|
+
test("blocks BEFORE GCS upload when pre-check finds existing assistant", async () => {
|
|
1622
|
+
setArgv("--from", "my-local", "--platform");
|
|
1623
|
+
|
|
1624
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1625
|
+
|
|
1626
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1627
|
+
if (name === "my-local") return localEntry;
|
|
1628
|
+
return null;
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
// Pre-check returns an existing assistant
|
|
1632
|
+
checkExistingPlatformAssistantMock.mockResolvedValue({
|
|
1633
|
+
id: "existing-platform-id",
|
|
1634
|
+
name: "existing-platform",
|
|
1635
|
+
status: "active",
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
const originalFetch = globalThis.fetch;
|
|
1639
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1640
|
+
|
|
1641
|
+
try {
|
|
1642
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1643
|
+
|
|
1644
|
+
// Pre-check should have been called
|
|
1645
|
+
expect(checkExistingPlatformAssistantMock).toHaveBeenCalledWith(
|
|
1646
|
+
"platform-token",
|
|
1647
|
+
undefined,
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
// GCS upload should NOT have been attempted
|
|
1651
|
+
expect(platformRequestUploadUrlMock).not.toHaveBeenCalled();
|
|
1652
|
+
expect(platformUploadToSignedUrlMock).not.toHaveBeenCalled();
|
|
1653
|
+
|
|
1654
|
+
// Hatch should NOT have been called
|
|
1655
|
+
expect(hatchAssistantMock).not.toHaveBeenCalled();
|
|
1656
|
+
|
|
1657
|
+
// Error message should be actionable
|
|
1658
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1659
|
+
expect.stringContaining("already have a platform assistant"),
|
|
1660
|
+
);
|
|
1661
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1662
|
+
expect.stringContaining("Retire it first"),
|
|
1663
|
+
);
|
|
1664
|
+
|
|
1665
|
+
// The existing assistant is saved to the lockfile
|
|
1666
|
+
expect(saveAssistantEntryMock).toHaveBeenCalledWith(
|
|
1667
|
+
expect.objectContaining({
|
|
1668
|
+
assistantId: "existing-platform-id",
|
|
1669
|
+
cloud: "vellum",
|
|
1670
|
+
}),
|
|
1671
|
+
);
|
|
1672
|
+
} finally {
|
|
1673
|
+
globalThis.fetch = originalFetch;
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
test("skips pre-check when targeting an existing named assistant", async () => {
|
|
1678
|
+
setArgv("--from", "my-local", "--platform", "existing-platform");
|
|
1679
|
+
|
|
1680
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1681
|
+
const platformEntry = makeEntry("existing-platform", {
|
|
1682
|
+
cloud: "vellum",
|
|
1683
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1687
|
+
if (name === "my-local") return localEntry;
|
|
1688
|
+
if (name === "existing-platform") return platformEntry;
|
|
1689
|
+
return null;
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
const originalFetch = globalThis.fetch;
|
|
1693
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1694
|
+
|
|
1695
|
+
try {
|
|
1696
|
+
await teleport();
|
|
1697
|
+
|
|
1698
|
+
// Pre-check should NOT be called when targeting a known named assistant
|
|
1699
|
+
expect(checkExistingPlatformAssistantMock).not.toHaveBeenCalled();
|
|
1700
|
+
|
|
1701
|
+
// Upload should proceed normally
|
|
1702
|
+
expect(platformRequestUploadUrlMock).toHaveBeenCalled();
|
|
1703
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
1704
|
+
expect.stringContaining("Teleport complete"),
|
|
1705
|
+
);
|
|
1706
|
+
} finally {
|
|
1707
|
+
globalThis.fetch = originalFetch;
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
test("pre-check failure is non-fatal — teleport proceeds", async () => {
|
|
1712
|
+
setArgv("--from", "my-local", "--platform");
|
|
1713
|
+
|
|
1714
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1715
|
+
|
|
1716
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1717
|
+
if (name === "my-local") return localEntry;
|
|
1718
|
+
return null;
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
// Pre-check returns null (no existing assistant or API failed gracefully)
|
|
1722
|
+
checkExistingPlatformAssistantMock.mockResolvedValue(null);
|
|
1723
|
+
|
|
1724
|
+
const originalFetch = globalThis.fetch;
|
|
1725
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1726
|
+
|
|
1727
|
+
try {
|
|
1728
|
+
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
|
+
);
|
|
1739
|
+
} finally {
|
|
1740
|
+
globalThis.fetch = originalFetch;
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
// ---------------------------------------------------------------------------
|
|
1746
|
+
// Version guard: block platform→non-platform when target is behind
|
|
1747
|
+
// ---------------------------------------------------------------------------
|
|
1748
|
+
|
|
1749
|
+
describe("version guard: block platform→non-platform when target is behind", () => {
|
|
1750
|
+
test("blocks platform→local when local version is behind platform", async () => {
|
|
1751
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1752
|
+
|
|
1753
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1754
|
+
cloud: "vellum",
|
|
1755
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1756
|
+
});
|
|
1757
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1758
|
+
|
|
1759
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1760
|
+
if (name === "my-platform") return platformEntry;
|
|
1761
|
+
if (name === "my-local") return localEntry;
|
|
1762
|
+
return null;
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// Source (platform) is on 0.7.0, target (local) is on 0.6.0
|
|
1766
|
+
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
1767
|
+
if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
|
|
1768
|
+
return Promise.resolve("0.6.0");
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
const originalFetch = globalThis.fetch;
|
|
1772
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1773
|
+
|
|
1774
|
+
try {
|
|
1775
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1776
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1777
|
+
expect.stringContaining("is running 0.6.0"),
|
|
1778
|
+
);
|
|
1779
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1780
|
+
expect.stringContaining("Upgrade your local assistant first"),
|
|
1781
|
+
);
|
|
1782
|
+
} finally {
|
|
1783
|
+
globalThis.fetch = originalFetch;
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
test("allows platform→local when versions are equal", async () => {
|
|
1788
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1789
|
+
|
|
1790
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1791
|
+
cloud: "vellum",
|
|
1792
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1793
|
+
});
|
|
1794
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1795
|
+
|
|
1796
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1797
|
+
if (name === "my-platform") return platformEntry;
|
|
1798
|
+
if (name === "my-local") return localEntry;
|
|
1799
|
+
return null;
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
// Both on 0.7.0
|
|
1803
|
+
fetchCurrentVersionMock.mockResolvedValue("0.7.0");
|
|
1804
|
+
|
|
1805
|
+
const originalFetch = globalThis.fetch;
|
|
1806
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1807
|
+
|
|
1808
|
+
try {
|
|
1809
|
+
await teleport();
|
|
1810
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
1811
|
+
expect.stringContaining("Teleport complete"),
|
|
1812
|
+
);
|
|
1813
|
+
} finally {
|
|
1814
|
+
globalThis.fetch = originalFetch;
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
test("allows platform→local when local is ahead of platform", async () => {
|
|
1819
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1820
|
+
|
|
1821
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1822
|
+
cloud: "vellum",
|
|
1823
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1824
|
+
});
|
|
1825
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1826
|
+
|
|
1827
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1828
|
+
if (name === "my-platform") return platformEntry;
|
|
1829
|
+
if (name === "my-local") return localEntry;
|
|
1830
|
+
return null;
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
// Source (platform) is on 0.7.0, target (local) is on 0.8.0
|
|
1834
|
+
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
1835
|
+
if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
|
|
1836
|
+
return Promise.resolve("0.8.0");
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
const originalFetch = globalThis.fetch;
|
|
1840
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1841
|
+
|
|
1842
|
+
try {
|
|
1843
|
+
await teleport();
|
|
1844
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
1845
|
+
expect.stringContaining("Teleport complete"),
|
|
1846
|
+
);
|
|
1847
|
+
} finally {
|
|
1848
|
+
globalThis.fetch = originalFetch;
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
test("allows teleport when source version cannot be fetched (best-effort)", async () => {
|
|
1853
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1854
|
+
|
|
1855
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1856
|
+
cloud: "vellum",
|
|
1857
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1858
|
+
});
|
|
1859
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1860
|
+
|
|
1861
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1862
|
+
if (name === "my-platform") return platformEntry;
|
|
1863
|
+
if (name === "my-local") return localEntry;
|
|
1864
|
+
return null;
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
// Source (platform) is unreachable, target (local) is on 0.6.0
|
|
1868
|
+
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
1869
|
+
if (url === "https://platform.vellum.ai")
|
|
1870
|
+
return Promise.resolve(undefined);
|
|
1871
|
+
return Promise.resolve("0.6.0");
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
const originalFetch = globalThis.fetch;
|
|
1875
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1876
|
+
|
|
1877
|
+
try {
|
|
1878
|
+
await teleport();
|
|
1879
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
1880
|
+
expect.stringContaining("Teleport complete"),
|
|
1881
|
+
);
|
|
1882
|
+
} finally {
|
|
1883
|
+
globalThis.fetch = originalFetch;
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
test("allows teleport when target version cannot be fetched (best-effort)", async () => {
|
|
1888
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1889
|
+
|
|
1890
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1891
|
+
cloud: "vellum",
|
|
1892
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1893
|
+
});
|
|
1894
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1895
|
+
|
|
1896
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1897
|
+
if (name === "my-platform") return platformEntry;
|
|
1898
|
+
if (name === "my-local") return localEntry;
|
|
1899
|
+
return null;
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
// Source (platform) is on 0.7.0, target (local) is unreachable
|
|
1903
|
+
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
1904
|
+
if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
|
|
1905
|
+
return Promise.resolve(undefined);
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
const originalFetch = globalThis.fetch;
|
|
1909
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1910
|
+
|
|
1911
|
+
try {
|
|
1912
|
+
await teleport();
|
|
1913
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
1914
|
+
expect.stringContaining("Teleport complete"),
|
|
1915
|
+
);
|
|
1916
|
+
} finally {
|
|
1917
|
+
globalThis.fetch = originalFetch;
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
test("pre-release target is behind release source: 0.7.0-local.xxx < 0.7.0", async () => {
|
|
1922
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1923
|
+
|
|
1924
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1925
|
+
cloud: "vellum",
|
|
1926
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1927
|
+
});
|
|
1928
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1929
|
+
|
|
1930
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1931
|
+
if (name === "my-platform") return platformEntry;
|
|
1932
|
+
if (name === "my-local") return localEntry;
|
|
1933
|
+
return null;
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
// Per semver, pre-release < release for same core version
|
|
1937
|
+
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
1938
|
+
if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
|
|
1939
|
+
return Promise.resolve("0.7.0-local.20260411.abc123");
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
const originalFetch = globalThis.fetch;
|
|
1943
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1944
|
+
|
|
1945
|
+
try {
|
|
1946
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1947
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1948
|
+
expect.stringContaining("is running 0.7.0-local.20260411.abc123"),
|
|
1949
|
+
);
|
|
1950
|
+
} finally {
|
|
1951
|
+
globalThis.fetch = originalFetch;
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
test("blocks platform→docker when docker version is behind platform", async () => {
|
|
1956
|
+
setArgv("--from", "my-platform", "--docker", "my-docker");
|
|
1957
|
+
|
|
1958
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1959
|
+
cloud: "vellum",
|
|
1960
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1961
|
+
});
|
|
1962
|
+
const dockerEntry = makeEntry("my-docker", { cloud: "docker" });
|
|
1963
|
+
|
|
1964
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1965
|
+
if (name === "my-platform") return platformEntry;
|
|
1966
|
+
if (name === "my-docker") return dockerEntry;
|
|
1967
|
+
return null;
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
// Source (platform) is on 0.7.0, target (docker) is on 0.5.0
|
|
1971
|
+
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
1972
|
+
if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
|
|
1973
|
+
return Promise.resolve("0.5.0");
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
const originalFetch = globalThis.fetch;
|
|
1977
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
1978
|
+
|
|
1979
|
+
try {
|
|
1980
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1981
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1982
|
+
expect.stringContaining("is running 0.5.0"),
|
|
1983
|
+
);
|
|
1984
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1985
|
+
expect.stringContaining("Upgrade your docker assistant first"),
|
|
1986
|
+
);
|
|
1987
|
+
} finally {
|
|
1988
|
+
globalThis.fetch = originalFetch;
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
test("dry-run: blocks platform→local when local version is behind", async () => {
|
|
1993
|
+
setArgv("--from", "my-platform", "--local", "my-local", "--dry-run");
|
|
1994
|
+
|
|
1995
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1996
|
+
cloud: "vellum",
|
|
1997
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1998
|
+
});
|
|
1999
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2000
|
+
|
|
2001
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2002
|
+
if (name === "my-platform") return platformEntry;
|
|
2003
|
+
if (name === "my-local") return localEntry;
|
|
2004
|
+
return null;
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
// Source (platform) is on 0.7.0, target (local) is on 0.6.0
|
|
2008
|
+
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
2009
|
+
if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
|
|
2010
|
+
return Promise.resolve("0.6.0");
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
const originalFetch = globalThis.fetch;
|
|
2014
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2015
|
+
|
|
2016
|
+
try {
|
|
2017
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
2018
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
2019
|
+
expect.stringContaining("is running 0.6.0"),
|
|
2020
|
+
);
|
|
2021
|
+
} finally {
|
|
2022
|
+
globalThis.fetch = originalFetch;
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
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
|
+
setArgv("--from", "my-platform", "--local");
|
|
2030
|
+
|
|
2031
|
+
const platformEntry = makeEntry("my-platform", {
|
|
2032
|
+
cloud: "vellum",
|
|
2033
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2034
|
+
});
|
|
2035
|
+
const newLocalEntry = makeEntry("new-local", { cloud: "local" });
|
|
2036
|
+
|
|
2037
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2038
|
+
if (name === "my-platform") return platformEntry;
|
|
2039
|
+
return null;
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
// Simulate hatch creating a new local entry
|
|
2043
|
+
loadAllAssistantsMock.mockImplementation(() => {
|
|
2044
|
+
if (hatchLocalMock.mock.calls.length > 0) {
|
|
2045
|
+
return [platformEntry, newLocalEntry];
|
|
2046
|
+
}
|
|
2047
|
+
return [platformEntry];
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
// Source (platform) is on 0.7.0, newly hatched local is on 0.6.0
|
|
2051
|
+
fetchCurrentVersionMock.mockImplementation((url: string) => {
|
|
2052
|
+
if (url === "https://platform.vellum.ai") return Promise.resolve("0.7.0");
|
|
2053
|
+
return Promise.resolve("0.6.0");
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
const originalFetch = globalThis.fetch;
|
|
2057
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2058
|
+
|
|
2059
|
+
try {
|
|
2060
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
2061
|
+
// Should have hatched a new local assistant
|
|
2062
|
+
expect(hatchLocalMock).toHaveBeenCalled();
|
|
2063
|
+
// Should retire the orphaned assistant
|
|
2064
|
+
expect(retireLocalMock).toHaveBeenCalledWith("new-local", newLocalEntry);
|
|
2065
|
+
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
|
+
} finally {
|
|
2095
|
+
globalThis.fetch = originalFetch;
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
// ---------------------------------------------------------------------------
|
|
2101
|
+
// Credential import display tests
|
|
2102
|
+
// ---------------------------------------------------------------------------
|
|
2103
|
+
|
|
2104
|
+
describe("credential import display", () => {
|
|
2105
|
+
test("prints credential counts when credentialsImported is present", async () => {
|
|
2106
|
+
setArgv("--from", "my-local", "--platform");
|
|
2107
|
+
|
|
2108
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2109
|
+
|
|
2110
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2111
|
+
if (name === "my-local") return localEntry;
|
|
2112
|
+
return null;
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
platformImportBundleFromGcsMock.mockResolvedValue({
|
|
2116
|
+
statusCode: 200,
|
|
2117
|
+
body: {
|
|
2118
|
+
success: true,
|
|
2119
|
+
summary: {
|
|
2120
|
+
total_files: 3,
|
|
2121
|
+
files_created: 2,
|
|
2122
|
+
files_overwritten: 1,
|
|
2123
|
+
files_skipped: 0,
|
|
2124
|
+
backups_created: 1,
|
|
2125
|
+
},
|
|
2126
|
+
credentialsImported: {
|
|
2127
|
+
total: 5,
|
|
2128
|
+
succeeded: 5,
|
|
2129
|
+
failed: 0,
|
|
2130
|
+
failedAccounts: [],
|
|
2131
|
+
},
|
|
2132
|
+
},
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
const originalFetch = globalThis.fetch;
|
|
2136
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2137
|
+
|
|
2138
|
+
try {
|
|
2139
|
+
await teleport();
|
|
2140
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(" Credentials imported: 5/5");
|
|
2141
|
+
} finally {
|
|
2142
|
+
globalThis.fetch = originalFetch;
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
test("does not print credential line when credentialsImported is absent (old server)", async () => {
|
|
2147
|
+
setArgv("--from", "my-local", "--platform");
|
|
2148
|
+
|
|
2149
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2150
|
+
|
|
2151
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2152
|
+
if (name === "my-local") return localEntry;
|
|
2153
|
+
return null;
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
// Mock the GCS import path (used by default since platformRequestUploadUrl
|
|
2157
|
+
// succeeds) with a response that has no credentialsImported field, simulating
|
|
2158
|
+
// an older server.
|
|
2159
|
+
platformImportBundleFromGcsMock.mockResolvedValue({
|
|
2160
|
+
statusCode: 200,
|
|
2161
|
+
body: {
|
|
2162
|
+
success: true,
|
|
2163
|
+
summary: {
|
|
2164
|
+
total_files: 3,
|
|
2165
|
+
files_created: 2,
|
|
2166
|
+
files_overwritten: 1,
|
|
2167
|
+
files_skipped: 0,
|
|
2168
|
+
backups_created: 1,
|
|
2169
|
+
},
|
|
2170
|
+
},
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
const originalFetch = globalThis.fetch;
|
|
2174
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2175
|
+
|
|
2176
|
+
try {
|
|
2177
|
+
await teleport();
|
|
2178
|
+
const allLogCalls = consoleLogSpy.mock.calls.map((c: unknown[]) => c[0]);
|
|
2179
|
+
const credentialLines = allLogCalls.filter(
|
|
2180
|
+
(msg: string) =>
|
|
2181
|
+
typeof msg === "string" && msg.includes("Credentials imported"),
|
|
2182
|
+
);
|
|
2183
|
+
expect(credentialLines).toHaveLength(0);
|
|
2184
|
+
} finally {
|
|
2185
|
+
globalThis.fetch = originalFetch;
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
test("lists failed credential accounts individually", async () => {
|
|
2190
|
+
setArgv("--from", "my-local", "--platform");
|
|
2191
|
+
|
|
2192
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2193
|
+
|
|
2194
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2195
|
+
if (name === "my-local") return localEntry;
|
|
2196
|
+
return null;
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
platformImportBundleFromGcsMock.mockResolvedValue({
|
|
2200
|
+
statusCode: 200,
|
|
2201
|
+
body: {
|
|
2202
|
+
success: true,
|
|
2203
|
+
summary: {
|
|
2204
|
+
total_files: 3,
|
|
2205
|
+
files_created: 2,
|
|
2206
|
+
files_overwritten: 1,
|
|
2207
|
+
files_skipped: 0,
|
|
2208
|
+
backups_created: 1,
|
|
2209
|
+
},
|
|
2210
|
+
credentialsImported: {
|
|
2211
|
+
total: 5,
|
|
2212
|
+
succeeded: 3,
|
|
2213
|
+
failed: 2,
|
|
2214
|
+
failedAccounts: ["google:user@example.com", "github:octocat"],
|
|
2215
|
+
},
|
|
2216
|
+
},
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
const originalFetch = globalThis.fetch;
|
|
2220
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2221
|
+
|
|
2222
|
+
try {
|
|
2223
|
+
await teleport();
|
|
2224
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(" Credentials imported: 3/5");
|
|
2225
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(" Credentials failed: 2");
|
|
2226
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
2227
|
+
" - google:user@example.com",
|
|
2228
|
+
);
|
|
2229
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(" - github:octocat");
|
|
2230
|
+
} finally {
|
|
2231
|
+
globalThis.fetch = originalFetch;
|
|
2232
|
+
}
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
test("shows platform credentials skipped count", async () => {
|
|
2236
|
+
setArgv("--from", "my-local", "--platform");
|
|
2237
|
+
|
|
2238
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2239
|
+
|
|
2240
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2241
|
+
if (name === "my-local") return localEntry;
|
|
2242
|
+
return null;
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
platformImportBundleFromGcsMock.mockResolvedValue({
|
|
2246
|
+
statusCode: 200,
|
|
2247
|
+
body: {
|
|
2248
|
+
success: true,
|
|
2249
|
+
summary: {
|
|
2250
|
+
total_files: 3,
|
|
2251
|
+
files_created: 2,
|
|
2252
|
+
files_overwritten: 1,
|
|
2253
|
+
files_skipped: 0,
|
|
2254
|
+
backups_created: 1,
|
|
2255
|
+
},
|
|
2256
|
+
credentialsImported: {
|
|
2257
|
+
total: 5,
|
|
2258
|
+
succeeded: 5,
|
|
2259
|
+
failed: 0,
|
|
2260
|
+
failedAccounts: [],
|
|
2261
|
+
skippedPlatform: 3,
|
|
2262
|
+
},
|
|
2263
|
+
},
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
const originalFetch = globalThis.fetch;
|
|
2267
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2268
|
+
|
|
2269
|
+
try {
|
|
2270
|
+
await teleport();
|
|
2271
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(" Credentials imported: 5/5");
|
|
2272
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
2273
|
+
" Platform credentials skipped: 3",
|
|
2274
|
+
);
|
|
2275
|
+
} finally {
|
|
2276
|
+
globalThis.fetch = originalFetch;
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
// ---------------------------------------------------------------------------
|
|
2282
|
+
// Platform credential injection after teleport
|
|
2283
|
+
// ---------------------------------------------------------------------------
|
|
2284
|
+
|
|
2285
|
+
describe("teleport platform credential injection", () => {
|
|
2286
|
+
test("platform→local teleport calls ensureSelfHostedLocalRegistration and injectCredentialsIntoAssistant", async () => {
|
|
2287
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
2288
|
+
|
|
2289
|
+
const platformEntry = makeEntry("my-platform", {
|
|
2290
|
+
cloud: "vellum",
|
|
2291
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2292
|
+
});
|
|
2293
|
+
const localEntry = makeEntry("my-local", {
|
|
2294
|
+
cloud: "local",
|
|
2295
|
+
bearerToken: "local-bearer",
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2299
|
+
if (name === "my-platform") return platformEntry;
|
|
2300
|
+
if (name === "my-local") return localEntry;
|
|
2301
|
+
return null;
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
const originalFetch = globalThis.fetch;
|
|
2305
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2306
|
+
|
|
2307
|
+
try {
|
|
2308
|
+
await teleport();
|
|
2309
|
+
expect(ensureSelfHostedLocalRegistrationMock).toHaveBeenCalledWith(
|
|
2310
|
+
"platform-token",
|
|
2311
|
+
"org-1",
|
|
2312
|
+
"device-id-123",
|
|
2313
|
+
"my-local",
|
|
2314
|
+
"cli",
|
|
2315
|
+
);
|
|
2316
|
+
expect(injectCredentialsIntoAssistantMock).toHaveBeenCalledWith({
|
|
2317
|
+
gatewayUrl: "http://localhost:7821",
|
|
2318
|
+
bearerToken: "local-bearer",
|
|
2319
|
+
assistantApiKey: "api-key-123",
|
|
2320
|
+
platformAssistantId: "platform-assistant-1",
|
|
2321
|
+
platformBaseUrl: "https://platform.vellum.ai",
|
|
2322
|
+
organizationId: "org-1",
|
|
2323
|
+
userId: "user-1",
|
|
2324
|
+
webhookSecret: "webhook-secret-123",
|
|
2325
|
+
});
|
|
2326
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
2327
|
+
" Platform credentials injected.",
|
|
2328
|
+
);
|
|
2329
|
+
} finally {
|
|
2330
|
+
globalThis.fetch = originalFetch;
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
test("platform→docker teleport also calls credential injection", async () => {
|
|
2335
|
+
setArgv("--from", "my-platform", "--docker", "my-docker");
|
|
2336
|
+
|
|
2337
|
+
const platformEntry = makeEntry("my-platform", {
|
|
2338
|
+
cloud: "vellum",
|
|
2339
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2340
|
+
});
|
|
2341
|
+
const dockerEntry = makeEntry("my-docker", {
|
|
2342
|
+
cloud: "docker",
|
|
2343
|
+
runtimeUrl: "http://localhost:8821",
|
|
2344
|
+
bearerToken: "docker-bearer",
|
|
2345
|
+
});
|
|
2346
|
+
|
|
2347
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2348
|
+
if (name === "my-platform") return platformEntry;
|
|
2349
|
+
if (name === "my-docker") return dockerEntry;
|
|
2350
|
+
return null;
|
|
2351
|
+
});
|
|
2352
|
+
|
|
2353
|
+
const originalFetch = globalThis.fetch;
|
|
2354
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2355
|
+
|
|
2356
|
+
try {
|
|
2357
|
+
await teleport();
|
|
2358
|
+
expect(ensureSelfHostedLocalRegistrationMock).toHaveBeenCalled();
|
|
2359
|
+
expect(injectCredentialsIntoAssistantMock).toHaveBeenCalled();
|
|
2360
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
2361
|
+
" Platform credentials injected.",
|
|
2362
|
+
);
|
|
2363
|
+
} finally {
|
|
2364
|
+
globalThis.fetch = originalFetch;
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
|
|
2368
|
+
test("local→local teleport does NOT call credential injection", async () => {
|
|
2369
|
+
setArgv("--from", "my-local", "--docker", "my-docker");
|
|
2370
|
+
|
|
2371
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2372
|
+
const dockerEntry = makeEntry("my-docker", {
|
|
2373
|
+
cloud: "docker",
|
|
2374
|
+
runtimeUrl: "http://localhost:8821",
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2378
|
+
if (name === "my-local") return localEntry;
|
|
2379
|
+
if (name === "my-docker") return dockerEntry;
|
|
2380
|
+
return null;
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
const originalFetch = globalThis.fetch;
|
|
2384
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2385
|
+
|
|
2386
|
+
try {
|
|
2387
|
+
await teleport();
|
|
2388
|
+
expect(ensureSelfHostedLocalRegistrationMock).not.toHaveBeenCalled();
|
|
2389
|
+
expect(injectCredentialsIntoAssistantMock).not.toHaveBeenCalled();
|
|
2390
|
+
} finally {
|
|
2391
|
+
globalThis.fetch = originalFetch;
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
test("if user is not logged in (readPlatformToken returns null), injection is skipped gracefully", async () => {
|
|
2396
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
2397
|
+
|
|
2398
|
+
const platformEntry = makeEntry("my-platform", {
|
|
2399
|
+
cloud: "vellum",
|
|
2400
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2401
|
+
});
|
|
2402
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2403
|
+
|
|
2404
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2405
|
+
if (name === "my-platform") return platformEntry;
|
|
2406
|
+
if (name === "my-local") return localEntry;
|
|
2407
|
+
return null;
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
// The readPlatformToken mock is called twice during teleport:
|
|
2411
|
+
// once during the platform export flow and once during credential injection.
|
|
2412
|
+
// We need the first call to return a token (for the export to work)
|
|
2413
|
+
// and the second call to return null (to test the skip path).
|
|
2414
|
+
let callCount = 0;
|
|
2415
|
+
readPlatformTokenMock.mockImplementation(() => {
|
|
2416
|
+
callCount++;
|
|
2417
|
+
// The platform export path calls readPlatformToken first;
|
|
2418
|
+
// the credential injection helper calls it after import.
|
|
2419
|
+
// Return null only on the last call (the injection helper).
|
|
2420
|
+
if (callCount <= 1) return "platform-token";
|
|
2421
|
+
return null;
|
|
2422
|
+
});
|
|
2423
|
+
|
|
2424
|
+
const originalFetch = globalThis.fetch;
|
|
2425
|
+
globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
|
|
2426
|
+
|
|
2427
|
+
try {
|
|
2428
|
+
await teleport();
|
|
2429
|
+
expect(ensureSelfHostedLocalRegistrationMock).not.toHaveBeenCalled();
|
|
2430
|
+
expect(injectCredentialsIntoAssistantMock).not.toHaveBeenCalled();
|
|
2431
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
2432
|
+
" Skipped platform credential injection (not logged in).",
|
|
2433
|
+
);
|
|
2434
|
+
// Teleport still succeeds
|
|
2435
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
2436
|
+
expect.stringContaining("Teleport complete"),
|
|
2437
|
+
);
|
|
2438
|
+
} finally {
|
|
2439
|
+
globalThis.fetch = originalFetch;
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
});
|