@vellumai/cli 0.10.4-dev.202607022227.14c7770 → 0.10.4-staging.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/node_modules/@vellumai/local-mode/src/index.ts +3 -12
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +2 -11
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +0 -6
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +1 -29
- package/node_modules/@vellumai/local-mode/src/retire.ts +5 -27
- package/package.json +1 -1
- package/src/__tests__/platform-releases.test.ts +0 -42
- package/src/__tests__/retire.test.ts +0 -353
- package/src/__tests__/teleport.test.ts +0 -8
- package/src/__tests__/workos-pkce.test.ts +7 -0
- package/src/commands/hatch.ts +1 -9
- package/src/commands/login.ts +2 -10
- package/src/commands/retire.ts +24 -197
- package/src/commands/teleport.ts +2 -9
- package/src/lib/docker.ts +4 -22
- package/src/lib/platform-client.ts +0 -14
- package/src/lib/platform-releases.ts +6 -38
- package/src/lib/workos-pkce.ts +2 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/retire.test.ts +0 -79
|
@@ -14,18 +14,9 @@ export {
|
|
|
14
14
|
resolveDevCliInvocation,
|
|
15
15
|
} from "./util";
|
|
16
16
|
export type { CliInvocation } from "./util";
|
|
17
|
-
export {
|
|
18
|
-
resolveLocalConfigFromEnv,
|
|
19
|
-
resolveLockfilePaths,
|
|
20
|
-
resolveConfigDir,
|
|
21
|
-
guardianTokenPath,
|
|
22
|
-
} from "./config";
|
|
17
|
+
export { resolveLocalConfigFromEnv, resolveLockfilePaths, resolveConfigDir, guardianTokenPath } from "./config";
|
|
23
18
|
export type { LocalEndpointConfig } from "./config";
|
|
24
|
-
export {
|
|
25
|
-
defaultEnvironmentFilePath,
|
|
26
|
-
readDefaultEnvironment,
|
|
27
|
-
resolveEnvironmentName,
|
|
28
|
-
} from "./environment";
|
|
19
|
+
export { defaultEnvironmentFilePath, readDefaultEnvironment, resolveEnvironmentName } from "./environment";
|
|
29
20
|
export {
|
|
30
21
|
getLockfileData,
|
|
31
22
|
upsertLockfileAssistant,
|
|
@@ -43,7 +34,7 @@ export type {
|
|
|
43
34
|
export { runHatch } from "./hatch";
|
|
44
35
|
export type { HatchResult } from "./hatch";
|
|
45
36
|
export { runRetire } from "./retire";
|
|
46
|
-
export type {
|
|
37
|
+
export type { RetireResult } from "./retire";
|
|
47
38
|
export { runSleep } from "./sleep";
|
|
48
39
|
export type { SleepResult } from "./sleep";
|
|
49
40
|
export { runWake } from "./wake";
|
|
@@ -21,9 +21,6 @@ describe("parseLockfile", () => {
|
|
|
21
21
|
species: "vellum",
|
|
22
22
|
hatchedAt: "2026-01-01T00:00:00.000Z",
|
|
23
23
|
organizationId: "org_1",
|
|
24
|
-
platformAssistantId: "platform-assistant-1",
|
|
25
|
-
platformBaseUrl: "https://platform.example.com",
|
|
26
|
-
platformOrganizationId: "org_1",
|
|
27
24
|
resources: { gatewayPort: 7777, daemonPort: 7778 },
|
|
28
25
|
},
|
|
29
26
|
],
|
|
@@ -277,19 +274,13 @@ describe("parseLockfile", () => {
|
|
|
277
274
|
expect(parseLockfile(raw).assistants).toEqual([
|
|
278
275
|
{ assistantId: "gcp_1", cloud: "gcp", runtimeUrl: "https://a" },
|
|
279
276
|
{ assistantId: "ssh_1", cloud: "custom", runtimeUrl: "https://b" },
|
|
280
|
-
{
|
|
281
|
-
assistantId: "local_1",
|
|
282
|
-
cloud: "local",
|
|
283
|
-
runtimeUrl: "http://localhost:7830",
|
|
284
|
-
},
|
|
277
|
+
{ assistantId: "local_1", cloud: "local", runtimeUrl: "http://localhost:7830" },
|
|
285
278
|
]);
|
|
286
279
|
});
|
|
287
280
|
|
|
288
281
|
test("prefers an explicit cloud over legacy remote markers", () => {
|
|
289
282
|
const raw = {
|
|
290
|
-
assistants: [
|
|
291
|
-
{ assistantId: "a", cloud: "vellum", project: "stale-proj" },
|
|
292
|
-
],
|
|
283
|
+
assistants: [{ assistantId: "a", cloud: "vellum", project: "stale-proj" }],
|
|
293
284
|
activeAssistant: null,
|
|
294
285
|
};
|
|
295
286
|
expect(parseLockfile(raw).assistants).toEqual([
|
|
@@ -109,12 +109,6 @@ export const LockfileAssistantSchema = z.object({
|
|
|
109
109
|
hatchedAt: z.string().optional(),
|
|
110
110
|
/** Owning org for platform assistants; absent for local ones. */
|
|
111
111
|
organizationId: z.string().optional(),
|
|
112
|
-
/** Platform assistant UUID for a self-hosted local assistant registration. */
|
|
113
|
-
platformAssistantId: z.string().optional(),
|
|
114
|
-
/** Platform base URL used for a self-hosted local assistant registration. */
|
|
115
|
-
platformBaseUrl: z.string().optional(),
|
|
116
|
-
/** Platform organization UUID used for a self-hosted local assistant registration. */
|
|
117
|
-
platformOrganizationId: z.string().optional(),
|
|
118
112
|
resources: LocalAssistantResourcesSchema.optional(),
|
|
119
113
|
});
|
|
120
114
|
|
|
@@ -171,32 +171,6 @@ describe("upsertLockfileAssistant", () => {
|
|
|
171
171
|
name: "Renamed",
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
|
-
|
|
175
|
-
test("preserves activeAssistant when no active id is provided", () => {
|
|
176
|
-
writeOnDisk({
|
|
177
|
-
activeAssistant: "asst_active",
|
|
178
|
-
assistants: [
|
|
179
|
-
{
|
|
180
|
-
assistantId: "asst_active",
|
|
181
|
-
cloud: "local",
|
|
182
|
-
runtimeUrl: "http://active",
|
|
183
|
-
},
|
|
184
|
-
{ assistantId: "asst_1", cloud: "local", runtimeUrl: "http://a" },
|
|
185
|
-
],
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const result = upsertLockfileAssistant(
|
|
189
|
-
[lockfilePath],
|
|
190
|
-
{ assistantId: "asst_1", name: "Renamed" },
|
|
191
|
-
undefined,
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
expect(result.ok).toBe(true);
|
|
195
|
-
expect(readOnDisk().activeAssistant).toBe("asst_active");
|
|
196
|
-
if (result.ok) {
|
|
197
|
-
expect(result.lockfile.activeAssistant).toBe("asst_active");
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
174
|
});
|
|
201
175
|
|
|
202
176
|
describe("replacePlatformAssistants", () => {
|
|
@@ -296,9 +270,7 @@ describe("replacePlatformAssistants", () => {
|
|
|
296
270
|
"org_a",
|
|
297
271
|
);
|
|
298
272
|
|
|
299
|
-
const assistants = readOnDisk().assistants as Array<
|
|
300
|
-
Record<string, unknown>
|
|
301
|
-
>;
|
|
273
|
+
const assistants = readOnDisk().assistants as Array<Record<string, unknown>>;
|
|
302
274
|
expect(assistants).toHaveLength(1);
|
|
303
275
|
expect(assistants[0]).toMatchObject({
|
|
304
276
|
assistantId: "asst_dup",
|
|
@@ -5,32 +5,18 @@ import type { CliInvocation } from "./util";
|
|
|
5
5
|
const RETIRE_TIMEOUT_MS = 60_000;
|
|
6
6
|
|
|
7
7
|
export type RetireResult =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export interface RetireOptions {
|
|
11
|
-
platformToken?: string;
|
|
12
|
-
}
|
|
8
|
+
| { ok: true }
|
|
9
|
+
| { ok: false; status: number; error: string };
|
|
13
10
|
|
|
14
11
|
export function runRetire(
|
|
15
12
|
invocation: CliInvocation,
|
|
16
13
|
assistantId: string,
|
|
17
|
-
options: RetireOptions = {},
|
|
18
14
|
): Promise<RetireResult> {
|
|
19
15
|
return new Promise((resolve) => {
|
|
20
16
|
const child = spawn(
|
|
21
17
|
invocation.command,
|
|
22
18
|
[...invocation.baseArgs, "retire", assistantId, "--yes"],
|
|
23
|
-
{
|
|
24
|
-
...(options.platformToken
|
|
25
|
-
? {
|
|
26
|
-
env: {
|
|
27
|
-
...process.env,
|
|
28
|
-
VELLUM_PLATFORM_TOKEN: options.platformToken,
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
: {}),
|
|
32
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
-
},
|
|
19
|
+
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
34
20
|
);
|
|
35
21
|
|
|
36
22
|
let stdout = "";
|
|
@@ -46,11 +32,7 @@ export function runRetire(
|
|
|
46
32
|
|
|
47
33
|
const timeout = setTimeout(() => {
|
|
48
34
|
child.kill("SIGTERM");
|
|
49
|
-
finish({
|
|
50
|
-
ok: false,
|
|
51
|
-
status: 500,
|
|
52
|
-
error: "Retire timed out after 60 seconds",
|
|
53
|
-
});
|
|
35
|
+
finish({ ok: false, status: 500, error: "Retire timed out after 60 seconds" });
|
|
54
36
|
}, RETIRE_TIMEOUT_MS);
|
|
55
37
|
|
|
56
38
|
child.stdout.on("data", (data: Buffer) => {
|
|
@@ -70,11 +52,7 @@ export function runRetire(
|
|
|
70
52
|
});
|
|
71
53
|
|
|
72
54
|
child.on("error", (err) => {
|
|
73
|
-
finish({
|
|
74
|
-
ok: false,
|
|
75
|
-
status: 500,
|
|
76
|
-
error: `Failed to spawn CLI: ${err.message}`,
|
|
77
|
-
});
|
|
55
|
+
finish({ ok: false, status: 500, error: `Failed to spawn CLI: ${err.message}` });
|
|
78
56
|
});
|
|
79
57
|
});
|
|
80
58
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
fetchLatestVersion,
|
|
5
4
|
fetchReleases,
|
|
6
5
|
resolveImageRefsDetailed,
|
|
7
6
|
} from "../lib/platform-releases.js";
|
|
@@ -115,45 +114,4 @@ describe("resolveImageRefsDetailed", () => {
|
|
|
115
114
|
const result = await resolveImageRefsDetailed("0.7.0");
|
|
116
115
|
expect(result.status).toBe("dockerhub-fallback");
|
|
117
116
|
});
|
|
118
|
-
|
|
119
|
-
test("requests the preview channel when channel=preview", async () => {
|
|
120
|
-
const fetchMock = mockFetchJson([RELEASE]);
|
|
121
|
-
await resolveImageRefsDetailed("v0.7.0", undefined, "preview");
|
|
122
|
-
const url = String(fetchMock.mock.calls[0][0]);
|
|
123
|
-
expect(url).toContain("channel=preview");
|
|
124
|
-
expect(url).not.toContain("stable=true");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("keeps the stable filter when channel=stable (default)", async () => {
|
|
128
|
-
const fetchMock = mockFetchJson([RELEASE]);
|
|
129
|
-
await resolveImageRefsDetailed("v0.7.0", undefined, "stable");
|
|
130
|
-
const url = String(fetchMock.mock.calls[0][0]);
|
|
131
|
-
expect(url).toContain("stable=true");
|
|
132
|
-
expect(url).not.toContain("channel=");
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("fetchLatestVersion", () => {
|
|
137
|
-
test("defaults to the stable channel", async () => {
|
|
138
|
-
const fetchMock = mockFetchJson([RELEASE]);
|
|
139
|
-
const version = await fetchLatestVersion();
|
|
140
|
-
expect(version).toBe("0.7.0");
|
|
141
|
-
const url = String(fetchMock.mock.calls[0][0]);
|
|
142
|
-
expect(url).toContain("stable=true");
|
|
143
|
-
expect(url).not.toContain("channel=");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("requests the preview channel when asked", async () => {
|
|
147
|
-
const preview = { ...RELEASE, version: "0.8.0-preview.3" };
|
|
148
|
-
const fetchMock = mockFetchJson([preview]);
|
|
149
|
-
const version = await fetchLatestVersion("preview");
|
|
150
|
-
expect(version).toBe("0.8.0-preview.3");
|
|
151
|
-
const url = String(fetchMock.mock.calls[0][0]);
|
|
152
|
-
expect(url).toContain("channel=preview");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("returns null when the channel has no releases", async () => {
|
|
156
|
-
mockFetchJson([]);
|
|
157
|
-
expect(await fetchLatestVersion("preview")).toBeNull();
|
|
158
|
-
});
|
|
159
117
|
});
|
|
@@ -20,74 +20,30 @@ 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";
|
|
25
23
|
import * as retireLocalModule from "../lib/retire-local.js";
|
|
26
24
|
|
|
27
25
|
const testDir = mkdtempSync(join(tmpdir(), "cli-retire-test-"));
|
|
28
26
|
const originalArgv = [...process.argv];
|
|
29
27
|
const originalExit = process.exit;
|
|
30
28
|
const originalLockfileDir = process.env.VELLUM_LOCKFILE_DIR;
|
|
31
|
-
const originalPlatformToken = process.env.VELLUM_PLATFORM_TOKEN;
|
|
32
29
|
const originalStdinIsTTY = process.stdin.isTTY;
|
|
33
30
|
const originalStdoutIsTTY = process.stdout.isTTY;
|
|
34
31
|
const originalStdinIsRaw = process.stdin.isRaw;
|
|
35
32
|
const originalSetRawMode = process.stdin.setRawMode;
|
|
36
33
|
const originalStdoutWrite = process.stdout.write;
|
|
37
34
|
const realRetireLocalModule = { ...retireLocalModule };
|
|
38
|
-
const realPlatformClientModule = { ...platformClientModule };
|
|
39
|
-
const realLoopbackFetchModule = { ...loopbackFetchModule };
|
|
40
35
|
|
|
41
36
|
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
|
-
);
|
|
68
37
|
|
|
69
38
|
mock.module("../lib/retire-local.js", () => ({
|
|
70
39
|
...realRetireLocalModule,
|
|
71
40
|
retireLocal: retireLocalMock,
|
|
72
41
|
}));
|
|
73
42
|
|
|
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
|
-
|
|
86
43
|
import { retire } from "../commands/retire.js";
|
|
87
44
|
|
|
88
45
|
let consoleLogSpy: ReturnType<typeof spyOn>;
|
|
89
46
|
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
|
90
|
-
let consoleWarnSpy: ReturnType<typeof spyOn>;
|
|
91
47
|
|
|
92
48
|
function makeEntry(
|
|
93
49
|
assistantId: string,
|
|
@@ -167,7 +123,6 @@ function restoreTerminal(): void {
|
|
|
167
123
|
describe("vellum retire", () => {
|
|
168
124
|
beforeEach(() => {
|
|
169
125
|
process.env.VELLUM_LOCKFILE_DIR = testDir;
|
|
170
|
-
delete process.env.VELLUM_PLATFORM_TOKEN;
|
|
171
126
|
rmSync(join(testDir, ".vellum.lock.json"), { force: true });
|
|
172
127
|
process.argv = ["bun", "vellum", "retire"];
|
|
173
128
|
process.exit = ((code?: number) => {
|
|
@@ -175,36 +130,9 @@ describe("vellum retire", () => {
|
|
|
175
130
|
}) as typeof process.exit;
|
|
176
131
|
retireLocalMock.mockReset();
|
|
177
132
|
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
|
-
);
|
|
204
133
|
setTerminalMode(false);
|
|
205
134
|
consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
206
135
|
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
207
|
-
consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
208
136
|
});
|
|
209
137
|
|
|
210
138
|
afterEach(() => {
|
|
@@ -213,23 +141,15 @@ describe("vellum retire", () => {
|
|
|
213
141
|
restoreTerminal();
|
|
214
142
|
consoleLogSpy.mockRestore();
|
|
215
143
|
consoleErrorSpy.mockRestore();
|
|
216
|
-
consoleWarnSpy.mockRestore();
|
|
217
144
|
});
|
|
218
145
|
|
|
219
146
|
afterAll(() => {
|
|
220
147
|
mock.module("../lib/retire-local.js", () => realRetireLocalModule);
|
|
221
|
-
mock.module("../lib/platform-client.js", () => realPlatformClientModule);
|
|
222
|
-
mock.module("../lib/loopback-fetch.js", () => realLoopbackFetchModule);
|
|
223
148
|
if (originalLockfileDir === undefined) {
|
|
224
149
|
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
225
150
|
} else {
|
|
226
151
|
process.env.VELLUM_LOCKFILE_DIR = originalLockfileDir;
|
|
227
152
|
}
|
|
228
|
-
if (originalPlatformToken === undefined) {
|
|
229
|
-
delete process.env.VELLUM_PLATFORM_TOKEN;
|
|
230
|
-
} else {
|
|
231
|
-
process.env.VELLUM_PLATFORM_TOKEN = originalPlatformToken;
|
|
232
|
-
}
|
|
233
153
|
rmSync(testDir, { recursive: true, force: true });
|
|
234
154
|
});
|
|
235
155
|
|
|
@@ -250,279 +170,6 @@ describe("vellum retire", () => {
|
|
|
250
170
|
);
|
|
251
171
|
});
|
|
252
172
|
|
|
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
|
-
|
|
526
173
|
test("non-interactive retire without --yes fails before deleting", async () => {
|
|
527
174
|
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
528
175
|
writeLockfile([entry]);
|
|
@@ -2266,14 +2266,6 @@ describe("platform credential injection", () => {
|
|
|
2266
2266
|
userId: "user-1",
|
|
2267
2267
|
webhookSecret: "webhook-secret-123",
|
|
2268
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
|
-
);
|
|
2277
2269
|
} finally {
|
|
2278
2270
|
restoreFetch();
|
|
2279
2271
|
}
|