@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.
- package/bun.lock +46 -52
- package/package.json +1 -1
- package/src/__tests__/teleport.test.ts +444 -1
- package/src/commands/hatch.ts +14 -1
- package/src/commands/rollback.ts +6 -0
- package/src/commands/teleport.ts +168 -17
- package/src/commands/upgrade.ts +7 -0
- package/src/lib/aws.ts +2 -1
- package/src/lib/constants.ts +0 -11
- package/src/lib/docker.ts +27 -13
- package/src/lib/gcp.ts +2 -5
- package/src/lib/platform-client.ts +142 -8
- package/src/lib/upgrade-lifecycle.ts +8 -0
- package/src/shared/provider-env-vars.ts +19 -0
|
@@ -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(
|
|
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
|
+
});
|
package/src/commands/hatch.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/commands/rollback.ts
CHANGED
|
@@ -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,
|