@vellumai/cli 0.5.16 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -108,6 +108,37 @@ const platformImportBundleMock = mock(async () => ({
108
108
  },
109
109
  } as Record<string, unknown>,
110
110
  }));
111
+ const platformRequestUploadUrlMock = mock(async () => ({
112
+ uploadUrl: "https://storage.googleapis.com/bucket/signed-upload-url",
113
+ bundleKey: "bundle-key-123",
114
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
115
+ }));
116
+ const platformUploadToSignedUrlMock = mock(async () => {});
117
+ const platformImportPreflightFromGcsMock = mock(async () => ({
118
+ statusCode: 200,
119
+ body: {
120
+ can_import: true,
121
+ summary: {
122
+ files_to_create: 2,
123
+ files_to_overwrite: 1,
124
+ files_unchanged: 0,
125
+ total_files: 3,
126
+ },
127
+ } as Record<string, unknown>,
128
+ }));
129
+ const platformImportBundleFromGcsMock = mock(async () => ({
130
+ statusCode: 200,
131
+ body: {
132
+ success: true,
133
+ summary: {
134
+ total_files: 3,
135
+ files_created: 2,
136
+ files_overwritten: 1,
137
+ files_skipped: 0,
138
+ backups_created: 1,
139
+ },
140
+ } as Record<string, unknown>,
141
+ }));
111
142
 
112
143
  mock.module("../lib/platform-client.js", () => ({
113
144
  readPlatformToken: readPlatformTokenMock,
@@ -119,6 +150,10 @@ mock.module("../lib/platform-client.js", () => ({
119
150
  platformDownloadExport: platformDownloadExportMock,
120
151
  platformImportPreflight: platformImportPreflightMock,
121
152
  platformImportBundle: platformImportBundleMock,
153
+ platformRequestUploadUrl: platformRequestUploadUrlMock,
154
+ platformUploadToSignedUrl: platformUploadToSignedUrlMock,
155
+ platformImportPreflightFromGcs: platformImportPreflightFromGcsMock,
156
+ platformImportBundleFromGcs: platformImportBundleFromGcsMock,
122
157
  }));
123
158
 
124
159
  const hatchLocalMock = mock(async () => {});
@@ -256,6 +291,41 @@ beforeEach(() => {
256
291
  },
257
292
  },
258
293
  });
294
+ platformRequestUploadUrlMock.mockReset();
295
+ platformRequestUploadUrlMock.mockResolvedValue({
296
+ uploadUrl: "https://storage.googleapis.com/bucket/signed-upload-url",
297
+ bundleKey: "bundle-key-123",
298
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
299
+ });
300
+ platformUploadToSignedUrlMock.mockReset();
301
+ platformUploadToSignedUrlMock.mockResolvedValue(undefined);
302
+ platformImportPreflightFromGcsMock.mockReset();
303
+ platformImportPreflightFromGcsMock.mockResolvedValue({
304
+ statusCode: 200,
305
+ body: {
306
+ can_import: true,
307
+ summary: {
308
+ files_to_create: 2,
309
+ files_to_overwrite: 1,
310
+ files_unchanged: 0,
311
+ total_files: 3,
312
+ },
313
+ },
314
+ });
315
+ platformImportBundleFromGcsMock.mockReset();
316
+ platformImportBundleFromGcsMock.mockResolvedValue({
317
+ statusCode: 200,
318
+ body: {
319
+ success: true,
320
+ summary: {
321
+ total_files: 3,
322
+ files_created: 2,
323
+ files_overwritten: 1,
324
+ files_skipped: 0,
325
+ backups_created: 1,
326
+ },
327
+ },
328
+ });
259
329
 
260
330
  hatchLocalMock.mockReset();
261
331
  hatchLocalMock.mockResolvedValue(undefined);
@@ -687,7 +757,10 @@ describe("resolveOrHatchTarget", () => {
687
757
  findAssistantByNameMock.mockReturnValue(null);
688
758
 
689
759
  const result = await resolveOrHatchTarget("platform", "nonexistent");
690
- expect(hatchAssistantMock).toHaveBeenCalledWith("platform-token");
760
+ expect(hatchAssistantMock).toHaveBeenCalledWith(
761
+ "platform-token",
762
+ "org-123",
763
+ );
691
764
  expect(saveAssistantEntryMock).toHaveBeenCalledWith(
692
765
  expect.objectContaining({
693
766
  assistantId: "platform-new-id",
@@ -998,3 +1071,373 @@ describe("teleport full flow", () => {
998
1071
  );
999
1072
  });
1000
1073
  });
1074
+
1075
+ // ---------------------------------------------------------------------------
1076
+ // Signed-URL upload tests
1077
+ // ---------------------------------------------------------------------------
1078
+
1079
+ describe("signed-URL upload flow", () => {
1080
+ test("happy path: signed URL upload succeeds → GCS-based import used", async () => {
1081
+ setArgv("--from", "my-local", "--platform");
1082
+
1083
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1084
+
1085
+ findAssistantByNameMock.mockImplementation((name: string) => {
1086
+ if (name === "my-local") return localEntry;
1087
+ return null;
1088
+ });
1089
+
1090
+ const originalFetch = globalThis.fetch;
1091
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1092
+
1093
+ try {
1094
+ await teleport();
1095
+
1096
+ // Signed-URL flow should be used
1097
+ expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1098
+ expect(platformUploadToSignedUrlMock).toHaveBeenCalled();
1099
+ expect(platformImportBundleFromGcsMock).toHaveBeenCalledWith(
1100
+ "bundle-key-123",
1101
+ "platform-token",
1102
+ "org-123",
1103
+ "https://platform.vellum.ai",
1104
+ );
1105
+ // Inline import should NOT be called
1106
+ expect(platformImportBundleMock).not.toHaveBeenCalled();
1107
+ expect(consoleLogSpy).toHaveBeenCalledWith(
1108
+ expect.stringContaining("Teleport complete"),
1109
+ );
1110
+ } finally {
1111
+ globalThis.fetch = originalFetch;
1112
+ }
1113
+ });
1114
+
1115
+ test("happy path dry-run: signed URL upload succeeds → GCS-based preflight used", async () => {
1116
+ setArgv(
1117
+ "--from",
1118
+ "my-local",
1119
+ "--platform",
1120
+ "existing-platform",
1121
+ "--dry-run",
1122
+ );
1123
+
1124
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1125
+ const platformEntry = makeEntry("existing-platform", {
1126
+ cloud: "vellum",
1127
+ runtimeUrl: "https://platform.vellum.ai",
1128
+ });
1129
+
1130
+ findAssistantByNameMock.mockImplementation((name: string) => {
1131
+ if (name === "my-local") return localEntry;
1132
+ if (name === "existing-platform") return platformEntry;
1133
+ return null;
1134
+ });
1135
+
1136
+ const originalFetch = globalThis.fetch;
1137
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1138
+
1139
+ try {
1140
+ await teleport();
1141
+
1142
+ // Signed-URL flow should be used for preflight
1143
+ expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1144
+ expect(platformUploadToSignedUrlMock).toHaveBeenCalled();
1145
+ expect(platformImportPreflightFromGcsMock).toHaveBeenCalledWith(
1146
+ "bundle-key-123",
1147
+ "platform-token",
1148
+ "org-123",
1149
+ "https://platform.vellum.ai",
1150
+ );
1151
+ // Inline preflight should NOT be called
1152
+ expect(platformImportPreflightMock).not.toHaveBeenCalled();
1153
+ } finally {
1154
+ globalThis.fetch = originalFetch;
1155
+ }
1156
+ });
1157
+
1158
+ test("fallback: platformRequestUploadUrl throws 503 → falls back to inline import", async () => {
1159
+ setArgv("--from", "my-local", "--platform");
1160
+
1161
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1162
+
1163
+ findAssistantByNameMock.mockImplementation((name: string) => {
1164
+ if (name === "my-local") return localEntry;
1165
+ return null;
1166
+ });
1167
+
1168
+ // Simulate 503 — "not available" in the error message
1169
+ platformRequestUploadUrlMock.mockRejectedValue(
1170
+ new Error("Signed uploads are not available on this platform instance"),
1171
+ );
1172
+
1173
+ const originalFetch = globalThis.fetch;
1174
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1175
+
1176
+ try {
1177
+ await teleport();
1178
+
1179
+ // Should fall back to inline import
1180
+ expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1181
+ expect(platformUploadToSignedUrlMock).not.toHaveBeenCalled();
1182
+ expect(platformImportBundleFromGcsMock).not.toHaveBeenCalled();
1183
+ expect(platformImportBundleMock).toHaveBeenCalled();
1184
+ expect(consoleLogSpy).toHaveBeenCalledWith(
1185
+ expect.stringContaining("Teleport complete"),
1186
+ );
1187
+ } finally {
1188
+ globalThis.fetch = originalFetch;
1189
+ }
1190
+ });
1191
+
1192
+ test("fallback: platformRequestUploadUrl throws 404 → falls back to inline import", async () => {
1193
+ setArgv("--from", "my-local", "--platform");
1194
+
1195
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1196
+
1197
+ findAssistantByNameMock.mockImplementation((name: string) => {
1198
+ if (name === "my-local") return localEntry;
1199
+ return null;
1200
+ });
1201
+
1202
+ // Simulate 404 — endpoint doesn't exist on older platform versions
1203
+ platformRequestUploadUrlMock.mockRejectedValue(
1204
+ new Error("Signed uploads are not available on this platform instance"),
1205
+ );
1206
+
1207
+ const originalFetch = globalThis.fetch;
1208
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1209
+
1210
+ try {
1211
+ await teleport();
1212
+
1213
+ // Should fall back to inline import
1214
+ expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1215
+ expect(platformUploadToSignedUrlMock).not.toHaveBeenCalled();
1216
+ expect(platformImportBundleFromGcsMock).not.toHaveBeenCalled();
1217
+ expect(platformImportBundleMock).toHaveBeenCalled();
1218
+ expect(consoleLogSpy).toHaveBeenCalledWith(
1219
+ expect.stringContaining("Teleport complete"),
1220
+ );
1221
+ } finally {
1222
+ globalThis.fetch = originalFetch;
1223
+ }
1224
+ });
1225
+
1226
+ test("upload error: platformUploadToSignedUrl throws → error propagates", async () => {
1227
+ setArgv("--from", "my-local", "--platform");
1228
+
1229
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1230
+
1231
+ findAssistantByNameMock.mockImplementation((name: string) => {
1232
+ if (name === "my-local") return localEntry;
1233
+ return null;
1234
+ });
1235
+
1236
+ // Upload succeeds at getting URL but fails during PUT
1237
+ platformUploadToSignedUrlMock.mockRejectedValue(
1238
+ new Error("Upload to signed URL failed: 500 Internal Server Error"),
1239
+ );
1240
+
1241
+ const originalFetch = globalThis.fetch;
1242
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1243
+
1244
+ try {
1245
+ await expect(teleport()).rejects.toThrow(
1246
+ "Upload to signed URL failed: 500 Internal Server Error",
1247
+ );
1248
+ // Should NOT fall back to inline import
1249
+ expect(platformImportBundleMock).not.toHaveBeenCalled();
1250
+ expect(platformImportBundleFromGcsMock).not.toHaveBeenCalled();
1251
+ } finally {
1252
+ globalThis.fetch = originalFetch;
1253
+ }
1254
+ });
1255
+
1256
+ test("413 from GCS import: error message includes 'too large'", async () => {
1257
+ setArgv("--from", "my-local", "--platform");
1258
+
1259
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1260
+
1261
+ findAssistantByNameMock.mockImplementation((name: string) => {
1262
+ if (name === "my-local") return localEntry;
1263
+ return null;
1264
+ });
1265
+
1266
+ // GCS import returns 413
1267
+ platformImportBundleFromGcsMock.mockRejectedValue(
1268
+ new Error("Bundle too large to import"),
1269
+ );
1270
+
1271
+ const originalFetch = globalThis.fetch;
1272
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1273
+
1274
+ try {
1275
+ await expect(teleport()).rejects.toThrow("too large");
1276
+ } finally {
1277
+ globalThis.fetch = originalFetch;
1278
+ }
1279
+ });
1280
+ });
1281
+
1282
+ // ---------------------------------------------------------------------------
1283
+ // Platform teleport org ID and reordered flow tests
1284
+ // ---------------------------------------------------------------------------
1285
+
1286
+ describe("platform teleport org ID and reordered flow", () => {
1287
+ test("hatchAssistant is called with the org ID from fetchOrganizationId", async () => {
1288
+ setArgv("--from", "my-local", "--platform");
1289
+
1290
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1291
+
1292
+ findAssistantByNameMock.mockImplementation((name: string) => {
1293
+ if (name === "my-local") return localEntry;
1294
+ return null;
1295
+ });
1296
+
1297
+ fetchOrganizationIdMock.mockResolvedValue("test-org-456");
1298
+
1299
+ const originalFetch = globalThis.fetch;
1300
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1301
+
1302
+ try {
1303
+ await teleport();
1304
+
1305
+ // fetchOrganizationId should have been called
1306
+ expect(fetchOrganizationIdMock).toHaveBeenCalled();
1307
+ // hatchAssistant must be called with (token, orgId)
1308
+ expect(hatchAssistantMock).toHaveBeenCalledWith(
1309
+ "platform-token",
1310
+ "test-org-456",
1311
+ );
1312
+ } finally {
1313
+ globalThis.fetch = originalFetch;
1314
+ }
1315
+ });
1316
+
1317
+ test("upload to GCS happens before hatchAssistant for platform targets", async () => {
1318
+ setArgv("--from", "my-local", "--platform");
1319
+
1320
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1321
+
1322
+ findAssistantByNameMock.mockImplementation((name: string) => {
1323
+ if (name === "my-local") return localEntry;
1324
+ return null;
1325
+ });
1326
+
1327
+ const callOrder: string[] = [];
1328
+
1329
+ platformRequestUploadUrlMock.mockImplementation(async () => {
1330
+ callOrder.push("platformRequestUploadUrl");
1331
+ return {
1332
+ uploadUrl: "https://storage.googleapis.com/bucket/signed-upload-url",
1333
+ bundleKey: "bundle-key-123",
1334
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1335
+ };
1336
+ });
1337
+
1338
+ platformUploadToSignedUrlMock.mockImplementation(async () => {
1339
+ callOrder.push("platformUploadToSignedUrl");
1340
+ });
1341
+
1342
+ hatchAssistantMock.mockImplementation(async () => {
1343
+ callOrder.push("hatchAssistant");
1344
+ return { id: "platform-new-id", name: "platform-new", status: "active" };
1345
+ });
1346
+
1347
+ const originalFetch = globalThis.fetch;
1348
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1349
+
1350
+ try {
1351
+ await teleport();
1352
+
1353
+ // Verify ordering: upload steps come before hatch
1354
+ const uploadUrlIdx = callOrder.indexOf("platformRequestUploadUrl");
1355
+ const uploadIdx = callOrder.indexOf("platformUploadToSignedUrl");
1356
+ const hatchIdx = callOrder.indexOf("hatchAssistant");
1357
+
1358
+ expect(uploadUrlIdx).toBeGreaterThanOrEqual(0);
1359
+ expect(uploadIdx).toBeGreaterThanOrEqual(0);
1360
+ expect(hatchIdx).toBeGreaterThanOrEqual(0);
1361
+ expect(uploadUrlIdx).toBeLessThan(hatchIdx);
1362
+ expect(uploadIdx).toBeLessThan(hatchIdx);
1363
+ } finally {
1364
+ globalThis.fetch = originalFetch;
1365
+ }
1366
+ });
1367
+
1368
+ test("signed-URL fallback: when platformRequestUploadUrl throws 'not available', falls back to inline upload via importToAssistant", async () => {
1369
+ setArgv("--from", "my-local", "--platform");
1370
+
1371
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1372
+
1373
+ findAssistantByNameMock.mockImplementation((name: string) => {
1374
+ if (name === "my-local") return localEntry;
1375
+ return null;
1376
+ });
1377
+
1378
+ // Simulate 503 — signed uploads not available
1379
+ platformRequestUploadUrlMock.mockRejectedValue(
1380
+ new Error("Signed uploads are not available on this platform instance"),
1381
+ );
1382
+
1383
+ const originalFetch = globalThis.fetch;
1384
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1385
+
1386
+ try {
1387
+ await teleport();
1388
+
1389
+ // Upload URL was attempted but failed
1390
+ expect(platformRequestUploadUrlMock).toHaveBeenCalled();
1391
+ // No signed URL upload should have happened
1392
+ expect(platformUploadToSignedUrlMock).not.toHaveBeenCalled();
1393
+ // Should NOT use GCS-based import
1394
+ expect(platformImportBundleFromGcsMock).not.toHaveBeenCalled();
1395
+ // Should fall back to inline import
1396
+ expect(platformImportBundleMock).toHaveBeenCalled();
1397
+ // Hatch should still succeed
1398
+ expect(hatchAssistantMock).toHaveBeenCalled();
1399
+ expect(consoleLogSpy).toHaveBeenCalledWith(
1400
+ expect.stringContaining("Teleport complete"),
1401
+ );
1402
+ } finally {
1403
+ globalThis.fetch = originalFetch;
1404
+ }
1405
+ });
1406
+
1407
+ test("bundleKey from pre-upload is forwarded to platformImportBundleFromGcs", async () => {
1408
+ setArgv("--from", "my-local", "--platform");
1409
+
1410
+ const localEntry = makeEntry("my-local", { cloud: "local" });
1411
+
1412
+ findAssistantByNameMock.mockImplementation((name: string) => {
1413
+ if (name === "my-local") return localEntry;
1414
+ return null;
1415
+ });
1416
+
1417
+ // Return a specific bundle key from the pre-upload step
1418
+ platformRequestUploadUrlMock.mockResolvedValue({
1419
+ uploadUrl: "https://storage.googleapis.com/bucket/signed-upload-url",
1420
+ bundleKey: "pre-uploaded-key-789",
1421
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1422
+ });
1423
+
1424
+ const originalFetch = globalThis.fetch;
1425
+ globalThis.fetch = createFetchMock() as unknown as typeof globalThis.fetch;
1426
+
1427
+ try {
1428
+ await teleport();
1429
+
1430
+ // The bundle key from the pre-upload step should be forwarded to GCS import
1431
+ expect(platformImportBundleFromGcsMock).toHaveBeenCalledWith(
1432
+ "pre-uploaded-key-789",
1433
+ "platform-token",
1434
+ expect.any(String),
1435
+ expect.any(String),
1436
+ );
1437
+ // Inline import should NOT be used since signed upload succeeded
1438
+ expect(platformImportBundleMock).not.toHaveBeenCalled();
1439
+ } finally {
1440
+ globalThis.fetch = originalFetch;
1441
+ }
1442
+ });
1443
+ });
@@ -21,6 +21,7 @@ import { hatchGcp } from "../lib/gcp";
21
21
  import type { PollResult, WatchHatchingResult } from "../lib/gcp";
22
22
  import { hatchLocal } from "../lib/hatch-local";
23
23
  import {
24
+ fetchOrganizationId,
24
25
  getPlatformUrl,
25
26
  hatchAssistant,
26
27
  readPlatformToken,
@@ -593,7 +594,19 @@ async function hatchVellumPlatform(): Promise<void> {
593
594
  console.log(" Hatching assistant on Vellum platform...");
594
595
  console.log("");
595
596
 
596
- const result = await hatchAssistant(token);
597
+ let orgId: string;
598
+ try {
599
+ orgId = await fetchOrganizationId(token);
600
+ } catch (err) {
601
+ const msg = err instanceof Error ? err.message : String(err);
602
+ if (msg.includes("401") || msg.includes("403")) {
603
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
604
+ process.exit(1);
605
+ }
606
+ throw err;
607
+ }
608
+
609
+ const result = await hatchAssistant(token, orgId);
597
610
 
598
611
  const platformUrl = getPlatformUrl();
599
612
 
@@ -352,6 +352,11 @@ export async function rollback(): Promise<void> {
352
352
  ` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
353
353
  );
354
354
 
355
+ // Capture GUARDIAN_BOOTSTRAP_SECRET from the gateway container (it is only
356
+ // set on gateway, not assistant) so it persists across container restarts.
357
+ const gatewayEnv = await captureContainerEnv(res.gatewayContainer);
358
+ const bootstrapSecret = gatewayEnv["GUARDIAN_BOOTSTRAP_SECRET"];
359
+
355
360
  // Extract CES_SERVICE_TOKEN from captured env, or generate fresh one
356
361
  const cesServiceToken =
357
362
  capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
@@ -430,6 +435,7 @@ export async function rollback(): Promise<void> {
430
435
  await startContainers(
431
436
  {
432
437
  signingKey,
438
+ bootstrapSecret,
433
439
  cesServiceToken,
434
440
  extraAssistantEnv,
435
441
  gatewayPort,