@vellumai/cli 0.10.3 → 0.10.4-dev.202607010119.ad15dbb
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/node_modules/@vellumai/local-mode/src/__tests__/retire.test.ts +79 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +12 -3
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +11 -2
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +6 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +29 -1
- package/node_modules/@vellumai/local-mode/src/retire.ts +27 -5
- package/package.json +1 -1
- package/src/__tests__/flags.test.ts +10 -10
- package/src/__tests__/recover.test.ts +34 -27
- package/src/__tests__/retire.test.ts +353 -0
- package/src/__tests__/teleport.test.ts +50 -17
- package/src/commands/flags.ts +1 -1
- package/src/commands/login.ts +10 -2
- package/src/commands/retire.ts +197 -24
- package/src/commands/teleport.ts +51 -18
- package/src/lib/local-runtime-client.ts +38 -0
- package/src/lib/platform-client.ts +14 -0
|
@@ -20,30 +20,74 @@ import { join } from "node:path";
|
|
|
20
20
|
|
|
21
21
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
22
22
|
import { loadAllAssistants } from "../lib/assistant-config.js";
|
|
23
|
+
import * as loopbackFetchModule from "../lib/loopback-fetch.js";
|
|
24
|
+
import * as platformClientModule from "../lib/platform-client.js";
|
|
23
25
|
import * as retireLocalModule from "../lib/retire-local.js";
|
|
24
26
|
|
|
25
27
|
const testDir = mkdtempSync(join(tmpdir(), "cli-retire-test-"));
|
|
26
28
|
const originalArgv = [...process.argv];
|
|
27
29
|
const originalExit = process.exit;
|
|
28
30
|
const originalLockfileDir = process.env.VELLUM_LOCKFILE_DIR;
|
|
31
|
+
const originalPlatformToken = process.env.VELLUM_PLATFORM_TOKEN;
|
|
29
32
|
const originalStdinIsTTY = process.stdin.isTTY;
|
|
30
33
|
const originalStdoutIsTTY = process.stdout.isTTY;
|
|
31
34
|
const originalStdinIsRaw = process.stdin.isRaw;
|
|
32
35
|
const originalSetRawMode = process.stdin.setRawMode;
|
|
33
36
|
const originalStdoutWrite = process.stdout.write;
|
|
34
37
|
const realRetireLocalModule = { ...retireLocalModule };
|
|
38
|
+
const realPlatformClientModule = { ...platformClientModule };
|
|
39
|
+
const realLoopbackFetchModule = { ...loopbackFetchModule };
|
|
35
40
|
|
|
36
41
|
const retireLocalMock = mock(async () => {});
|
|
42
|
+
const readPlatformTokenMock = mock((): string | null => null);
|
|
43
|
+
const authHeadersMock = mock(async () => ({
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
"X-Session-Token": "session-token",
|
|
46
|
+
}));
|
|
47
|
+
const authHeadersForKnownOrganizationMock = mock(
|
|
48
|
+
(token: string, organizationId: string) => ({
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"X-Session-Token": token,
|
|
51
|
+
"Vellum-Organization-Id": organizationId,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
const getPlatformUrlMock = mock(() => "https://platform.example.com");
|
|
55
|
+
const readGatewayCredentialMock = mock(
|
|
56
|
+
async (
|
|
57
|
+
_gatewayUrl: string,
|
|
58
|
+
_name: string,
|
|
59
|
+
_bearerToken?: string,
|
|
60
|
+
): Promise<{ value: string | null; unreachable: boolean }> => ({
|
|
61
|
+
value: null,
|
|
62
|
+
unreachable: false,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
const loopbackSafeFetchMock = mock(
|
|
66
|
+
async (): Promise<Response> => new Response(null, { status: 204 }),
|
|
67
|
+
);
|
|
37
68
|
|
|
38
69
|
mock.module("../lib/retire-local.js", () => ({
|
|
39
70
|
...realRetireLocalModule,
|
|
40
71
|
retireLocal: retireLocalMock,
|
|
41
72
|
}));
|
|
42
73
|
|
|
74
|
+
mock.module("../lib/platform-client.js", () => ({
|
|
75
|
+
authHeaders: authHeadersMock,
|
|
76
|
+
authHeadersForKnownOrganization: authHeadersForKnownOrganizationMock,
|
|
77
|
+
getPlatformUrl: getPlatformUrlMock,
|
|
78
|
+
readGatewayCredential: readGatewayCredentialMock,
|
|
79
|
+
readPlatformToken: readPlatformTokenMock,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
mock.module("../lib/loopback-fetch.js", () => ({
|
|
83
|
+
loopbackSafeFetch: loopbackSafeFetchMock,
|
|
84
|
+
}));
|
|
85
|
+
|
|
43
86
|
import { retire } from "../commands/retire.js";
|
|
44
87
|
|
|
45
88
|
let consoleLogSpy: ReturnType<typeof spyOn>;
|
|
46
89
|
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
|
90
|
+
let consoleWarnSpy: ReturnType<typeof spyOn>;
|
|
47
91
|
|
|
48
92
|
function makeEntry(
|
|
49
93
|
assistantId: string,
|
|
@@ -123,6 +167,7 @@ function restoreTerminal(): void {
|
|
|
123
167
|
describe("vellum retire", () => {
|
|
124
168
|
beforeEach(() => {
|
|
125
169
|
process.env.VELLUM_LOCKFILE_DIR = testDir;
|
|
170
|
+
delete process.env.VELLUM_PLATFORM_TOKEN;
|
|
126
171
|
rmSync(join(testDir, ".vellum.lock.json"), { force: true });
|
|
127
172
|
process.argv = ["bun", "vellum", "retire"];
|
|
128
173
|
process.exit = ((code?: number) => {
|
|
@@ -130,9 +175,36 @@ describe("vellum retire", () => {
|
|
|
130
175
|
}) as typeof process.exit;
|
|
131
176
|
retireLocalMock.mockReset();
|
|
132
177
|
retireLocalMock.mockResolvedValue(undefined);
|
|
178
|
+
readPlatformTokenMock.mockReset();
|
|
179
|
+
readPlatformTokenMock.mockReturnValue(null);
|
|
180
|
+
authHeadersMock.mockReset();
|
|
181
|
+
authHeadersMock.mockResolvedValue({
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
"X-Session-Token": "session-token",
|
|
184
|
+
});
|
|
185
|
+
authHeadersForKnownOrganizationMock.mockReset();
|
|
186
|
+
authHeadersForKnownOrganizationMock.mockImplementation(
|
|
187
|
+
(token: string, organizationId: string) => ({
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
"X-Session-Token": token,
|
|
190
|
+
"Vellum-Organization-Id": organizationId,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
getPlatformUrlMock.mockReset();
|
|
194
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.com");
|
|
195
|
+
readGatewayCredentialMock.mockReset();
|
|
196
|
+
readGatewayCredentialMock.mockResolvedValue({
|
|
197
|
+
value: null,
|
|
198
|
+
unreachable: false,
|
|
199
|
+
});
|
|
200
|
+
loopbackSafeFetchMock.mockReset();
|
|
201
|
+
loopbackSafeFetchMock.mockResolvedValue(
|
|
202
|
+
new Response(null, { status: 204 }),
|
|
203
|
+
);
|
|
133
204
|
setTerminalMode(false);
|
|
134
205
|
consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
135
206
|
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
207
|
+
consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
136
208
|
});
|
|
137
209
|
|
|
138
210
|
afterEach(() => {
|
|
@@ -141,15 +213,23 @@ describe("vellum retire", () => {
|
|
|
141
213
|
restoreTerminal();
|
|
142
214
|
consoleLogSpy.mockRestore();
|
|
143
215
|
consoleErrorSpy.mockRestore();
|
|
216
|
+
consoleWarnSpy.mockRestore();
|
|
144
217
|
});
|
|
145
218
|
|
|
146
219
|
afterAll(() => {
|
|
147
220
|
mock.module("../lib/retire-local.js", () => realRetireLocalModule);
|
|
221
|
+
mock.module("../lib/platform-client.js", () => realPlatformClientModule);
|
|
222
|
+
mock.module("../lib/loopback-fetch.js", () => realLoopbackFetchModule);
|
|
148
223
|
if (originalLockfileDir === undefined) {
|
|
149
224
|
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
150
225
|
} else {
|
|
151
226
|
process.env.VELLUM_LOCKFILE_DIR = originalLockfileDir;
|
|
152
227
|
}
|
|
228
|
+
if (originalPlatformToken === undefined) {
|
|
229
|
+
delete process.env.VELLUM_PLATFORM_TOKEN;
|
|
230
|
+
} else {
|
|
231
|
+
process.env.VELLUM_PLATFORM_TOKEN = originalPlatformToken;
|
|
232
|
+
}
|
|
153
233
|
rmSync(testDir, { recursive: true, force: true });
|
|
154
234
|
});
|
|
155
235
|
|
|
@@ -170,6 +250,279 @@ describe("vellum retire", () => {
|
|
|
170
250
|
);
|
|
171
251
|
});
|
|
172
252
|
|
|
253
|
+
test("local retire unregisters the self-hosted platform record first", async () => {
|
|
254
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
255
|
+
writeLockfile([entry]);
|
|
256
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
257
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test/api");
|
|
258
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
259
|
+
if (name === "vellum:platform_assistant_id") {
|
|
260
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
261
|
+
}
|
|
262
|
+
if (name === "vellum:platform_base_url") {
|
|
263
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
264
|
+
}
|
|
265
|
+
if (name === "vellum:platform_organization_id") {
|
|
266
|
+
return { value: "org-123", unreachable: false };
|
|
267
|
+
}
|
|
268
|
+
return { value: null, unreachable: false };
|
|
269
|
+
});
|
|
270
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
271
|
+
|
|
272
|
+
await retire();
|
|
273
|
+
|
|
274
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
275
|
+
entry.localUrl ?? entry.runtimeUrl,
|
|
276
|
+
"vellum:platform_assistant_id",
|
|
277
|
+
entry.bearerToken,
|
|
278
|
+
);
|
|
279
|
+
expect(authHeadersMock).not.toHaveBeenCalled();
|
|
280
|
+
expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
|
|
281
|
+
"https://platform.example.test/api/v1/assistants/platform-assistant-1/retire/",
|
|
282
|
+
{
|
|
283
|
+
method: "DELETE",
|
|
284
|
+
headers: {
|
|
285
|
+
"Content-Type": "application/json",
|
|
286
|
+
"X-Session-Token": "session-token",
|
|
287
|
+
"Vellum-Organization-Id": "org-123",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
292
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("local retire reads platform registration through the loopback gateway URL", async () => {
|
|
296
|
+
const localGatewayUrl = "http://127.0.0.1:7831";
|
|
297
|
+
const entry = makeEntry("assistant-1", {
|
|
298
|
+
name: "Example Assistant",
|
|
299
|
+
runtimeUrl: "http://assistant-1.local:7831",
|
|
300
|
+
localUrl: localGatewayUrl,
|
|
301
|
+
});
|
|
302
|
+
writeLockfile([entry]);
|
|
303
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
304
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
305
|
+
readGatewayCredentialMock.mockImplementation(async (gatewayUrl, name) => {
|
|
306
|
+
expect(gatewayUrl).toBe(localGatewayUrl);
|
|
307
|
+
if (name === "vellum:platform_assistant_id") {
|
|
308
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
309
|
+
}
|
|
310
|
+
if (name === "vellum:platform_base_url") {
|
|
311
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
312
|
+
}
|
|
313
|
+
if (name === "vellum:platform_organization_id") {
|
|
314
|
+
return { value: "org-123", unreachable: false };
|
|
315
|
+
}
|
|
316
|
+
return { value: null, unreachable: false };
|
|
317
|
+
});
|
|
318
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
319
|
+
|
|
320
|
+
await retire();
|
|
321
|
+
|
|
322
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
323
|
+
localGatewayUrl,
|
|
324
|
+
"vellum:platform_assistant_id",
|
|
325
|
+
entry.bearerToken,
|
|
326
|
+
);
|
|
327
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
328
|
+
localGatewayUrl,
|
|
329
|
+
"vellum:platform_base_url",
|
|
330
|
+
entry.bearerToken,
|
|
331
|
+
);
|
|
332
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
333
|
+
localGatewayUrl,
|
|
334
|
+
"vellum:platform_organization_id",
|
|
335
|
+
entry.bearerToken,
|
|
336
|
+
);
|
|
337
|
+
expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
|
|
338
|
+
"https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
|
|
339
|
+
expect.any(Object),
|
|
340
|
+
);
|
|
341
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("local retire uses saved platform registration when local credentials are unreachable", async () => {
|
|
345
|
+
const entry = makeEntry("assistant-1", {
|
|
346
|
+
name: "Example Assistant",
|
|
347
|
+
platformAssistantId: "platform-assistant-1",
|
|
348
|
+
platformBaseUrl: "https://platform.example.test",
|
|
349
|
+
platformOrganizationId: "org-123",
|
|
350
|
+
});
|
|
351
|
+
writeLockfile([entry]);
|
|
352
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
353
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
354
|
+
readGatewayCredentialMock.mockResolvedValue({
|
|
355
|
+
value: null,
|
|
356
|
+
unreachable: true,
|
|
357
|
+
});
|
|
358
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
359
|
+
|
|
360
|
+
await retire();
|
|
361
|
+
|
|
362
|
+
expect(readGatewayCredentialMock).toHaveBeenCalledWith(
|
|
363
|
+
entry.localUrl ?? entry.runtimeUrl,
|
|
364
|
+
"vellum:platform_assistant_id",
|
|
365
|
+
entry.bearerToken,
|
|
366
|
+
);
|
|
367
|
+
expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
|
|
368
|
+
"https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
|
|
369
|
+
{
|
|
370
|
+
method: "DELETE",
|
|
371
|
+
headers: {
|
|
372
|
+
"Content-Type": "application/json",
|
|
373
|
+
"X-Session-Token": "session-token",
|
|
374
|
+
"Vellum-Organization-Id": "org-123",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
379
|
+
"using saved platform registration",
|
|
380
|
+
);
|
|
381
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
382
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("local retire continues when local credentials are unreachable and no saved platform registration exists", async () => {
|
|
386
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
387
|
+
writeLockfile([entry]);
|
|
388
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
389
|
+
readGatewayCredentialMock.mockResolvedValue({
|
|
390
|
+
value: null,
|
|
391
|
+
unreachable: true,
|
|
392
|
+
});
|
|
393
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
394
|
+
|
|
395
|
+
await retire();
|
|
396
|
+
|
|
397
|
+
expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
|
|
398
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
399
|
+
"no saved platform registration is available",
|
|
400
|
+
);
|
|
401
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
402
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("local retire skips platform unregister when stored platform URL origin mismatches", async () => {
|
|
406
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
407
|
+
writeLockfile([entry]);
|
|
408
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
409
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.com");
|
|
410
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
411
|
+
if (name === "vellum:platform_assistant_id") {
|
|
412
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
413
|
+
}
|
|
414
|
+
if (name === "vellum:platform_base_url") {
|
|
415
|
+
return { value: "https://malicious.example.test", unreachable: false };
|
|
416
|
+
}
|
|
417
|
+
if (name === "vellum:platform_organization_id") {
|
|
418
|
+
return { value: "org-123", unreachable: false };
|
|
419
|
+
}
|
|
420
|
+
return { value: null, unreachable: false };
|
|
421
|
+
});
|
|
422
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
423
|
+
|
|
424
|
+
await retire();
|
|
425
|
+
|
|
426
|
+
expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
|
|
427
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
428
|
+
"Stored platform URL does not match the configured platform URL",
|
|
429
|
+
);
|
|
430
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("local retire skips platform unregister when stored organization ID is missing", async () => {
|
|
434
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
435
|
+
writeLockfile([entry]);
|
|
436
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
437
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
438
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
439
|
+
if (name === "vellum:platform_assistant_id") {
|
|
440
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
441
|
+
}
|
|
442
|
+
if (name === "vellum:platform_base_url") {
|
|
443
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
444
|
+
}
|
|
445
|
+
return { value: null, unreachable: false };
|
|
446
|
+
});
|
|
447
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
448
|
+
|
|
449
|
+
await retire();
|
|
450
|
+
|
|
451
|
+
expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
|
|
452
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
453
|
+
"Could not read platform organization ID",
|
|
454
|
+
);
|
|
455
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
456
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("local retire uses injected platform token from the app host", async () => {
|
|
460
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
461
|
+
writeLockfile([entry]);
|
|
462
|
+
process.env.VELLUM_PLATFORM_TOKEN = "host-session-token";
|
|
463
|
+
readPlatformTokenMock.mockReturnValue(null);
|
|
464
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
465
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
466
|
+
if (name === "vellum:platform_assistant_id") {
|
|
467
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
468
|
+
}
|
|
469
|
+
if (name === "vellum:platform_base_url") {
|
|
470
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
471
|
+
}
|
|
472
|
+
if (name === "vellum:platform_organization_id") {
|
|
473
|
+
return { value: "org-123", unreachable: false };
|
|
474
|
+
}
|
|
475
|
+
return { value: null, unreachable: false };
|
|
476
|
+
});
|
|
477
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
478
|
+
|
|
479
|
+
await retire();
|
|
480
|
+
|
|
481
|
+
expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
|
|
482
|
+
"https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
|
|
483
|
+
{
|
|
484
|
+
method: "DELETE",
|
|
485
|
+
headers: {
|
|
486
|
+
"Content-Type": "application/json",
|
|
487
|
+
"X-Session-Token": "host-session-token",
|
|
488
|
+
"Vellum-Organization-Id": "org-123",
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("local retire continues when self-hosted platform unregister fails", async () => {
|
|
496
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
497
|
+
writeLockfile([entry]);
|
|
498
|
+
readPlatformTokenMock.mockReturnValue("session-token");
|
|
499
|
+
getPlatformUrlMock.mockReturnValue("https://platform.example.test");
|
|
500
|
+
readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
|
|
501
|
+
if (name === "vellum:platform_assistant_id") {
|
|
502
|
+
return { value: "platform-assistant-1", unreachable: false };
|
|
503
|
+
}
|
|
504
|
+
if (name === "vellum:platform_base_url") {
|
|
505
|
+
return { value: "https://platform.example.test", unreachable: false };
|
|
506
|
+
}
|
|
507
|
+
if (name === "vellum:platform_organization_id") {
|
|
508
|
+
return { value: "org-123", unreachable: false };
|
|
509
|
+
}
|
|
510
|
+
return { value: null, unreachable: false };
|
|
511
|
+
});
|
|
512
|
+
loopbackSafeFetchMock.mockResolvedValue(
|
|
513
|
+
new Response("platform unavailable", { status: 503 }),
|
|
514
|
+
);
|
|
515
|
+
process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
|
|
516
|
+
|
|
517
|
+
await retire();
|
|
518
|
+
|
|
519
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
520
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
521
|
+
expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
|
|
522
|
+
"Platform self-hosted unregister failed (503): platform unavailable",
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
|
|
173
526
|
test("non-interactive retire without --yes fails before deleting", async () => {
|
|
174
527
|
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
175
528
|
writeLockfile([entry]);
|
|
@@ -259,12 +259,27 @@ const localRuntimePollJobStatusMock = mock<
|
|
|
259
259
|
},
|
|
260
260
|
}));
|
|
261
261
|
|
|
262
|
+
const localRuntimePreflightFromGcsMock = mock<
|
|
263
|
+
typeof localRuntimeClient.localRuntimePreflightFromGcs
|
|
264
|
+
>(async () => ({
|
|
265
|
+
can_import: true,
|
|
266
|
+
summary: {
|
|
267
|
+
files_to_create: 2,
|
|
268
|
+
files_to_overwrite: 1,
|
|
269
|
+
files_unchanged: 0,
|
|
270
|
+
total_files: 3,
|
|
271
|
+
},
|
|
272
|
+
files: [],
|
|
273
|
+
conflicts: [],
|
|
274
|
+
}));
|
|
275
|
+
|
|
262
276
|
mock.module("../lib/local-runtime-client.js", () => ({
|
|
263
277
|
...realLocalRuntimeClient,
|
|
264
278
|
localRuntimeExportToGcs: localRuntimeExportToGcsMock,
|
|
265
279
|
localRuntimeImportFromGcs: localRuntimeImportFromGcsMock,
|
|
266
280
|
localRuntimeIdentity: localRuntimeIdentityMock,
|
|
267
281
|
localRuntimePollJobStatus: localRuntimePollJobStatusMock,
|
|
282
|
+
localRuntimePreflightFromGcs: localRuntimePreflightFromGcsMock,
|
|
268
283
|
}));
|
|
269
284
|
|
|
270
285
|
const hatchLocalMock = mock(async () => {});
|
|
@@ -483,6 +498,18 @@ beforeEach(() => {
|
|
|
483
498
|
localRuntimeImportFromGcsMock.mockResolvedValue({
|
|
484
499
|
jobId: "local-import-job-1",
|
|
485
500
|
});
|
|
501
|
+
localRuntimePreflightFromGcsMock.mockReset();
|
|
502
|
+
localRuntimePreflightFromGcsMock.mockResolvedValue({
|
|
503
|
+
can_import: true,
|
|
504
|
+
summary: {
|
|
505
|
+
files_to_create: 2,
|
|
506
|
+
files_to_overwrite: 1,
|
|
507
|
+
files_unchanged: 0,
|
|
508
|
+
total_files: 3,
|
|
509
|
+
},
|
|
510
|
+
files: [],
|
|
511
|
+
conflicts: [],
|
|
512
|
+
});
|
|
486
513
|
localRuntimeIdentityMock.mockReset();
|
|
487
514
|
localRuntimeIdentityMock.mockResolvedValue({ version: "0.6.5" });
|
|
488
515
|
localRuntimePollJobStatusMock.mockReset();
|
|
@@ -1932,7 +1959,7 @@ describe("dry-run", () => {
|
|
|
1932
1959
|
}
|
|
1933
1960
|
});
|
|
1934
1961
|
|
|
1935
|
-
test("dry-run
|
|
1962
|
+
test("dry-run with existing local target calls preflight-from-gcs on runtime", async () => {
|
|
1936
1963
|
setArgv("--from", "my-platform", "--local", "my-local", "--dry-run");
|
|
1937
1964
|
|
|
1938
1965
|
const platformEntry = makeEntry("my-platform", {
|
|
@@ -1947,26 +1974,24 @@ describe("dry-run", () => {
|
|
|
1947
1974
|
return null;
|
|
1948
1975
|
});
|
|
1949
1976
|
|
|
1950
|
-
platformPollJobStatusMock.mockResolvedValue({
|
|
1951
|
-
jobId: "platform-export-job-1",
|
|
1952
|
-
type: "export",
|
|
1953
|
-
status: "complete",
|
|
1954
|
-
bundleKey: "bundle-key-from-platform",
|
|
1955
|
-
});
|
|
1956
|
-
|
|
1957
1977
|
const restoreFetch = installTrackingFetch();
|
|
1958
1978
|
try {
|
|
1959
|
-
await
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1979
|
+
await teleport();
|
|
1980
|
+
|
|
1981
|
+
// Preflight was run against the local runtime
|
|
1982
|
+
expect(localRuntimePreflightFromGcsMock).toHaveBeenCalledTimes(1);
|
|
1983
|
+
expect(localRuntimePreflightFromGcsMock).toHaveBeenCalledWith(
|
|
1984
|
+
expect.objectContaining({ assistantId: "my-local" }),
|
|
1985
|
+
expect.any(String),
|
|
1986
|
+
{ bundleUrl: "https://storage.googleapis.com/bucket/signed-download" },
|
|
1964
1987
|
);
|
|
1965
1988
|
|
|
1966
|
-
//
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1989
|
+
// No actual import happened — dry-run only
|
|
1990
|
+
expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
|
|
1991
|
+
|
|
1992
|
+
// Source was not retired
|
|
1993
|
+
expect(retireLocalMock).not.toHaveBeenCalled();
|
|
1994
|
+
expect(retireDockerMock).not.toHaveBeenCalled();
|
|
1970
1995
|
} finally {
|
|
1971
1996
|
restoreFetch();
|
|
1972
1997
|
}
|
|
@@ -2241,6 +2266,14 @@ describe("platform credential injection", () => {
|
|
|
2241
2266
|
userId: "user-1",
|
|
2242
2267
|
webhookSecret: "webhook-secret-123",
|
|
2243
2268
|
});
|
|
2269
|
+
expect(saveAssistantEntryMock).toHaveBeenCalledWith(
|
|
2270
|
+
expect.objectContaining({
|
|
2271
|
+
assistantId: "my-local",
|
|
2272
|
+
platformAssistantId: "platform-assistant-1",
|
|
2273
|
+
platformBaseUrl: "https://platform.vellum.ai",
|
|
2274
|
+
platformOrganizationId: "org-1",
|
|
2275
|
+
}),
|
|
2276
|
+
);
|
|
2244
2277
|
} finally {
|
|
2245
2278
|
restoreFetch();
|
|
2246
2279
|
}
|
package/src/commands/flags.ts
CHANGED
|
@@ -98,7 +98,7 @@ function printHelp(): void {
|
|
|
98
98
|
" $ vellum flags set voice-mode true # enable a flag",
|
|
99
99
|
);
|
|
100
100
|
console.log(
|
|
101
|
-
" $ vellum flags set
|
|
101
|
+
" $ vellum flags set voice-mode true --assistant eval-1 # target by name/id",
|
|
102
102
|
);
|
|
103
103
|
}
|
|
104
104
|
|
package/src/commands/login.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
loadAllAssistants,
|
|
8
8
|
removeAssistantEntry,
|
|
9
9
|
resolveAssistant,
|
|
10
|
+
saveAssistantEntry,
|
|
10
11
|
setActiveAssistant,
|
|
11
12
|
} from "../lib/assistant-config";
|
|
12
13
|
import { computeDeviceId } from "../lib/guardian-token";
|
|
@@ -383,6 +384,7 @@ export async function login(): Promise<void> {
|
|
|
383
384
|
fetchCurrentVersion(entry.runtimeUrl),
|
|
384
385
|
fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
|
|
385
386
|
]);
|
|
387
|
+
const platformUrl = getPlatformUrl();
|
|
386
388
|
const registration = await ensureSelfHostedLocalRegistration(
|
|
387
389
|
token,
|
|
388
390
|
orgId,
|
|
@@ -390,9 +392,15 @@ export async function login(): Promise<void> {
|
|
|
390
392
|
entry.assistantId,
|
|
391
393
|
"cli",
|
|
392
394
|
assistantVersion,
|
|
393
|
-
|
|
395
|
+
platformUrl,
|
|
394
396
|
ingressUrl,
|
|
395
397
|
);
|
|
398
|
+
saveAssistantEntry({
|
|
399
|
+
...entry,
|
|
400
|
+
platformAssistantId: registration.assistant.id,
|
|
401
|
+
platformBaseUrl: platformUrl,
|
|
402
|
+
platformOrganizationId: orgId,
|
|
403
|
+
});
|
|
396
404
|
console.log(
|
|
397
405
|
`Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
|
|
398
406
|
);
|
|
@@ -433,7 +441,7 @@ export async function login(): Promise<void> {
|
|
|
433
441
|
bearerToken: entry.bearerToken,
|
|
434
442
|
assistantApiKey,
|
|
435
443
|
platformAssistantId: registration.assistant.id,
|
|
436
|
-
platformBaseUrl:
|
|
444
|
+
platformBaseUrl: platformUrl,
|
|
437
445
|
organizationId: orgId,
|
|
438
446
|
userId: user.id,
|
|
439
447
|
webhookSecret: registration.webhook_secret,
|