@vellumai/cli 0.6.6 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -2
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/config-utils.test.ts +159 -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 +919 -1255
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +40 -8
- package/src/commands/hatch.ts +6 -2
- package/src/commands/login.ts +89 -6
- package/src/commands/ps.ts +104 -20
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +15 -2
- package/src/commands/teleport.ts +447 -583
- package/src/commands/terminal.ts +9 -221
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +304 -152
- package/src/index.ts +3 -0
- 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 +383 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/assistant-config.ts +12 -8
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/config-utils.ts +97 -1
- package/src/lib/docker.ts +73 -75
- 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 +178 -0
- package/src/lib/local.ts +139 -15
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +215 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/terminal-session.ts +457 -0
- package/src/shared/provider-env-vars.ts +2 -3
- package/src/__tests__/orphan-detection.test.ts +0 -214
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
MigrationInProgressError,
|
|
5
|
+
localRuntimeExportToGcs,
|
|
6
|
+
localRuntimeImportFromGcs,
|
|
7
|
+
localRuntimePollJobStatus,
|
|
8
|
+
} from "../local-runtime-client.js";
|
|
9
|
+
|
|
10
|
+
const RUNTIME_URL = "http://127.0.0.1:8765";
|
|
11
|
+
const TOKEN = "local-bearer-token";
|
|
12
|
+
|
|
13
|
+
interface CapturedCall {
|
|
14
|
+
url: string;
|
|
15
|
+
method: string;
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
body: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function captureFetch(
|
|
21
|
+
responder: (call: CapturedCall) => Response | Promise<Response>,
|
|
22
|
+
): {
|
|
23
|
+
calls: CapturedCall[];
|
|
24
|
+
fetchMock: typeof globalThis.fetch;
|
|
25
|
+
} {
|
|
26
|
+
const calls: CapturedCall[] = [];
|
|
27
|
+
const fetchMock = mock(
|
|
28
|
+
async (url: string | URL | Request, init?: RequestInit) => {
|
|
29
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
30
|
+
const rawHeaders = (init?.headers ?? {}) as
|
|
31
|
+
| Record<string, string>
|
|
32
|
+
| Headers;
|
|
33
|
+
const headers: Record<string, string> = {};
|
|
34
|
+
if (rawHeaders instanceof Headers) {
|
|
35
|
+
rawHeaders.forEach((v, k) => {
|
|
36
|
+
headers[k] = v;
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
Object.assign(headers, rawHeaders);
|
|
40
|
+
}
|
|
41
|
+
let parsedBody: unknown = undefined;
|
|
42
|
+
const b = init?.body;
|
|
43
|
+
if (typeof b === "string") {
|
|
44
|
+
try {
|
|
45
|
+
parsedBody = JSON.parse(b);
|
|
46
|
+
} catch {
|
|
47
|
+
parsedBody = b;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const call: CapturedCall = {
|
|
51
|
+
url: urlStr,
|
|
52
|
+
method: init?.method ?? "GET",
|
|
53
|
+
headers,
|
|
54
|
+
body: parsedBody,
|
|
55
|
+
};
|
|
56
|
+
calls.push(call);
|
|
57
|
+
return responder(call);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
return { calls, fetchMock: fetchMock as unknown as typeof globalThis.fetch };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let originalFetch: typeof globalThis.fetch;
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
originalFetch = globalThis.fetch;
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
globalThis.fetch = originalFetch;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("localRuntimeExportToGcs", () => {
|
|
72
|
+
test("POSTs {upload_url, description} with Bearer auth and returns job_id on 202", async () => {
|
|
73
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
74
|
+
return new Response(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
job_id: "export-job-1",
|
|
77
|
+
status: "pending",
|
|
78
|
+
type: "export",
|
|
79
|
+
}),
|
|
80
|
+
{ status: 202, headers: { "Content-Type": "application/json" } },
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
globalThis.fetch = fetchMock;
|
|
84
|
+
|
|
85
|
+
const result = await localRuntimeExportToGcs(RUNTIME_URL, TOKEN, {
|
|
86
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
87
|
+
description: "teleport export",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result.jobId).toBe("export-job-1");
|
|
91
|
+
expect(calls).toHaveLength(1);
|
|
92
|
+
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/migrations/export-to-gcs`);
|
|
93
|
+
expect(calls[0]!.method).toBe("POST");
|
|
94
|
+
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
95
|
+
expect(calls[0]!.headers["Content-Type"]).toBe("application/json");
|
|
96
|
+
expect(calls[0]!.body).toEqual({
|
|
97
|
+
upload_url: "https://storage.example/signed/abc",
|
|
98
|
+
description: "teleport export",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("omits description when not provided", async () => {
|
|
103
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
104
|
+
return new Response(
|
|
105
|
+
JSON.stringify({ job_id: "j", status: "pending", type: "export" }),
|
|
106
|
+
{ status: 202 },
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
globalThis.fetch = fetchMock;
|
|
110
|
+
|
|
111
|
+
await localRuntimeExportToGcs(RUNTIME_URL, TOKEN, {
|
|
112
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(calls[0]!.body).toEqual({
|
|
116
|
+
upload_url: "https://storage.example/signed/abc",
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("409 export_in_progress (nested {error:{code,job_id}}) throws MigrationInProgressError carrying existing job_id", async () => {
|
|
121
|
+
const { fetchMock } = captureFetch(() => {
|
|
122
|
+
return new Response(
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
error: {
|
|
125
|
+
code: "export_in_progress",
|
|
126
|
+
job_id: "existing-export-42",
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
{ status: 409 },
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
globalThis.fetch = fetchMock;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await localRuntimeExportToGcs(RUNTIME_URL, TOKEN, {
|
|
136
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
137
|
+
});
|
|
138
|
+
throw new Error("expected to throw");
|
|
139
|
+
} catch (err) {
|
|
140
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
141
|
+
const mip = err as MigrationInProgressError;
|
|
142
|
+
expect(mip.kind).toBe("export_in_progress");
|
|
143
|
+
expect(mip.existingJobId).toBe("existing-export-42");
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("409 export_in_progress regression: nested job_id 'abc-123' is surfaced (not empty)", async () => {
|
|
148
|
+
const { fetchMock } = captureFetch(() => {
|
|
149
|
+
return new Response(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
error: { code: "export_in_progress", job_id: "abc-123" },
|
|
152
|
+
}),
|
|
153
|
+
{ status: 409 },
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
globalThis.fetch = fetchMock;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await localRuntimeExportToGcs(RUNTIME_URL, TOKEN, {
|
|
160
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
161
|
+
});
|
|
162
|
+
throw new Error("expected to throw");
|
|
163
|
+
} catch (err) {
|
|
164
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
165
|
+
const mip = err as MigrationInProgressError;
|
|
166
|
+
expect(mip.existingJobId).toBe("abc-123");
|
|
167
|
+
expect(mip.existingJobId).not.toBe("");
|
|
168
|
+
expect(mip.kind).toBe("export_in_progress");
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("409 export_in_progress with legacy flat shape is still parsed", async () => {
|
|
173
|
+
const { fetchMock } = captureFetch(() => {
|
|
174
|
+
return new Response(
|
|
175
|
+
JSON.stringify({
|
|
176
|
+
code: "export_in_progress",
|
|
177
|
+
job_id: "legacy-export-9",
|
|
178
|
+
}),
|
|
179
|
+
{ status: 409 },
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
globalThis.fetch = fetchMock;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await localRuntimeExportToGcs(RUNTIME_URL, TOKEN, {
|
|
186
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
187
|
+
});
|
|
188
|
+
throw new Error("expected to throw");
|
|
189
|
+
} catch (err) {
|
|
190
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
191
|
+
const mip = err as MigrationInProgressError;
|
|
192
|
+
expect(mip.kind).toBe("export_in_progress");
|
|
193
|
+
expect(mip.existingJobId).toBe("legacy-export-9");
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("non-202 non-409 responses throw with status + body", async () => {
|
|
198
|
+
const { fetchMock } = captureFetch(() => {
|
|
199
|
+
return new Response("boom", { status: 500 });
|
|
200
|
+
});
|
|
201
|
+
globalThis.fetch = fetchMock;
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
localRuntimeExportToGcs(RUNTIME_URL, TOKEN, {
|
|
205
|
+
uploadUrl: "https://storage.example/signed/abc",
|
|
206
|
+
}),
|
|
207
|
+
).rejects.toThrow(/500/);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("localRuntimeImportFromGcs", () => {
|
|
212
|
+
test("POSTs {bundle_url} with Bearer auth and returns job_id on 202", async () => {
|
|
213
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
214
|
+
return new Response(
|
|
215
|
+
JSON.stringify({
|
|
216
|
+
job_id: "import-job-1",
|
|
217
|
+
status: "pending",
|
|
218
|
+
type: "import",
|
|
219
|
+
}),
|
|
220
|
+
{ status: 202 },
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
globalThis.fetch = fetchMock;
|
|
224
|
+
|
|
225
|
+
const result = await localRuntimeImportFromGcs(RUNTIME_URL, TOKEN, {
|
|
226
|
+
bundleUrl: "https://storage.example/signed/dl-xyz",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result.jobId).toBe("import-job-1");
|
|
230
|
+
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/migrations/import-from-gcs`);
|
|
231
|
+
expect(calls[0]!.method).toBe("POST");
|
|
232
|
+
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
233
|
+
expect(calls[0]!.body).toEqual({
|
|
234
|
+
bundle_url: "https://storage.example/signed/dl-xyz",
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("409 import_in_progress (nested {error:{code,job_id}}) throws MigrationInProgressError carrying existing job_id", async () => {
|
|
239
|
+
const { fetchMock } = captureFetch(() => {
|
|
240
|
+
return new Response(
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
error: {
|
|
243
|
+
code: "import_in_progress",
|
|
244
|
+
job_id: "existing-import-7",
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
{ status: 409 },
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
globalThis.fetch = fetchMock;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
await localRuntimeImportFromGcs(RUNTIME_URL, TOKEN, {
|
|
254
|
+
bundleUrl: "https://storage.example/signed/dl-xyz",
|
|
255
|
+
});
|
|
256
|
+
throw new Error("expected to throw");
|
|
257
|
+
} catch (err) {
|
|
258
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
259
|
+
const mip = err as MigrationInProgressError;
|
|
260
|
+
expect(mip.kind).toBe("import_in_progress");
|
|
261
|
+
expect(mip.existingJobId).toBe("existing-import-7");
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("409 import_in_progress with legacy flat shape is still parsed", async () => {
|
|
266
|
+
const { fetchMock } = captureFetch(() => {
|
|
267
|
+
return new Response(
|
|
268
|
+
JSON.stringify({
|
|
269
|
+
code: "import_in_progress",
|
|
270
|
+
job_id: "legacy-import-2",
|
|
271
|
+
}),
|
|
272
|
+
{ status: 409 },
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
globalThis.fetch = fetchMock;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
await localRuntimeImportFromGcs(RUNTIME_URL, TOKEN, {
|
|
279
|
+
bundleUrl: "https://storage.example/signed/dl-xyz",
|
|
280
|
+
});
|
|
281
|
+
throw new Error("expected to throw");
|
|
282
|
+
} catch (err) {
|
|
283
|
+
expect(err).toBeInstanceOf(MigrationInProgressError);
|
|
284
|
+
const mip = err as MigrationInProgressError;
|
|
285
|
+
expect(mip.kind).toBe("import_in_progress");
|
|
286
|
+
expect(mip.existingJobId).toBe("legacy-import-2");
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("localRuntimePollJobStatus", () => {
|
|
292
|
+
test("GETs /v1/migrations/jobs/{jobId} with Bearer auth and parses processing", async () => {
|
|
293
|
+
const { calls, fetchMock } = captureFetch(() => {
|
|
294
|
+
return new Response(
|
|
295
|
+
JSON.stringify({
|
|
296
|
+
job_id: "poll-1",
|
|
297
|
+
type: "export",
|
|
298
|
+
status: "processing",
|
|
299
|
+
}),
|
|
300
|
+
{ status: 200 },
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
globalThis.fetch = fetchMock;
|
|
304
|
+
|
|
305
|
+
const status = await localRuntimePollJobStatus(
|
|
306
|
+
RUNTIME_URL,
|
|
307
|
+
TOKEN,
|
|
308
|
+
"poll-1",
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(status).toEqual({
|
|
312
|
+
jobId: "poll-1",
|
|
313
|
+
type: "export",
|
|
314
|
+
status: "processing",
|
|
315
|
+
});
|
|
316
|
+
expect(calls[0]!.url).toBe(`${RUNTIME_URL}/v1/migrations/jobs/poll-1`);
|
|
317
|
+
expect(calls[0]!.method).toBe("GET");
|
|
318
|
+
expect(calls[0]!.headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("parses complete with bundle_key", async () => {
|
|
322
|
+
const { fetchMock } = captureFetch(() => {
|
|
323
|
+
return new Response(
|
|
324
|
+
JSON.stringify({
|
|
325
|
+
job_id: "poll-2",
|
|
326
|
+
type: "export",
|
|
327
|
+
status: "complete",
|
|
328
|
+
bundle_key: "bundles/x.tar.gz",
|
|
329
|
+
}),
|
|
330
|
+
{ status: 200 },
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
globalThis.fetch = fetchMock;
|
|
334
|
+
|
|
335
|
+
const status = await localRuntimePollJobStatus(
|
|
336
|
+
RUNTIME_URL,
|
|
337
|
+
TOKEN,
|
|
338
|
+
"poll-2",
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(status.status).toBe("complete");
|
|
342
|
+
if (status.status === "complete") {
|
|
343
|
+
expect(status.bundleKey).toBe("bundles/x.tar.gz");
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("parses failed with error", async () => {
|
|
348
|
+
const { fetchMock } = captureFetch(() => {
|
|
349
|
+
return new Response(
|
|
350
|
+
JSON.stringify({
|
|
351
|
+
job_id: "poll-3",
|
|
352
|
+
type: "import",
|
|
353
|
+
status: "failed",
|
|
354
|
+
error: "corrupted bundle",
|
|
355
|
+
}),
|
|
356
|
+
{ status: 200 },
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
globalThis.fetch = fetchMock;
|
|
360
|
+
|
|
361
|
+
const status = await localRuntimePollJobStatus(
|
|
362
|
+
RUNTIME_URL,
|
|
363
|
+
TOKEN,
|
|
364
|
+
"poll-3",
|
|
365
|
+
);
|
|
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(RUNTIME_URL, TOKEN, "missing"),
|
|
381
|
+
).rejects.toThrow(/Migration job not found/);
|
|
382
|
+
});
|
|
383
|
+
});
|