@vellumai/cli 0.7.0 → 0.7.2
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/README.md +49 -0
- package/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +591 -0
- package/src/__tests__/config-utils.test.ts +35 -48
- package/src/__tests__/teleport.test.ts +597 -37
- package/src/commands/backup.ts +149 -70
- package/src/commands/client.ts +56 -14
- package/src/commands/events.ts +3 -0
- package/src/commands/exec.ts +34 -12
- package/src/commands/hatch.ts +3 -7
- package/src/commands/login.ts +15 -33
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +41 -6
- package/src/commands/restore.ts +32 -47
- package/src/commands/setup.ts +38 -73
- package/src/commands/ssh.ts +2 -5
- package/src/commands/teleport.ts +148 -34
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +114 -7
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +65 -129
- package/src/index.ts +2 -13
- package/src/lib/__tests__/docker.test.ts +50 -32
- package/src/lib/__tests__/local-runtime-client.test.ts +308 -25
- package/src/lib/__tests__/platform-client-signed-url.test.ts +237 -2
- package/src/lib/__tests__/runtime-url.test.ts +125 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +18 -26
- package/src/lib/assistant-config.ts +34 -41
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +1 -1
- package/src/lib/config-utils.ts +1 -97
- package/src/lib/docker-statefulset.ts +381 -0
- package/src/lib/docker.ts +8 -247
- package/src/lib/guardian-token.ts +56 -6
- package/src/lib/hatch-local.ts +3 -26
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/local-runtime-client.ts +162 -28
- package/src/lib/local.ts +35 -64
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +97 -221
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +52 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +127 -48
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/upgrade-lifecycle.ts +65 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/commands/pair.ts +0 -212
|
@@ -91,14 +91,6 @@ const hatchAssistantMock = spyOn(
|
|
|
91
91
|
reusedExisting: false,
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
const platformInitiateExportMock = spyOn(
|
|
95
|
-
platformClient,
|
|
96
|
-
"platformInitiateExport",
|
|
97
|
-
).mockResolvedValue({
|
|
98
|
-
jobId: "platform-export-job-1",
|
|
99
|
-
status: "pending",
|
|
100
|
-
});
|
|
101
|
-
|
|
102
94
|
const platformPollJobStatusMock = spyOn(
|
|
103
95
|
platformClient,
|
|
104
96
|
"platformPollJobStatus",
|
|
@@ -204,6 +196,15 @@ const localRuntimeImportFromGcsMock = spyOn(
|
|
|
204
196
|
"localRuntimeImportFromGcs",
|
|
205
197
|
).mockResolvedValue({ jobId: "local-import-job-1" });
|
|
206
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
|
+
|
|
207
208
|
const localRuntimePollJobStatusMock = spyOn(
|
|
208
209
|
localRuntimeClient,
|
|
209
210
|
"localRuntimePollJobStatus",
|
|
@@ -294,7 +295,6 @@ afterAll(() => {
|
|
|
294
295
|
getPlatformUrlMock.mockRestore();
|
|
295
296
|
hatchAssistantMock.mockRestore();
|
|
296
297
|
checkExistingPlatformAssistantMock.mockRestore();
|
|
297
|
-
platformInitiateExportMock.mockRestore();
|
|
298
298
|
platformPollJobStatusMock.mockRestore();
|
|
299
299
|
platformRequestSignedUrlMock.mockRestore();
|
|
300
300
|
platformImportBundleFromGcsMock.mockRestore();
|
|
@@ -306,6 +306,7 @@ afterAll(() => {
|
|
|
306
306
|
computeDeviceIdMock.mockRestore();
|
|
307
307
|
localRuntimeExportToGcsMock.mockRestore();
|
|
308
308
|
localRuntimeImportFromGcsMock.mockRestore();
|
|
309
|
+
localRuntimeIdentityMock.mockRestore();
|
|
309
310
|
localRuntimePollJobStatusMock.mockRestore();
|
|
310
311
|
rmSync(testDir, { recursive: true, force: true });
|
|
311
312
|
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
@@ -319,7 +320,7 @@ let consoleErrorSpy: ReturnType<typeof spyOn>;
|
|
|
319
320
|
let fetchCalls: Array<{ url: string; body: unknown }>;
|
|
320
321
|
|
|
321
322
|
function defaultLocalRuntimePollImpl(
|
|
322
|
-
|
|
323
|
+
_entry: unknown,
|
|
323
324
|
_token: string,
|
|
324
325
|
jobId: string,
|
|
325
326
|
): Promise<{
|
|
@@ -378,11 +379,6 @@ beforeEach(() => {
|
|
|
378
379
|
},
|
|
379
380
|
reusedExisting: false,
|
|
380
381
|
});
|
|
381
|
-
platformInitiateExportMock.mockReset();
|
|
382
|
-
platformInitiateExportMock.mockResolvedValue({
|
|
383
|
-
jobId: "platform-export-job-1",
|
|
384
|
-
status: "pending",
|
|
385
|
-
});
|
|
386
382
|
platformPollJobStatusMock.mockReset();
|
|
387
383
|
platformPollJobStatusMock.mockResolvedValue({
|
|
388
384
|
jobId: "platform-job-1",
|
|
@@ -460,6 +456,8 @@ beforeEach(() => {
|
|
|
460
456
|
localRuntimeImportFromGcsMock.mockResolvedValue({
|
|
461
457
|
jobId: "local-import-job-1",
|
|
462
458
|
});
|
|
459
|
+
localRuntimeIdentityMock.mockReset();
|
|
460
|
+
localRuntimeIdentityMock.mockResolvedValue({ version: "0.6.5" });
|
|
463
461
|
localRuntimePollJobStatusMock.mockReset();
|
|
464
462
|
localRuntimePollJobStatusMock.mockImplementation(defaultLocalRuntimePollImpl);
|
|
465
463
|
|
|
@@ -864,14 +862,23 @@ describe("unified GCS flow — four directions", () => {
|
|
|
864
862
|
// Signed-URL request for upload — pinned to the platform target's URL
|
|
865
863
|
// so upload and download land on the same platform.
|
|
866
864
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
867
|
-
expect.objectContaining({
|
|
865
|
+
expect.objectContaining({
|
|
866
|
+
operation: "upload",
|
|
867
|
+
minRuntimeVersion: "0.6.5",
|
|
868
|
+
maxRuntimeVersion: null,
|
|
869
|
+
}),
|
|
868
870
|
"platform-token",
|
|
869
871
|
"https://platform.vellum.ai",
|
|
870
872
|
);
|
|
871
873
|
|
|
872
|
-
// Runtime export-to-gcs kicked off with the signed upload URL
|
|
874
|
+
// Runtime export-to-gcs kicked off with the signed upload URL.
|
|
875
|
+
// Helper takes an entry, not a bare URL — the entry's cloud drives
|
|
876
|
+
// URL construction (local → gateway loopback path).
|
|
873
877
|
expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
|
|
874
|
-
|
|
878
|
+
expect.objectContaining({
|
|
879
|
+
cloud: "local",
|
|
880
|
+
runtimeUrl: "http://localhost:7821",
|
|
881
|
+
}),
|
|
875
882
|
"local-token",
|
|
876
883
|
expect.objectContaining({
|
|
877
884
|
uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
|
|
@@ -926,30 +933,92 @@ describe("unified GCS flow — four directions", () => {
|
|
|
926
933
|
bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
|
|
927
934
|
});
|
|
928
935
|
|
|
936
|
+
// The bundle key now flows from the upload signed-URL request rather than
|
|
937
|
+
// the job-status payload — pin it so the download-URL assertion below
|
|
938
|
+
// still uses the same expected key.
|
|
939
|
+
platformRequestSignedUrlMock.mockImplementation(async (params) => ({
|
|
940
|
+
url:
|
|
941
|
+
params.operation === "upload"
|
|
942
|
+
? "https://storage.googleapis.com/bucket/signed-upload"
|
|
943
|
+
: "https://storage.googleapis.com/bucket/signed-download",
|
|
944
|
+
bundleKey:
|
|
945
|
+
params.bundleKey ?? "platform-exports/org-1/bundle-abc.vbundle",
|
|
946
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
947
|
+
}));
|
|
948
|
+
|
|
929
949
|
const restoreFetch = installTrackingFetch();
|
|
930
950
|
try {
|
|
931
951
|
await teleport();
|
|
932
952
|
|
|
933
|
-
// Platform side:
|
|
934
|
-
|
|
935
|
-
expect(
|
|
953
|
+
// Platform side: requested an upload URL, kicked off a runtime export to
|
|
954
|
+
// GCS, and polled the unified job status.
|
|
955
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
956
|
+
expect.objectContaining({
|
|
957
|
+
operation: "upload",
|
|
958
|
+
minRuntimeVersion: "0.6.5",
|
|
959
|
+
maxRuntimeVersion: null,
|
|
960
|
+
}),
|
|
961
|
+
"platform-token",
|
|
962
|
+
"https://platform.vellum.ai",
|
|
963
|
+
);
|
|
964
|
+
// For platform sources, export-to-gcs is reached via the platform's
|
|
965
|
+
// wildcard runtime proxy. The helper builds the assistant-scoped URL
|
|
966
|
+
// from the entry (`/v1/assistants/<id>/migrations/export-to-gcs`) and
|
|
967
|
+
// sends platform-token auth — no guardian-token bootstrap.
|
|
968
|
+
expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
|
|
969
|
+
expect.objectContaining({
|
|
970
|
+
cloud: "vellum",
|
|
971
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
972
|
+
assistantId: "my-platform",
|
|
973
|
+
}),
|
|
974
|
+
"platform-token",
|
|
975
|
+
expect.objectContaining({
|
|
976
|
+
uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
|
|
977
|
+
description: "teleport export",
|
|
978
|
+
}),
|
|
979
|
+
);
|
|
980
|
+
// Polling for platform sources also goes through the wildcard via
|
|
981
|
+
// localRuntimePollJobStatus(entry, ...) — the dedicated
|
|
982
|
+
// `/v1/migrations/jobs/{id}/` endpoint queries platform-side
|
|
983
|
+
// ImportJob records and would 404 on runtime-created job IDs.
|
|
984
|
+
expect(localRuntimePollJobStatusMock).toHaveBeenCalledWith(
|
|
985
|
+
expect.objectContaining({
|
|
986
|
+
cloud: "vellum",
|
|
987
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
988
|
+
}),
|
|
989
|
+
"platform-token",
|
|
990
|
+
"local-export-job-1",
|
|
991
|
+
);
|
|
936
992
|
|
|
937
993
|
// For the local target we request a download URL keyed by the
|
|
938
994
|
// platform's bundle_key. The URL must target the SOURCE platform
|
|
939
995
|
// (where the bundle was written) — pinned so a lockfile change
|
|
940
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.
|
|
941
1002
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
942
|
-
{
|
|
1003
|
+
expect.objectContaining({
|
|
943
1004
|
operation: "download",
|
|
944
1005
|
bundleKey: "platform-exports/org-1/bundle-abc.vbundle",
|
|
945
|
-
|
|
1006
|
+
targetRuntimeVersion: "0.6.5",
|
|
1007
|
+
}),
|
|
946
1008
|
"platform-token",
|
|
947
1009
|
"https://platform.vellum.ai",
|
|
948
1010
|
);
|
|
1011
|
+
expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
|
|
1012
|
+
expect.objectContaining({ cloud: "local" }),
|
|
1013
|
+
expect.any(String),
|
|
1014
|
+
);
|
|
949
1015
|
|
|
950
1016
|
// Runtime import-from-gcs was kicked off with that URL.
|
|
951
1017
|
expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
|
|
952
|
-
|
|
1018
|
+
expect.objectContaining({
|
|
1019
|
+
cloud: "local",
|
|
1020
|
+
runtimeUrl: "http://localhost:7821",
|
|
1021
|
+
}),
|
|
953
1022
|
"local-token",
|
|
954
1023
|
expect.objectContaining({
|
|
955
1024
|
bundleUrl: "https://storage.googleapis.com/bucket/signed-download",
|
|
@@ -993,23 +1062,37 @@ describe("unified GCS flow — four directions", () => {
|
|
|
993
1062
|
// lives in one place end-to-end. For local→docker neither side is
|
|
994
1063
|
// platform, so we default to getPlatformUrl() (resolved once).
|
|
995
1064
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
996
|
-
expect.objectContaining({
|
|
1065
|
+
expect.objectContaining({
|
|
1066
|
+
operation: "upload",
|
|
1067
|
+
minRuntimeVersion: "0.6.5",
|
|
1068
|
+
maxRuntimeVersion: null,
|
|
1069
|
+
}),
|
|
997
1070
|
"platform-token",
|
|
998
1071
|
"https://platform.vellum.ai",
|
|
999
1072
|
);
|
|
1000
1073
|
expect(localRuntimeExportToGcsMock).toHaveBeenCalled();
|
|
1001
1074
|
|
|
1002
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.
|
|
1003
1078
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1004
|
-
{
|
|
1079
|
+
expect.objectContaining({
|
|
1005
1080
|
operation: "download",
|
|
1006
1081
|
bundleKey: "bundle-key-123",
|
|
1007
|
-
|
|
1082
|
+
targetRuntimeVersion: "0.6.5",
|
|
1083
|
+
}),
|
|
1008
1084
|
"platform-token",
|
|
1009
1085
|
"https://platform.vellum.ai",
|
|
1010
1086
|
);
|
|
1087
|
+
expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
|
|
1088
|
+
expect.objectContaining({ cloud: "docker" }),
|
|
1089
|
+
expect.any(String),
|
|
1090
|
+
);
|
|
1011
1091
|
expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
|
|
1012
|
-
|
|
1092
|
+
expect.objectContaining({
|
|
1093
|
+
cloud: "docker",
|
|
1094
|
+
runtimeUrl: "http://localhost:7822",
|
|
1095
|
+
}),
|
|
1013
1096
|
"local-token",
|
|
1014
1097
|
expect.objectContaining({
|
|
1015
1098
|
bundleUrl: "https://storage.googleapis.com/bucket/signed-download",
|
|
@@ -1057,12 +1140,19 @@ describe("unified GCS flow — four directions", () => {
|
|
|
1057
1140
|
// Export leg: upload-URL (pinned to the same platform as import),
|
|
1058
1141
|
// then runtime export.
|
|
1059
1142
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1060
|
-
expect.objectContaining({
|
|
1143
|
+
expect.objectContaining({
|
|
1144
|
+
operation: "upload",
|
|
1145
|
+
minRuntimeVersion: "0.6.5",
|
|
1146
|
+
maxRuntimeVersion: null,
|
|
1147
|
+
}),
|
|
1061
1148
|
"platform-token",
|
|
1062
1149
|
"https://platform.vellum.ai",
|
|
1063
1150
|
);
|
|
1064
1151
|
expect(localRuntimeExportToGcsMock).toHaveBeenCalledWith(
|
|
1065
|
-
|
|
1152
|
+
expect.objectContaining({
|
|
1153
|
+
cloud: "docker",
|
|
1154
|
+
runtimeUrl: "http://localhost:7822",
|
|
1155
|
+
}),
|
|
1066
1156
|
"local-token",
|
|
1067
1157
|
expect.objectContaining({
|
|
1068
1158
|
uploadUrl: "https://storage.googleapis.com/bucket/signed-upload",
|
|
@@ -1071,7 +1161,10 @@ describe("unified GCS flow — four directions", () => {
|
|
|
1071
1161
|
|
|
1072
1162
|
// Import leg: download-URL targets the new local runtime
|
|
1073
1163
|
expect(localRuntimeImportFromGcsMock).toHaveBeenCalledWith(
|
|
1074
|
-
|
|
1164
|
+
expect.objectContaining({
|
|
1165
|
+
cloud: "local",
|
|
1166
|
+
runtimeUrl: "http://localhost:7823",
|
|
1167
|
+
}),
|
|
1075
1168
|
"local-token",
|
|
1076
1169
|
expect.anything(),
|
|
1077
1170
|
);
|
|
@@ -1115,7 +1208,11 @@ describe("signed-URL request targets the bundle-owning platform", () => {
|
|
|
1115
1208
|
// The signed-URL request for upload MUST target the existing
|
|
1116
1209
|
// platform assistant's runtimeUrl, not the default platform URL.
|
|
1117
1210
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1118
|
-
expect.objectContaining({
|
|
1211
|
+
expect.objectContaining({
|
|
1212
|
+
operation: "upload",
|
|
1213
|
+
minRuntimeVersion: "0.6.5",
|
|
1214
|
+
maxRuntimeVersion: null,
|
|
1215
|
+
}),
|
|
1119
1216
|
"platform-token",
|
|
1120
1217
|
"https://staging-platform.vellum.ai",
|
|
1121
1218
|
);
|
|
@@ -1159,14 +1256,31 @@ describe("signed-URL request targets the bundle-owning platform", () => {
|
|
|
1159
1256
|
bundleKey: "dev-bundle-key",
|
|
1160
1257
|
});
|
|
1161
1258
|
|
|
1259
|
+
// Bundle key flows from the upload signed-URL request now; pin it so the
|
|
1260
|
+
// download-URL assertion below uses the same key.
|
|
1261
|
+
platformRequestSignedUrlMock.mockImplementation(async (params) => ({
|
|
1262
|
+
url:
|
|
1263
|
+
params.operation === "upload"
|
|
1264
|
+
? "https://storage.googleapis.com/bucket/signed-upload"
|
|
1265
|
+
: "https://storage.googleapis.com/bucket/signed-download",
|
|
1266
|
+
bundleKey: params.bundleKey ?? "dev-bundle-key",
|
|
1267
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
1268
|
+
}));
|
|
1269
|
+
|
|
1162
1270
|
const restoreFetch = installTrackingFetch();
|
|
1163
1271
|
try {
|
|
1164
1272
|
await teleport();
|
|
1165
1273
|
|
|
1166
1274
|
// The download URL must be requested from the SOURCE platform (where
|
|
1167
|
-
// the bundle was written by the
|
|
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.
|
|
1168
1278
|
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
1169
|
-
{
|
|
1279
|
+
expect.objectContaining({
|
|
1280
|
+
operation: "download",
|
|
1281
|
+
bundleKey: "dev-bundle-key",
|
|
1282
|
+
targetRuntimeVersion: "0.6.5",
|
|
1283
|
+
}),
|
|
1170
1284
|
"platform-token",
|
|
1171
1285
|
"https://dev-platform.vellum.ai",
|
|
1172
1286
|
);
|
|
@@ -1465,6 +1579,274 @@ describe("MigrationInProgressError handling", () => {
|
|
|
1465
1579
|
});
|
|
1466
1580
|
});
|
|
1467
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
|
+
|
|
1468
1850
|
// ---------------------------------------------------------------------------
|
|
1469
1851
|
// Dry-run behavior
|
|
1470
1852
|
// ---------------------------------------------------------------------------
|
|
@@ -1555,10 +1937,9 @@ describe("dry-run", () => {
|
|
|
1555
1937
|
),
|
|
1556
1938
|
);
|
|
1557
1939
|
|
|
1558
|
-
// Must fail BEFORE any export work — no signed URL request, no
|
|
1559
|
-
// export
|
|
1940
|
+
// Must fail BEFORE any export work — no signed URL request, no runtime
|
|
1941
|
+
// export kickoff, nothing that costs time or bandwidth.
|
|
1560
1942
|
expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
|
|
1561
|
-
expect(platformInitiateExportMock).not.toHaveBeenCalled();
|
|
1562
1943
|
expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
|
|
1563
1944
|
} finally {
|
|
1564
1945
|
restoreFetch();
|
|
@@ -2090,6 +2471,185 @@ describe("auth + transient-error resilience", () => {
|
|
|
2090
2471
|
});
|
|
2091
2472
|
});
|
|
2092
2473
|
|
|
2474
|
+
// ---------------------------------------------------------------------------
|
|
2475
|
+
// Source-runtime version is sourced from the daemon, not the CLI
|
|
2476
|
+
// (Codex P1 regression guard for PR #29436)
|
|
2477
|
+
// ---------------------------------------------------------------------------
|
|
2478
|
+
|
|
2479
|
+
describe("upload signed-URL records source runtime version (not CLI version)", () => {
|
|
2480
|
+
test("local source: identity is fetched BEFORE the upload signed-URL request", async () => {
|
|
2481
|
+
setArgv("--from", "my-local", "--platform");
|
|
2482
|
+
|
|
2483
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2484
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
2485
|
+
name === "my-local" ? localEntry : null,
|
|
2486
|
+
);
|
|
2487
|
+
|
|
2488
|
+
// Distinguish the daemon's version from anything else hardcoded.
|
|
2489
|
+
localRuntimeIdentityMock.mockResolvedValue({ version: "0.5.9" });
|
|
2490
|
+
|
|
2491
|
+
// Order tracker: capture which mock was called first.
|
|
2492
|
+
const callOrder: string[] = [];
|
|
2493
|
+
localRuntimeIdentityMock.mockImplementationOnce(async () => {
|
|
2494
|
+
callOrder.push("identity");
|
|
2495
|
+
return { version: "0.5.9" };
|
|
2496
|
+
});
|
|
2497
|
+
platformRequestSignedUrlMock.mockImplementationOnce(async (params) => {
|
|
2498
|
+
callOrder.push("signed-url");
|
|
2499
|
+
return {
|
|
2500
|
+
url: "https://storage.googleapis.com/bucket/signed-upload",
|
|
2501
|
+
bundleKey: params.bundleKey ?? "bundle-key-123",
|
|
2502
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
2503
|
+
};
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
const restoreFetch = installTrackingFetch();
|
|
2507
|
+
try {
|
|
2508
|
+
await teleport();
|
|
2509
|
+
|
|
2510
|
+
expect(callOrder[0]).toBe("identity");
|
|
2511
|
+
expect(callOrder[1]).toBe("signed-url");
|
|
2512
|
+
|
|
2513
|
+
// Upload request stamps minRuntimeVersion with the daemon's version,
|
|
2514
|
+
// NOT cliPkg.version.
|
|
2515
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
2516
|
+
expect.objectContaining({
|
|
2517
|
+
operation: "upload",
|
|
2518
|
+
minRuntimeVersion: "0.5.9",
|
|
2519
|
+
maxRuntimeVersion: null,
|
|
2520
|
+
}),
|
|
2521
|
+
"platform-token",
|
|
2522
|
+
"https://platform.vellum.ai",
|
|
2523
|
+
);
|
|
2524
|
+
} finally {
|
|
2525
|
+
restoreFetch();
|
|
2526
|
+
}
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
test("local source: identity fetch failure aborts before signed-URL request", async () => {
|
|
2530
|
+
setArgv("--from", "my-local", "--platform");
|
|
2531
|
+
|
|
2532
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2533
|
+
findAssistantByNameMock.mockImplementation((name: string) =>
|
|
2534
|
+
name === "my-local" ? localEntry : null,
|
|
2535
|
+
);
|
|
2536
|
+
|
|
2537
|
+
localRuntimeIdentityMock.mockRejectedValue(
|
|
2538
|
+
new Error("Failed to fetch runtime identity: 503 Service Unavailable"),
|
|
2539
|
+
);
|
|
2540
|
+
|
|
2541
|
+
const restoreFetch = installTrackingFetch();
|
|
2542
|
+
try {
|
|
2543
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
2544
|
+
|
|
2545
|
+
// Must NOT have proceeded to signed URL or export.
|
|
2546
|
+
expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
|
|
2547
|
+
expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
|
|
2548
|
+
|
|
2549
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
2550
|
+
expect.stringContaining("Could not fetch runtime identity"),
|
|
2551
|
+
);
|
|
2552
|
+
} finally {
|
|
2553
|
+
restoreFetch();
|
|
2554
|
+
}
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
test("platform source: managed runtime's identity is fetched and recorded", async () => {
|
|
2558
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
2559
|
+
|
|
2560
|
+
const platformEntry = makeEntry("my-platform", {
|
|
2561
|
+
cloud: "vellum",
|
|
2562
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2563
|
+
});
|
|
2564
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2565
|
+
|
|
2566
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2567
|
+
if (name === "my-platform") return platformEntry;
|
|
2568
|
+
if (name === "my-local") return localEntry;
|
|
2569
|
+
return null;
|
|
2570
|
+
});
|
|
2571
|
+
|
|
2572
|
+
platformPollJobStatusMock.mockResolvedValue({
|
|
2573
|
+
jobId: "platform-export-job-1",
|
|
2574
|
+
type: "export",
|
|
2575
|
+
status: "complete",
|
|
2576
|
+
bundleKey: "platform-bundle",
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
localRuntimeIdentityMock.mockResolvedValue({ version: "0.7.2" });
|
|
2580
|
+
|
|
2581
|
+
const restoreFetch = installTrackingFetch();
|
|
2582
|
+
try {
|
|
2583
|
+
await teleport();
|
|
2584
|
+
|
|
2585
|
+
// Identity was fetched against the platform-managed runtime entry
|
|
2586
|
+
// (cloud=vellum) with the platform token — not via guardian token.
|
|
2587
|
+
expect(localRuntimeIdentityMock).toHaveBeenCalledWith(
|
|
2588
|
+
expect.objectContaining({
|
|
2589
|
+
cloud: "vellum",
|
|
2590
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2591
|
+
assistantId: "my-platform",
|
|
2592
|
+
}),
|
|
2593
|
+
"platform-token",
|
|
2594
|
+
);
|
|
2595
|
+
|
|
2596
|
+
// The recorded version came from the platform runtime, not the CLI.
|
|
2597
|
+
expect(platformRequestSignedUrlMock).toHaveBeenCalledWith(
|
|
2598
|
+
expect.objectContaining({
|
|
2599
|
+
operation: "upload",
|
|
2600
|
+
minRuntimeVersion: "0.7.2",
|
|
2601
|
+
maxRuntimeVersion: null,
|
|
2602
|
+
}),
|
|
2603
|
+
"platform-token",
|
|
2604
|
+
"https://platform.vellum.ai",
|
|
2605
|
+
);
|
|
2606
|
+
} finally {
|
|
2607
|
+
restoreFetch();
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
|
|
2611
|
+
test("platform source: identity fetch failure aborts before signed-URL request", async () => {
|
|
2612
|
+
setArgv("--from", "my-platform", "--local", "my-local");
|
|
2613
|
+
|
|
2614
|
+
const platformEntry = makeEntry("my-platform", {
|
|
2615
|
+
cloud: "vellum",
|
|
2616
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
2617
|
+
});
|
|
2618
|
+
const localEntry = makeEntry("my-local", { cloud: "local" });
|
|
2619
|
+
|
|
2620
|
+
findAssistantByNameMock.mockImplementation((name: string) => {
|
|
2621
|
+
if (name === "my-platform") return platformEntry;
|
|
2622
|
+
if (name === "my-local") return localEntry;
|
|
2623
|
+
return null;
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
localRuntimeIdentityMock.mockRejectedValue(
|
|
2627
|
+
new Error("Failed to fetch runtime identity: 502 Bad Gateway"),
|
|
2628
|
+
);
|
|
2629
|
+
|
|
2630
|
+
const restoreFetch = installTrackingFetch();
|
|
2631
|
+
try {
|
|
2632
|
+
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
2633
|
+
|
|
2634
|
+
// Signed-URL upload request must not have happened.
|
|
2635
|
+
const uploadCalls = platformRequestSignedUrlMock.mock.calls.filter(
|
|
2636
|
+
(call: unknown[]) =>
|
|
2637
|
+
(call[0] as { operation: string }).operation === "upload",
|
|
2638
|
+
);
|
|
2639
|
+
expect(uploadCalls.length).toBe(0);
|
|
2640
|
+
|
|
2641
|
+
// Runtime export was never kicked off.
|
|
2642
|
+
expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
|
|
2643
|
+
|
|
2644
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
2645
|
+
expect.stringContaining("Could not fetch runtime identity"),
|
|
2646
|
+
);
|
|
2647
|
+
} finally {
|
|
2648
|
+
restoreFetch();
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2651
|
+
});
|
|
2652
|
+
|
|
2093
2653
|
// ---------------------------------------------------------------------------
|
|
2094
2654
|
// Misc: legacy --to deprecation
|
|
2095
2655
|
// ---------------------------------------------------------------------------
|