@vellumai/cli 0.7.1 → 0.7.3
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 +3 -11
- package/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +121 -5
- package/src/__tests__/teleport.test.ts +515 -10
- package/src/commands/backup.ts +35 -2
- package/src/commands/client.ts +90 -7
- package/src/commands/exec.ts +13 -4
- package/src/commands/hatch.ts +1 -1
- package/src/commands/login.ts +11 -0
- package/src/commands/restore.ts +7 -1
- package/src/commands/rollback.ts +1 -1
- package/src/commands/setup.ts +38 -73
- package/src/commands/teleport.ts +122 -12
- package/src/commands/upgrade.ts +8 -2
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +42 -130
- package/src/index.ts +1 -7
- package/src/lib/__tests__/docker.test.ts +53 -35
- package/src/lib/__tests__/local-runtime-client.test.ts +186 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +235 -0
- package/src/lib/__tests__/runtime-url.test.ts +39 -1
- package/src/lib/assistant-client.ts +13 -5
- package/src/lib/assistant-config.ts +0 -25
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/client-identity.ts +9 -5
- package/src/lib/docker.ts +6 -267
- package/src/lib/environments/paths.ts +20 -0
- package/src/lib/guardian-token.ts +56 -6
- package/src/lib/hatch-local.ts +3 -26
- package/src/lib/local-runtime-client.ts +82 -1
- package/src/lib/local.ts +9 -7
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +100 -1
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +22 -0
- package/src/lib/statefulset.ts +375 -0
- package/src/lib/upgrade-lifecycle.ts +97 -1
- package/src/commands/pair.ts +0 -212
|
@@ -196,6 +196,15 @@ const localRuntimeImportFromGcsMock = spyOn(
|
|
|
196
196
|
"localRuntimeImportFromGcs",
|
|
197
197
|
).mockResolvedValue({ jobId: "local-import-job-1" });
|
|
198
198
|
|
|
199
|
+
// Default to a fixed version string. Tests that exercise the version-gate
|
|
200
|
+
// surface override this mock per-case to assert the value flows from the
|
|
201
|
+
// target runtime's `/v1/identity` (NOT from `cliPkg.version`) into the
|
|
202
|
+
// download signed-URL request.
|
|
203
|
+
const localRuntimeIdentityMock = spyOn(
|
|
204
|
+
localRuntimeClient,
|
|
205
|
+
"localRuntimeIdentity",
|
|
206
|
+
).mockResolvedValue({ version: "0.6.5" });
|
|
207
|
+
|
|
199
208
|
const localRuntimePollJobStatusMock = spyOn(
|
|
200
209
|
localRuntimeClient,
|
|
201
210
|
"localRuntimePollJobStatus",
|
|
@@ -297,6 +306,7 @@ afterAll(() => {
|
|
|
297
306
|
computeDeviceIdMock.mockRestore();
|
|
298
307
|
localRuntimeExportToGcsMock.mockRestore();
|
|
299
308
|
localRuntimeImportFromGcsMock.mockRestore();
|
|
309
|
+
localRuntimeIdentityMock.mockRestore();
|
|
300
310
|
localRuntimePollJobStatusMock.mockRestore();
|
|
301
311
|
rmSync(testDir, { recursive: true, force: true });
|
|
302
312
|
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
@@ -446,6 +456,8 @@ beforeEach(() => {
|
|
|
446
456
|
localRuntimeImportFromGcsMock.mockResolvedValue({
|
|
447
457
|
jobId: "local-import-job-1",
|
|
448
458
|
});
|
|
459
|
+
localRuntimeIdentityMock.mockReset();
|
|
460
|
+
localRuntimeIdentityMock.mockResolvedValue({ version: "0.6.5" });
|
|
449
461
|
localRuntimePollJobStatusMock.mockReset();
|
|
450
462
|
localRuntimePollJobStatusMock.mockImplementation(defaultLocalRuntimePollImpl);
|
|
451
463
|
|
|
@@ -850,7 +862,11 @@ describe("unified GCS flow — four directions", () => {
|
|
|
850
862
|
// Signed-URL request for upload — pinned to the platform target's URL
|
|
851
863
|
// so upload and download land on the same platform.
|
|
852
864
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
853
|
-
expect.objectContaining({
|
|
865
|
+
expect.objectContaining({
|
|
866
|
+
operation: "upload",
|
|
867
|
+
minRuntimeVersion: "0.6.5",
|
|
868
|
+
maxRuntimeVersion: null,
|
|
869
|
+
}),
|
|
854
870
|
"platform-token",
|
|
855
871
|
"https://platform.vellum.ai",
|
|
856
872
|
);
|
|
@@ -937,7 +953,11 @@ describe("unified GCS flow — four directions", () => {
|
|
|
937
953
|
// Platform side: requested an upload URL, kicked off a runtime export to
|
|
938
954
|
// GCS, and polled the unified job status.
|
|
939
955
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
940
|
-
expect.objectContaining({
|
|
956
|
+
expect.objectContaining({
|
|
957
|
+
operation: "upload",
|
|
958
|
+
minRuntimeVersion: "0.6.5",
|
|
959
|
+
maxRuntimeVersion: null,
|
|
960
|
+
}),
|
|
941
961
|
"platform-token",
|
|
942
962
|
"https://platform.vellum.ai",
|
|
943
963
|
);
|
|
@@ -974,14 +994,24 @@ describe("unified GCS flow — four directions", () => {
|
|
|
974
994
|
// platform's bundle_key. The URL must target the SOURCE platform
|
|
975
995
|
// (where the bundle was written) — pinned so a lockfile change
|
|
976
996
|
// can't split upload and download across instances.
|
|
997
|
+
//
|
|
998
|
+
// `targetRuntimeVersion` MUST come from the target runtime's
|
|
999
|
+
// `/v1/identity` (mocked to "0.6.5"), NOT from the CLI's package
|
|
1000
|
+
// version. The local target's daemon can be on a different version
|
|
1001
|
+
// than the CLI orchestrating the teleport.
|
|
977
1002
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
978
|
-
{
|
|
1003
|
+
expect.objectContaining({
|
|
979
1004
|
operation: "download",
|
|
980
1005
|
bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
|
|
981
|
-
|
|
1006
|
+
targetRuntimeVersion: "0.6.5",
|
|
1007
|
+
}),
|
|
982
1008
|
"platform-token",
|
|
983
1009
|
"https://platform.vellum.ai",
|
|
984
1010
|
);
|
|
1011
|
+
expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
|
|
1012
|
+
expect.objectContaining({ cloud: "local" }),
|
|
1013
|
+
expect.any(String),
|
|
1014
|
+
);
|
|
985
1015
|
|
|
986
1016
|
// Runtime import-from-gcs was kicked off with that URL.
|
|
987
1017
|
expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
|
|
@@ -1032,21 +1062,32 @@ describe("unified GCS flow — four directions", () => {
|
|
|
1032
1062
|
// lives in one place end-to-end. For local→docker neither side is
|
|
1033
1063
|
// platform, so we default to getPlatformUrl() (resolved once).
|
|
1034
1064
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1035
|
-
expect.objectContaining({
|
|
1065
|
+
expect.objectContaining({
|
|
1066
|
+
operation: "upload",
|
|
1067
|
+
minRuntimeVersion: "0.6.5",
|
|
1068
|
+
maxRuntimeVersion: null,
|
|
1069
|
+
}),
|
|
1036
1070
|
"platform-token",
|
|
1037
1071
|
"https://platform.vellum.ai",
|
|
1038
1072
|
);
|
|
1039
1073
|
expect(localRuntimeExportToGcsMock).toHaveBeenCalled();
|
|
1040
1074
|
|
|
1041
1075
|
// Import: download-URL for the docker target, then runtime import.
|
|
1076
|
+
// targetRuntimeVersion comes from the docker target's runtime
|
|
1077
|
+
// identity (mocked to "0.6.5"), not from cliPkg.version.
|
|
1042
1078
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1043
|
-
{
|
|
1079
|
+
expect.objectContaining({
|
|
1044
1080
|
operation: "download",
|
|
1045
1081
|
bundleKey: "bundle-key-123",
|
|
1046
|
-
|
|
1082
|
+
targetRuntimeVersion: "0.6.5",
|
|
1083
|
+
}),
|
|
1047
1084
|
"platform-token",
|
|
1048
1085
|
"https://platform.vellum.ai",
|
|
1049
1086
|
);
|
|
1087
|
+
expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
|
|
1088
|
+
expect.objectContaining({ cloud: "docker" }),
|
|
1089
|
+
expect.any(String),
|
|
1090
|
+
);
|
|
1050
1091
|
expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
|
|
1051
1092
|
expect.objectContaining({
|
|
1052
1093
|
cloud: "docker",
|
|
@@ -1099,7 +1140,11 @@ describe("unified GCS flow — four directions", () => {
|
|
|
1099
1140
|
// Export leg: upload-URL (pinned to the same platform as import),
|
|
1100
1141
|
// then runtime export.
|
|
1101
1142
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1102
|
-
expect.objectContaining({
|
|
1143
|
+
expect.objectContaining({
|
|
1144
|
+
operation: "upload",
|
|
1145
|
+
minRuntimeVersion: "0.6.5",
|
|
1146
|
+
maxRuntimeVersion: null,
|
|
1147
|
+
}),
|
|
1103
1148
|
"platform-token",
|
|
1104
1149
|
"https://platform.vellum.ai",
|
|
1105
1150
|
);
|
|
@@ -1163,7 +1208,11 @@ describe("signed-URL request targets the bundle-owning platform", () => {
|
|
|
1163
1208
|
// The signed-URL request for upload MUST target the existing
|
|
1164
1209
|
// platform assistant's runtimeUrl, not the default platform URL.
|
|
1165
1210
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1166
|
-
expect.objectContaining({
|
|
1211
|
+
expect.objectContaining({
|
|
1212
|
+
operation: "upload",
|
|
1213
|
+
minRuntimeVersion: "0.6.5",
|
|
1214
|
+
maxRuntimeVersion: null,
|
|
1215
|
+
}),
|
|
1167
1216
|
"platform-token",
|
|
1168
1217
|
"https://staging-platform.vellum.ai",
|
|
1169
1218
|
);
|
|
@@ -1224,8 +1273,14 @@ describe("signed-URL request targets the bundle-owning platform", () => {
|
|
|
1224
1273
|
|
|
1225
1274
|
// The download URL must be requested from the SOURCE platform (where
|
|
1226
1275
|
// the bundle was written by the runtime export), not the default.
|
|
1276
|
+
// targetRuntimeVersion comes from the local target's `/v1/identity`
|
|
1277
|
+
// (mocked to "0.6.5"), not from cliPkg.version.
|
|
1227
1278
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1228
|
-
{
|
|
1279
|
+
expect.objectContaining({
|
|
1280
|
+
operation: "download",
|
|
1281
|
+
bundleKey: "dev-bundle-key",
|
|
1282
|
+
targetRuntimeVersion: "0.6.5",
|
|
1283
|
+
}),
|
|
1229
1284
|
"platform-token",
|
|
1230
1285
|
"https://dev-platform.vellum.ai",
|
|
1231
1286
|
);
|
|
@@ -1524,6 +1579,274 @@ describe("MigrationInProgressError handling", () => {
|
|
|
1524
1579
|
});
|
|
1525
1580
|
});
|
|
1526
1581
|
|
|
1582
|
+
// ---------------------------------------------------------------------------
|
|
1583
|
+
// VersionMismatchError handling — 422 from platformRequestSignedUrl on the
|
|
1584
|
+
// download leg is terminal: surface a friendly message + exit 1, no retry.
|
|
1585
|
+
// ---------------------------------------------------------------------------
|
|
1586
|
+
|
|
1587
|
+
describe("VersionMismatchError handling", () => {
|
|
1588
|
+
test("local target: 422 on download signed-URL exits 1 with friendly message", async () => {
|
|
1589
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1590
|
+
|
|
1591
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1592
|
+
cloud: "vellum",
|
|
1593
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1594
|
+
});
|
|
1595
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1596
|
+
|
|
1597
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1598
|
+
if (name === "my-platform") return platformEntry;
|
|
1599
|
+
if (name === "my-local") return localEntry;
|
|
1600
|
+
return null;
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1604
|
+
jobId: "platform-export-job-1",
|
|
1605
|
+
type: "export",
|
|
1606
|
+
status: "complete",
|
|
1607
|
+
bundleKey: "platform-bundle-key-abc",
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
// Upload signed-URL succeeds; download signed-URL throws version-mismatch.
|
|
1611
|
+
platformRequestSignedUrlMock.mockImplementation(async (params) => {
|
|
1612
|
+
if (params.operation === "download") {
|
|
1613
|
+
throw new platformClient.VersionMismatchError(
|
|
1614
|
+
{ min_runtime_version: "99.0.0", max_runtime_version: null },
|
|
1615
|
+
"0.7.1",
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
return {
|
|
1619
|
+
url: "https://storage.googleapis.com/bucket/signed-upload",
|
|
1620
|
+
bundleKey: params.bundleKey ?? "bundle-key-123",
|
|
1621
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
1622
|
+
};
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
const restoreFetch = installTrackingFetch();
|
|
1626
|
+
try {
|
|
1627
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1628
|
+
|
|
1629
|
+
// Friendly message uses the prefix from VersionMismatchError.formatMessage.
|
|
1630
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1631
|
+
expect.stringContaining("Cannot import: bundle requires runtime"),
|
|
1632
|
+
);
|
|
1633
|
+
|
|
1634
|
+
// Terminal: no retry of the download signed-URL request.
|
|
1635
|
+
const downloadCalls = platformRequestSignedUrlMock.mock.calls.filter(
|
|
1636
|
+
(c) => (c[0] as { operation: string }).operation === "download",
|
|
1637
|
+
);
|
|
1638
|
+
expect(downloadCalls).toHaveLength(1);
|
|
1639
|
+
|
|
1640
|
+
// Runtime import-from-gcs must NOT be kicked off after the 422.
|
|
1641
|
+
expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
|
|
1642
|
+
} finally {
|
|
1643
|
+
restoreFetch();
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
test("docker target: 422 on download signed-URL exits 1 with friendly message", async () => {
|
|
1648
|
+
setArgv("--from", "my-local", "--docker");
|
|
1649
|
+
|
|
1650
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1651
|
+
const dockerEntry = makeEntry("new-docker", {
|
|
1652
|
+
cloud: "docker",
|
|
1653
|
+
runtimeUrl: "http://localhost:7822",
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
1657
|
+
name === "my-local" ? localEntry : null,
|
|
1658
|
+
);
|
|
1659
|
+
|
|
1660
|
+
loadAllAssistantsMock.mockImplementation(() => {
|
|
1661
|
+
if (hatchDockerMock.mock.calls.length > 0) {
|
|
1662
|
+
return [localEntry, dockerEntry];
|
|
1663
|
+
}
|
|
1664
|
+
return [localEntry];
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
platformRequestSignedUrlMock.mockImplementation(async (params) => {
|
|
1668
|
+
if (params.operation === "download") {
|
|
1669
|
+
throw new platformClient.VersionMismatchError(
|
|
1670
|
+
{ min_runtime_version: "99.0.0", max_runtime_version: null },
|
|
1671
|
+
"0.7.1",
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
return {
|
|
1675
|
+
url: "https://storage.googleapis.com/bucket/signed-upload",
|
|
1676
|
+
bundleKey: params.bundleKey ?? "bundle-key-123",
|
|
1677
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
1678
|
+
};
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
const restoreFetch = installTrackingFetch();
|
|
1682
|
+
try {
|
|
1683
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1684
|
+
|
|
1685
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1686
|
+
expect.stringContaining("Cannot import: bundle requires runtime"),
|
|
1687
|
+
);
|
|
1688
|
+
|
|
1689
|
+
expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
|
|
1690
|
+
} finally {
|
|
1691
|
+
restoreFetch();
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
test("non-VersionMismatchError on download signed-URL re-raises", async () => {
|
|
1696
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1697
|
+
|
|
1698
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1699
|
+
cloud: "vellum",
|
|
1700
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1701
|
+
});
|
|
1702
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1703
|
+
|
|
1704
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1705
|
+
if (name === "my-platform") return platformEntry;
|
|
1706
|
+
if (name === "my-local") return localEntry;
|
|
1707
|
+
return null;
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1711
|
+
jobId: "platform-export-job-1",
|
|
1712
|
+
type: "export",
|
|
1713
|
+
status: "complete",
|
|
1714
|
+
bundleKey: "platform-bundle-key-abc",
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
platformRequestSignedUrlMock.mockImplementation(async (params) => {
|
|
1718
|
+
if (params.operation === "download") {
|
|
1719
|
+
throw new Error("network blew up");
|
|
1720
|
+
}
|
|
1721
|
+
return {
|
|
1722
|
+
url: "https://storage.googleapis.com/bucket/signed-upload",
|
|
1723
|
+
bundleKey: params.bundleKey ?? "bundle-key-123",
|
|
1724
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
1725
|
+
};
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
const restoreFetch = installTrackingFetch();
|
|
1729
|
+
try {
|
|
1730
|
+
await expect(teleport()).rejects.toThrow("network blew up");
|
|
1731
|
+
} finally {
|
|
1732
|
+
restoreFetch();
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
// ---------------------------------------------------------------------------
|
|
1738
|
+
// Target-runtime version fetch — the download signed-URL request must use
|
|
1739
|
+
// the TARGET runtime's reported version, not the orchestrating CLI's
|
|
1740
|
+
// version (which can diverge when the target was upgraded independently).
|
|
1741
|
+
// ---------------------------------------------------------------------------
|
|
1742
|
+
|
|
1743
|
+
describe("target runtime version fetch", () => {
|
|
1744
|
+
test("local target: targetRuntimeVersion comes from /v1/identity, not cliPkg", async () => {
|
|
1745
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1746
|
+
|
|
1747
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1748
|
+
cloud: "vellum",
|
|
1749
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1750
|
+
});
|
|
1751
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1752
|
+
|
|
1753
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1754
|
+
if (name === "my-platform") return platformEntry;
|
|
1755
|
+
if (name === "my-local") return localEntry;
|
|
1756
|
+
return null;
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1760
|
+
jobId: "platform-export-job-1",
|
|
1761
|
+
type: "export",
|
|
1762
|
+
status: "complete",
|
|
1763
|
+
bundleKey: "platform-bundle-key-abc",
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
// Choose a version unlikely to match cliPkg.version so a regression
|
|
1767
|
+
// would be obvious.
|
|
1768
|
+
localRuntimeIdentityMock.mockResolvedValue({ version: "1.2.3-runtime" });
|
|
1769
|
+
|
|
1770
|
+
const restoreFetch = installTrackingFetch();
|
|
1771
|
+
try {
|
|
1772
|
+
await teleport();
|
|
1773
|
+
|
|
1774
|
+
const downloadCall = platformRequestSignedUrlMock.mock.calls.find(
|
|
1775
|
+
(c) => (c[0] as { operation: string }).operation === "download",
|
|
1776
|
+
);
|
|
1777
|
+
expect(downloadCall).toBeDefined();
|
|
1778
|
+
expect(downloadCall![0]).toMatchObject({
|
|
1779
|
+
targetRuntimeVersion: "1.2.3-runtime",
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// Identity was fetched against the TARGET (local) entry, not the
|
|
1783
|
+
// platform source.
|
|
1784
|
+
expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
|
|
1785
|
+
expect.objectContaining({
|
|
1786
|
+
cloud: "local",
|
|
1787
|
+
assistantId: "my-local",
|
|
1788
|
+
}),
|
|
1789
|
+
expect.any(String),
|
|
1790
|
+
);
|
|
1791
|
+
} finally {
|
|
1792
|
+
restoreFetch();
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
test("identity fetch failure aborts before requesting download URL", async () => {
|
|
1797
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
1798
|
+
|
|
1799
|
+
const platformEntry = makeEntry("my-platform", {
|
|
1800
|
+
cloud: "vellum",
|
|
1801
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
1802
|
+
});
|
|
1803
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
1804
|
+
|
|
1805
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
1806
|
+
if (name === "my-platform") return platformEntry;
|
|
1807
|
+
if (name === "my-local") return localEntry;
|
|
1808
|
+
return null;
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
1812
|
+
jobId: "platform-export-job-1",
|
|
1813
|
+
type: "export",
|
|
1814
|
+
status: "complete",
|
|
1815
|
+
bundleKey: "platform-bundle-key-abc",
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
// Identity is fetched twice in this flow: once on the source (platform)
|
|
1819
|
+
// for `min_runtime_version` on the upload signed-URL, then once on the
|
|
1820
|
+
// target (local) for `targetRuntimeVersion` on the download signed-URL.
|
|
1821
|
+
// Succeed on the source call so the flow gets as far as the target
|
|
1822
|
+
// identity fetch — that's the failure this test is exercising.
|
|
1823
|
+
localRuntimeIdentityMock.mockImplementation(async (entry) => {
|
|
1824
|
+
if (entry.cloud === "local") {
|
|
1825
|
+
throw new Error("Local runtime identity failed (502): bad gateway");
|
|
1826
|
+
}
|
|
1827
|
+
return { version: "0.6.5" };
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
const restoreFetch = installTrackingFetch();
|
|
1831
|
+
try {
|
|
1832
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
1833
|
+
|
|
1834
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1835
|
+
expect.stringContaining("Could not read target runtime version"),
|
|
1836
|
+
);
|
|
1837
|
+
|
|
1838
|
+
// No download signed-URL request was made — we aborted before that.
|
|
1839
|
+
const downloadCalls = platformRequestSignedUrlMock.mock.calls.filter(
|
|
1840
|
+
(c) => (c[0] as { operation: string }).operation === "download",
|
|
1841
|
+
);
|
|
1842
|
+
expect(downloadCalls).toHaveLength(0);
|
|
1843
|
+
expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
|
|
1844
|
+
} finally {
|
|
1845
|
+
restoreFetch();
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1527
1850
|
// ---------------------------------------------------------------------------
|
|
1528
1851
|
// Dry-run behavior
|
|
1529
1852
|
// ---------------------------------------------------------------------------
|
|
@@ -1878,6 +2201,9 @@ describe("platform credential injection", () => {
|
|
|
1878
2201
|
"device-id-123",
|
|
1879
2202
|
"my-local",
|
|
1880
2203
|
"cli",
|
|
2204
|
+
undefined, // assistantVersion (gateway unreachable in test)
|
|
2205
|
+
expect.any(String), // platformUrl from getPlatformUrl()
|
|
2206
|
+
undefined, // ingressUrl (gateway unreachable in test)
|
|
1881
2207
|
);
|
|
1882
2208
|
expect(injectCredentialsIntoAssistantMock).toHaveBeenCalledWith({
|
|
1883
2209
|
gatewayUrl: "http://localhost:7821",
|
|
@@ -2148,6 +2474,185 @@ describe("auth + transient-error resilience", () => {
|
|
|
2148
2474
|
});
|
|
2149
2475
|
});
|
|
2150
2476
|
|
|
2477
|
+
// ---------------------------------------------------------------------------
|
|
2478
|
+
// Source-runtime version is sourced from the daemon, not the CLI
|
|
2479
|
+
// (Codex P1 regression guard for PR #29436)
|
|
2480
|
+
// ---------------------------------------------------------------------------
|
|
2481
|
+
|
|
2482
|
+
describe("upload signed-URL records source runtime version (not CLI version)", () => {
|
|
2483
|
+
test("local source: identity is fetched BEFORE the upload signed-URL request", async () => {
|
|
2484
|
+
setArgv("--from", "my-local", "--platform");
|
|
2485
|
+
|
|
2486
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2487
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
2488
|
+
name === "my-local" ? localEntry : null,
|
|
2489
|
+
);
|
|
2490
|
+
|
|
2491
|
+
// Distinguish the daemon's version from anything else hardcoded.
|
|
2492
|
+
localRuntimeIdentityMock.mockResolvedValue({ version: "0.5.9" });
|
|
2493
|
+
|
|
2494
|
+
// Order tracker: capture which mock was called first.
|
|
2495
|
+
const callOrder: string[] = [];
|
|
2496
|
+
localRuntimeIdentityMock.mockImplementationOnce(async () => {
|
|
2497
|
+
callOrder.push("identity");
|
|
2498
|
+
return { version: "0.5.9" };
|
|
2499
|
+
});
|
|
2500
|
+
platformRequestSignedUrlMock.mockImplementationOnce(async (params) => {
|
|
2501
|
+
callOrder.push("signed-url");
|
|
2502
|
+
return {
|
|
2503
|
+
url: "https://storage.googleapis.com/bucket/signed-upload",
|
|
2504
|
+
bundleKey: params.bundleKey ?? "bundle-key-123",
|
|
2505
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
2506
|
+
};
|
|
2507
|
+
});
|
|
2508
|
+
|
|
2509
|
+
const restoreFetch = installTrackingFetch();
|
|
2510
|
+
try {
|
|
2511
|
+
await teleport();
|
|
2512
|
+
|
|
2513
|
+
expect(callOrder[0]).toBe("identity");
|
|
2514
|
+
expect(callOrder[1]).toBe("signed-url");
|
|
2515
|
+
|
|
2516
|
+
// Upload request stamps minRuntimeVersion with the daemon's version,
|
|
2517
|
+
// NOT cliPkg.version.
|
|
2518
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
2519
|
+
expect.objectContaining({
|
|
2520
|
+
operation: "upload",
|
|
2521
|
+
minRuntimeVersion: "0.5.9",
|
|
2522
|
+
maxRuntimeVersion: null,
|
|
2523
|
+
}),
|
|
2524
|
+
"platform-token",
|
|
2525
|
+
"https://platform.vellum.ai",
|
|
2526
|
+
);
|
|
2527
|
+
} finally {
|
|
2528
|
+
restoreFetch();
|
|
2529
|
+
}
|
|
2530
|
+
});
|
|
2531
|
+
|
|
2532
|
+
test("local source: identity fetch failure aborts before signed-URL request", async () => {
|
|
2533
|
+
setArgv("--from", "my-local", "--platform");
|
|
2534
|
+
|
|
2535
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2536
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
2537
|
+
name === "my-local" ? localEntry : null,
|
|
2538
|
+
);
|
|
2539
|
+
|
|
2540
|
+
localRuntimeIdentityMock.mockRejectedValue(
|
|
2541
|
+
new Error("Failed to fetch runtime identity: 503 Service Unavailable"),
|
|
2542
|
+
);
|
|
2543
|
+
|
|
2544
|
+
const restoreFetch = installTrackingFetch();
|
|
2545
|
+
try {
|
|
2546
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
2547
|
+
|
|
2548
|
+
// Must NOT have proceeded to signed URL or export.
|
|
2549
|
+
expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
|
|
2550
|
+
expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
|
|
2551
|
+
|
|
2552
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
2553
|
+
expect.stringContaining("Could not fetch runtime identity"),
|
|
2554
|
+
);
|
|
2555
|
+
} finally {
|
|
2556
|
+
restoreFetch();
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
|
|
2560
|
+
test("platform source: managed runtime's identity is fetched and recorded", async () => {
|
|
2561
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
2562
|
+
|
|
2563
|
+
const platformEntry = makeEntry("my-platform", {
|
|
2564
|
+
cloud: "vellum",
|
|
2565
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2566
|
+
});
|
|
2567
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2568
|
+
|
|
2569
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2570
|
+
if (name === "my-platform") return platformEntry;
|
|
2571
|
+
if (name === "my-local") return localEntry;
|
|
2572
|
+
return null;
|
|
2573
|
+
});
|
|
2574
|
+
|
|
2575
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
2576
|
+
jobId: "platform-export-job-1",
|
|
2577
|
+
type: "export",
|
|
2578
|
+
status: "complete",
|
|
2579
|
+
bundleKey: "platform-bundle",
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2582
|
+
localRuntimeIdentityMock.mockResolvedValue({ version: "0.7.2" });
|
|
2583
|
+
|
|
2584
|
+
const restoreFetch = installTrackingFetch();
|
|
2585
|
+
try {
|
|
2586
|
+
await teleport();
|
|
2587
|
+
|
|
2588
|
+
// Identity was fetched against the platform-managed runtime entry
|
|
2589
|
+
// (cloud=vellum) with the platform token — not via guardian token.
|
|
2590
|
+
expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
|
|
2591
|
+
expect.objectContaining({
|
|
2592
|
+
cloud: "vellum",
|
|
2593
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2594
|
+
assistantId: "my-platform",
|
|
2595
|
+
}),
|
|
2596
|
+
"platform-token",
|
|
2597
|
+
);
|
|
2598
|
+
|
|
2599
|
+
// The recorded version came from the platform runtime, not the CLI.
|
|
2600
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
2601
|
+
expect.objectContaining({
|
|
2602
|
+
operation: "upload",
|
|
2603
|
+
minRuntimeVersion: "0.7.2",
|
|
2604
|
+
maxRuntimeVersion: null,
|
|
2605
|
+
}),
|
|
2606
|
+
"platform-token",
|
|
2607
|
+
"https://platform.vellum.ai",
|
|
2608
|
+
);
|
|
2609
|
+
} finally {
|
|
2610
|
+
restoreFetch();
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
test("platform source: identity fetch failure aborts before signed-URL request", async () => {
|
|
2615
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
2616
|
+
|
|
2617
|
+
const platformEntry = makeEntry("my-platform", {
|
|
2618
|
+
cloud: "vellum",
|
|
2619
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2620
|
+
});
|
|
2621
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2622
|
+
|
|
2623
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2624
|
+
if (name === "my-platform") return platformEntry;
|
|
2625
|
+
if (name === "my-local") return localEntry;
|
|
2626
|
+
return null;
|
|
2627
|
+
});
|
|
2628
|
+
|
|
2629
|
+
localRuntimeIdentityMock.mockRejectedValue(
|
|
2630
|
+
new Error("Failed to fetch runtime identity: 502 Bad Gateway"),
|
|
2631
|
+
);
|
|
2632
|
+
|
|
2633
|
+
const restoreFetch = installTrackingFetch();
|
|
2634
|
+
try {
|
|
2635
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
2636
|
+
|
|
2637
|
+
// Signed-URL upload request must not have happened.
|
|
2638
|
+
const uploadCalls = platformRequestSignedUrlMock.mock.calls.filter(
|
|
2639
|
+
(call: unknown[]) =>
|
|
2640
|
+
(call[0] as { operation: string }).operation === "upload",
|
|
2641
|
+
);
|
|
2642
|
+
expect(uploadCalls.length).toBe(0);
|
|
2643
|
+
|
|
2644
|
+
// Runtime export was never kicked off.
|
|
2645
|
+
expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
|
|
2646
|
+
|
|
2647
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
2648
|
+
expect.stringContaining("Could not fetch runtime identity"),
|
|
2649
|
+
);
|
|
2650
|
+
} finally {
|
|
2651
|
+
restoreFetch();
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
});
|
|
2655
|
+
|
|
2151
2656
|
// ---------------------------------------------------------------------------
|
|
2152
2657
|
// Misc: legacy --to deprecation
|
|
2153
2658
|
// ---------------------------------------------------------------------------
|
package/src/commands/backup.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { pollJobUntilDone } from "../lib/job-polling.js";
|
|
|
9
9
|
import {
|
|
10
10
|
MigrationInProgressError,
|
|
11
11
|
localRuntimeExportToGcs,
|
|
12
|
+
localRuntimeIdentity,
|
|
12
13
|
localRuntimePollJobStatus,
|
|
13
14
|
} from "../lib/local-runtime-client.js";
|
|
14
15
|
import {
|
|
@@ -232,9 +233,30 @@ async function backupPlatform(
|
|
|
232
233
|
// signed-download request.
|
|
233
234
|
let exportPlatformToken = platformToken;
|
|
234
235
|
|
|
236
|
+
// Step 0 — Ask the source runtime which version it's running. The bundle
|
|
237
|
+
// is produced by the daemon (not the CLI), and the CLI version can drift
|
|
238
|
+
// from the daemon version, so the daemon's version is the authoritative
|
|
239
|
+
// value to record as the bundle's `min_runtime_version`. Stamping with
|
|
240
|
+
// `cliPkg.version` here would record an inaccurate compatibility band on
|
|
241
|
+
// the signed-URL request.
|
|
242
|
+
let runtimeIdentity: { version: string };
|
|
243
|
+
try {
|
|
244
|
+
runtimeIdentity = await localRuntimeIdentity(entry, exportPlatformToken);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
247
|
+
console.error(
|
|
248
|
+
`Error: Could not fetch runtime identity from '${name}': ${msg}`,
|
|
249
|
+
);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
235
253
|
// Step 1 — Request a signed upload URL.
|
|
236
254
|
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
237
|
-
{
|
|
255
|
+
{
|
|
256
|
+
operation: "upload",
|
|
257
|
+
minRuntimeVersion: runtimeIdentity.version,
|
|
258
|
+
maxRuntimeVersion: null,
|
|
259
|
+
},
|
|
238
260
|
exportPlatformToken,
|
|
239
261
|
platformUrl,
|
|
240
262
|
);
|
|
@@ -289,8 +311,19 @@ async function backupPlatform(
|
|
|
289
311
|
// poll-loop 401 refresh doesn't get clobbered here — otherwise a long
|
|
290
312
|
// export that recovered mid-poll via re-auth would still 401 on the
|
|
291
313
|
// download-URL request and abort an otherwise successful run.
|
|
314
|
+
//
|
|
315
|
+
// We deliberately do NOT send `targetRuntimeVersion` here. This flow
|
|
316
|
+
// saves the bundle to disk for offline storage; there is no target
|
|
317
|
+
// runtime to gate against, and the user can later restore the file
|
|
318
|
+
// into any compatible runtime. Sending the CLI's version would
|
|
319
|
+
// incorrectly block older CLIs from backing up newer assistants.
|
|
320
|
+
// The platform treats `target_runtime_version` as optional and skips
|
|
321
|
+
// the version check when it's omitted.
|
|
292
322
|
const { url: bundleUrl } = await platformRequestSignedUrl(
|
|
293
|
-
{
|
|
323
|
+
{
|
|
324
|
+
operation: "download",
|
|
325
|
+
bundleKey,
|
|
326
|
+
},
|
|
294
327
|
exportPlatformToken,
|
|
295
328
|
platformUrl,
|
|
296
329
|
);
|