@vellumai/cli 0.6.6 → 0.7.1
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 +8 -2
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/backup.test.ts +475 -0
- package/src/__tests__/config-utils.test.ts +146 -0
- package/src/__tests__/env-drift.test.ts +10 -32
- package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
- package/src/__tests__/multi-local.test.ts +0 -5
- package/src/__tests__/sleep.test.ts +1 -2
- package/src/__tests__/teleport.test.ts +988 -1266
- package/src/commands/backup.ts +117 -71
- package/src/commands/client.ts +10 -9
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +58 -13
- package/src/commands/login.ts +77 -12
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +144 -25
- package/src/commands/restore.ts +26 -47
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +17 -7
- package/src/commands/teleport.ts +462 -584
- package/src/commands/terminal.ts +9 -221
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +108 -7
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +328 -154
- package/src/index.ts +5 -7
- package/src/lib/__tests__/docker.test.ts +50 -74
- package/src/lib/__tests__/job-polling.test.ts +278 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +480 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/__tests__/runtime-url.test.ts +87 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +5 -21
- package/src/lib/assistant-config.ts +46 -24
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/docker.ts +75 -77
- package/src/lib/environments/__tests__/paths.test.ts +2 -0
- package/src/lib/environments/resolve.ts +89 -7
- package/src/lib/environments/seeds.ts +8 -5
- package/src/lib/environments/types.ts +10 -0
- package/src/lib/hatch-local.ts +15 -120
- package/src/lib/health-check.ts +98 -0
- package/src/lib/job-polling.ts +195 -0
- package/src/lib/local-runtime-client.ts +231 -0
- package/src/lib/local.ts +165 -72
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +190 -194
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/runtime-url.ts +30 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +536 -0
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/shared/provider-env-vars.ts +2 -3
- package/src/__tests__/orphan-detection.test.ts +0 -214
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { AssistantEntry } from "../assistant-config.js";
|
|
4
|
+
import {
|
|
5
|
+
MigrationInProgressError,
|
|
6
|
+
localRuntimeExportToGcs,
|
|
7
|
+
localRuntimeImportFromGcs,
|
|
8
|
+
localRuntimePollJobStatus,
|
|
9
|
+
} from "../local-runtime-client.js";
|
|
10
|
+
|
|
11
|
+
const RUNTIME_URL = "http://127.0.0.1:8765";
|
|
12
|
+
const TOKEN = "local-bearer-token";
|
|
13
|
+
|
|
14
|
+
// All tests in this file exercise the local/docker code path (cloud="local"),
|
|
15
|
+
// which builds `{runtimeUrl}/v1/migrations/<subpath>` URLs and uses
|
|
16
|
+
// guardian-token bearer auth. The platform path (cloud="vellum") is covered
|
|
17
|
+
// by `runtime-url.test.ts` (URL construction) and the teleport tests
|
|
18
|
+
// (call-site wiring).
|
|
19
|
+
const ENTRY: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId"> = {
|
|
20
|
+
cloud: "local",
|
|
21
|
+
runtimeUrl: RUNTIME_URL,
|
|
22
|
+
assistantId: "ast-test-1",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface CapturedCall {
|
|
26
|
+
url: string;
|
|
27
|
+
method: string;
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
body: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function captureFetch(
|
|
33
|
+
responder: (call: CapturedCall) => Response | Promise<Response>,
|
|
34
|
+
): {
|
|
35
|
+
calls: CapturedCall[];
|
|
36
|
+
fetchMock: typeof globalThis.fetch;
|
|
37
|
+
} {
|
|
38
|
+
const calls: CapturedCall[] = [];
|
|
39
|
+
const fetchMock = mock(
|
|
40
|
+
async (url: string | URL | Request, init?: RequestInit) => {
|
|
41
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
42
|
+
const rawHeaders = (init?.headers ?? {}) as
|
|
43
|
+
| Record<string, string>
|
|
44
|
+
| Headers;
|
|
45
|
+
const headers: Record<string, string> = {};
|
|
46
|
+
if (rawHeaders instanceof Headers) {
|
|
47
|
+
rawHeaders.forEach((v, k) => {
|
|
48
|
+
headers[k] = v;
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
Object.assign(headers, rawHeaders);
|
|
52
|
+
}
|
|
53
|
+
let parsedBody: unknown = undefined;
|
|
54
|
+
const b = init?.body;
|
|
55
|
+
if (typeof b === "string") {
|
|
56
|
+
try {
|
|
57
|
+
parsedBody = JSON.parse(b);
|
|
58
|
+
} catch {
|
|
59
|
+
parsedBody = b;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const call: CapturedCall = {
|
|
63
|
+
url: urlStr,
|
|
64
|
+
method: init?.method ?? "GET",
|
|
65
|
+
headers,
|
|
66
|
+
body: parsedBody,
|
|
67
|
+
};
|
|
68
|
+
calls.push(call);
|
|
69
|
+
return responder(call);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
return { calls, fetchMock: fetchMock as unknown as typeof globalThis.fetch };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let originalFetch: typeof globalThis.fetch;
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
originalFetch = globalThis.fetch;
|
|
78
|
+
});
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
globalThis.fetch = originalFetch;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("localRuntimeExportToGcs", () => {
|
|
84
|
+
test("POSTs {upload_url, description} with Bearer auth and returns job_id on 202", async () => {
|
|
85
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
86
|
+
return new Response(
|
|
87
|
+
JSON.stringify({
|
|
88
|
+
job_id: "export-job-1",
|
|
89
|
+
status: "pending",
|
|
90
|
+
type: "export",
|
|
91
|
+
}),
|
|
92
|
+
{ status: 202, headers: { "Content-Type": "application/json" } },
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
globalThis.fetch = fetchMock;
|
|
96
|
+
|
|
97
|
+
const result = await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
98
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
99
|
+
description: "teleport export",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.jobId).toBe("export-job-1");
|
|
103
|
+
expect(calls).toHaveLength(1);
|
|
104
|
+
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/migrations/export-to-gcs`);
|
|
105
|
+
expect(calls[0]!.method).toBe("POST");
|
|
106
|
+
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
107
|
+
expect(calls[0]!.headers["Content-Type"]).toBe("application/json");
|
|
108
|
+
expect(calls[0]!.body).toEqual({
|
|
109
|
+
upload_url: "https://storage.example/signed/abc",
|
|
110
|
+
description: "teleport export",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("omits description when not provided", async () => {
|
|
115
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
116
|
+
return new Response(
|
|
117
|
+
JSON.stringify({ job_id: "j", status: "pending", type: "export" }),
|
|
118
|
+
{ status: 202 },
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
globalThis.fetch = fetchMock;
|
|
122
|
+
|
|
123
|
+
await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
124
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(calls[0]!.body).toEqual({
|
|
128
|
+
upload_url: "https://storage.example/signed/abc",
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("409 export_in_progress (nested {error:{code,job_id}}) throws MigrationInProgressError carrying existing job_id", async () => {
|
|
133
|
+
const { fetchMock } = captureFetch(() => {
|
|
134
|
+
return new Response(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
error: {
|
|
137
|
+
code: "export_in_progress",
|
|
138
|
+
job_id: "existing-export-42",
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
{ status: 409 },
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
globalThis.fetch = fetchMock;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
148
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
149
|
+
});
|
|
150
|
+
throw new Error("expected to throw");
|
|
151
|
+
} catch (err) {
|
|
152
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
153
|
+
const mip = err as MigrationInProgressError;
|
|
154
|
+
expect(mip.kind).toBe("export_in_progress");
|
|
155
|
+
expect(mip.existingJobId).toBe("existing-export-42");
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("409 export_in_progress regression: nested job_id 'abc-123' is surfaced (not empty)", async () => {
|
|
160
|
+
const { fetchMock } = captureFetch(() => {
|
|
161
|
+
return new Response(
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
error: { code: "export_in_progress", job_id: "abc-123" },
|
|
164
|
+
}),
|
|
165
|
+
{ status: 409 },
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
globalThis.fetch = fetchMock;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
172
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
173
|
+
});
|
|
174
|
+
throw new Error("expected to throw");
|
|
175
|
+
} catch (err) {
|
|
176
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
177
|
+
const mip = err as MigrationInProgressError;
|
|
178
|
+
expect(mip.existingJobId).toBe("abc-123");
|
|
179
|
+
expect(mip.existingJobId).not.toBe("");
|
|
180
|
+
expect(mip.kind).toBe("export_in_progress");
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("409 export_in_progress with legacy flat shape is still parsed", async () => {
|
|
185
|
+
const { fetchMock } = captureFetch(() => {
|
|
186
|
+
return new Response(
|
|
187
|
+
JSON.stringify({
|
|
188
|
+
code: "export_in_progress",
|
|
189
|
+
job_id: "legacy-export-9",
|
|
190
|
+
}),
|
|
191
|
+
{ status: 409 },
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
globalThis.fetch = fetchMock;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
198
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
199
|
+
});
|
|
200
|
+
throw new Error("expected to throw");
|
|
201
|
+
} catch (err) {
|
|
202
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
203
|
+
const mip = err as MigrationInProgressError;
|
|
204
|
+
expect(mip.kind).toBe("export_in_progress");
|
|
205
|
+
expect(mip.existingJobId).toBe("legacy-export-9");
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("non-202 non-409 responses throw with status + body", async () => {
|
|
210
|
+
const { fetchMock } = captureFetch(() => {
|
|
211
|
+
return new Response("boom", { status: 500 });
|
|
212
|
+
});
|
|
213
|
+
globalThis.fetch = fetchMock;
|
|
214
|
+
|
|
215
|
+
await expect(
|
|
216
|
+
localRuntimeExportToGcs(ENTRY, TOKEN, {
|
|
217
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
218
|
+
}),
|
|
219
|
+
).rejects.toThrow(/500/);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("localRuntimeImportFromGcs", () => {
|
|
224
|
+
test("POSTs {bundle_url} with Bearer auth and returns job_id on 202", async () => {
|
|
225
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
226
|
+
return new Response(
|
|
227
|
+
JSON.stringify({
|
|
228
|
+
job_id: "import-job-1",
|
|
229
|
+
status: "pending",
|
|
230
|
+
type: "import",
|
|
231
|
+
}),
|
|
232
|
+
{ status: 202 },
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
globalThis.fetch = fetchMock;
|
|
236
|
+
|
|
237
|
+
const result = await localRuntimeImportFromGcs(ENTRY, TOKEN, {
|
|
238
|
+
bundleUrl: "https://storage.example/signed/dl-xyz",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(result.jobId).toBe("import-job-1");
|
|
242
|
+
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/migrations/import-from-gcs`);
|
|
243
|
+
expect(calls[0]!.method).toBe("POST");
|
|
244
|
+
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
245
|
+
expect(calls[0]!.body).toEqual({
|
|
246
|
+
bundle_url: "https://storage.example/signed/dl-xyz",
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("409 import_in_progress (nested {error:{code,job_id}}) throws MigrationInProgressError carrying existing job_id", async () => {
|
|
251
|
+
const { fetchMock } = captureFetch(() => {
|
|
252
|
+
return new Response(
|
|
253
|
+
JSON.stringify({
|
|
254
|
+
error: {
|
|
255
|
+
code: "import_in_progress",
|
|
256
|
+
job_id: "existing-import-7",
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
{ status: 409 },
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
globalThis.fetch = fetchMock;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await localRuntimeImportFromGcs(ENTRY, TOKEN, {
|
|
266
|
+
bundleUrl: "https://storage.example/signed/dl-xyz",
|
|
267
|
+
});
|
|
268
|
+
throw new Error("expected to throw");
|
|
269
|
+
} catch (err) {
|
|
270
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
271
|
+
const mip = err as MigrationInProgressError;
|
|
272
|
+
expect(mip.kind).toBe("import_in_progress");
|
|
273
|
+
expect(mip.existingJobId).toBe("existing-import-7");
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("409 import_in_progress with legacy flat shape is still parsed", async () => {
|
|
278
|
+
const { fetchMock } = captureFetch(() => {
|
|
279
|
+
return new Response(
|
|
280
|
+
JSON.stringify({
|
|
281
|
+
code: "import_in_progress",
|
|
282
|
+
job_id: "legacy-import-2",
|
|
283
|
+
}),
|
|
284
|
+
{ status: 409 },
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
globalThis.fetch = fetchMock;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await localRuntimeImportFromGcs(ENTRY, TOKEN, {
|
|
291
|
+
bundleUrl: "https://storage.example/signed/dl-xyz",
|
|
292
|
+
});
|
|
293
|
+
throw new Error("expected to throw");
|
|
294
|
+
} catch (err) {
|
|
295
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
296
|
+
const mip = err as MigrationInProgressError;
|
|
297
|
+
expect(mip.kind).toBe("import_in_progress");
|
|
298
|
+
expect(mip.existingJobId).toBe("legacy-import-2");
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("localRuntimePollJobStatus", () => {
|
|
304
|
+
test("GETs /v1/migrations/jobs/{jobId} with Bearer auth and parses processing", async () => {
|
|
305
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
306
|
+
return new Response(
|
|
307
|
+
JSON.stringify({
|
|
308
|
+
job_id: "poll-1",
|
|
309
|
+
type: "export",
|
|
310
|
+
status: "processing",
|
|
311
|
+
}),
|
|
312
|
+
{ status: 200 },
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
globalThis.fetch = fetchMock;
|
|
316
|
+
|
|
317
|
+
const status = await localRuntimePollJobStatus(ENTRY, TOKEN, "poll-1");
|
|
318
|
+
|
|
319
|
+
expect(status).toEqual({
|
|
320
|
+
jobId: "poll-1",
|
|
321
|
+
type: "export",
|
|
322
|
+
status: "processing",
|
|
323
|
+
});
|
|
324
|
+
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/migrations/jobs/poll-1`);
|
|
325
|
+
expect(calls[0]!.method).toBe("GET");
|
|
326
|
+
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("parses complete with bundle_key", async () => {
|
|
330
|
+
const { fetchMock } = captureFetch(() => {
|
|
331
|
+
return new Response(
|
|
332
|
+
JSON.stringify({
|
|
333
|
+
job_id: "poll-2",
|
|
334
|
+
type: "export",
|
|
335
|
+
status: "complete",
|
|
336
|
+
bundle_key: "bundles/x.tar.gz",
|
|
337
|
+
}),
|
|
338
|
+
{ status: 200 },
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
globalThis.fetch = fetchMock;
|
|
342
|
+
|
|
343
|
+
const status = await localRuntimePollJobStatus(ENTRY, TOKEN, "poll-2");
|
|
344
|
+
|
|
345
|
+
expect(status.status).toBe("complete");
|
|
346
|
+
if (status.status === "complete") {
|
|
347
|
+
expect(status.bundleKey).toBe("bundles/x.tar.gz");
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("parses failed with error", async () => {
|
|
352
|
+
const { fetchMock } = captureFetch(() => {
|
|
353
|
+
return new Response(
|
|
354
|
+
JSON.stringify({
|
|
355
|
+
job_id: "poll-3",
|
|
356
|
+
type: "import",
|
|
357
|
+
status: "failed",
|
|
358
|
+
error: "corrupted bundle",
|
|
359
|
+
}),
|
|
360
|
+
{ status: 200 },
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
globalThis.fetch = fetchMock;
|
|
364
|
+
|
|
365
|
+
const status = await localRuntimePollJobStatus(ENTRY, TOKEN, "poll-3");
|
|
366
|
+
|
|
367
|
+
expect(status.status).toBe("failed");
|
|
368
|
+
if (status.status === "failed") {
|
|
369
|
+
expect(status.error).toBe("corrupted bundle");
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("404 → throws 'Migration job not found'", async () => {
|
|
374
|
+
const { fetchMock } = captureFetch(() => {
|
|
375
|
+
return new Response("{}", { status: 404 });
|
|
376
|
+
});
|
|
377
|
+
globalThis.fetch = fetchMock;
|
|
378
|
+
|
|
379
|
+
await expect(
|
|
380
|
+
localRuntimePollJobStatus(ENTRY, TOKEN, "missing"),
|
|
381
|
+
).rejects.toThrow(/Migration job not found/);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Platform-managed assistants (cloud="vellum") route through the platform's
|
|
387
|
+
// wildcard runtime proxy at `/v1/assistants/<id>/migrations/...` with
|
|
388
|
+
// platform-token auth (NOT guardian-token bearer). This block asserts the
|
|
389
|
+
// actual URL and headers built by the helpers — not mocked, not abstracted.
|
|
390
|
+
// Regression guard for the routing bug fixed in this PR.
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
const VELLUM_ENTRY: Pick<
|
|
393
|
+
AssistantEntry,
|
|
394
|
+
"cloud" | "runtimeUrl" | "assistantId"
|
|
395
|
+
> = {
|
|
396
|
+
cloud: "vellum",
|
|
397
|
+
runtimeUrl: "https://platform.vellum.ai",
|
|
398
|
+
assistantId: "11111111-2222-3333-4444-555555555555",
|
|
399
|
+
};
|
|
400
|
+
// `vak_` prefix bypasses `fetchOrganizationId` (org-scoped API keys); the
|
|
401
|
+
// auth header collapses to a single `Authorization: Bearer vak_...` so this
|
|
402
|
+
// test stays free of network mocks.
|
|
403
|
+
const VAK_TOKEN = "vak_platform-token";
|
|
404
|
+
|
|
405
|
+
describe("vellum-cloud routing through wildcard proxy", () => {
|
|
406
|
+
test("export-to-gcs URL has /v1/assistants/<id>/migrations/ prefix and uses platform-token bearer (no guardian)", async () => {
|
|
407
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
408
|
+
return new Response(
|
|
409
|
+
JSON.stringify({ job_id: "wp-export-1", status: "pending" }),
|
|
410
|
+
{ status: 202, headers: { "Content-Type": "application/json" } },
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
globalThis.fetch = fetchMock;
|
|
414
|
+
|
|
415
|
+
const result = await localRuntimeExportToGcs(VELLUM_ENTRY, VAK_TOKEN, {
|
|
416
|
+
uploadUrl: "https://storage.example/signed/x",
|
|
417
|
+
description: "teleport export",
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(result.jobId).toBe("wp-export-1");
|
|
421
|
+
expect(calls[0]!.url).toBe(
|
|
422
|
+
`https://platform.vellum.ai/v1/assistants/11111111-2222-3333-4444-555555555555/migrations/export-to-gcs`,
|
|
423
|
+
);
|
|
424
|
+
expect(calls[0]!.method).toBe("POST");
|
|
425
|
+
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${VAK_TOKEN}`);
|
|
426
|
+
expect(calls[0]!.body).toEqual({
|
|
427
|
+
upload_url: "https://storage.example/signed/x",
|
|
428
|
+
description: "teleport export",
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("import-from-gcs URL has /v1/assistants/<id>/migrations/ prefix", async () => {
|
|
433
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
434
|
+
return new Response(
|
|
435
|
+
JSON.stringify({ job_id: "wp-import-1", status: "pending" }),
|
|
436
|
+
{ status: 202 },
|
|
437
|
+
);
|
|
438
|
+
});
|
|
439
|
+
globalThis.fetch = fetchMock;
|
|
440
|
+
|
|
441
|
+
await localRuntimeImportFromGcs(VELLUM_ENTRY, VAK_TOKEN, {
|
|
442
|
+
bundleUrl: "https://storage.example/download/y",
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(calls[0]!.url).toBe(
|
|
446
|
+
`https://platform.vellum.ai/v1/assistants/11111111-2222-3333-4444-555555555555/migrations/import-from-gcs`,
|
|
447
|
+
);
|
|
448
|
+
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${VAK_TOKEN}`);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("jobs/<id> URL has /v1/assistants/<id>/migrations/ prefix (NOT the dedicated platform endpoint)", async () => {
|
|
452
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
453
|
+
return new Response(
|
|
454
|
+
JSON.stringify({
|
|
455
|
+
job_id: "wp-export-1",
|
|
456
|
+
status: "complete",
|
|
457
|
+
type: "export",
|
|
458
|
+
bundle_key: "exports/org-1/x.vbundle",
|
|
459
|
+
}),
|
|
460
|
+
{ status: 200 },
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
globalThis.fetch = fetchMock;
|
|
464
|
+
|
|
465
|
+
const status = await localRuntimePollJobStatus(
|
|
466
|
+
VELLUM_ENTRY,
|
|
467
|
+
VAK_TOKEN,
|
|
468
|
+
"wp-export-1",
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
expect(calls[0]!.url).toBe(
|
|
472
|
+
`https://platform.vellum.ai/v1/assistants/11111111-2222-3333-4444-555555555555/migrations/jobs/wp-export-1`,
|
|
473
|
+
);
|
|
474
|
+
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${VAK_TOKEN}`);
|
|
475
|
+
expect(status.status).toBe("complete");
|
|
476
|
+
if (status.status === "complete") {
|
|
477
|
+
expect(status.bundleKey).toBe("exports/org-1/x.vbundle");
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
});
|