@vellumai/cli 0.8.1 → 0.8.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.
@@ -20,7 +20,19 @@ const testDir = mkdtempSync(join(tmpdir(), "cli-teleport-test-"));
20
20
  process.env.VELLUM_LOCKFILE_DIR = testDir;
21
21
 
22
22
  // ---------------------------------------------------------------------------
23
- // Mocks — must be set up before importing the module under test
23
+ // Mocks — must be set up before importing the module under test.
24
+ //
25
+ // We use Bun's `mock.module()` API (rather than `spyOn` on the imported
26
+ // module namespace). `spyOn` mutates the shared module namespace, and
27
+ // `mockRestore()` in `afterAll` was not reliably restoring on CI — the
28
+ // mutations leaked into sibling test files (notably `guardian-token.test.ts`)
29
+ // and caused flaky failures when the runtime function for that file returned
30
+ // teleport's mock value instead of the real implementation.
31
+ //
32
+ // `mock.module()` registers a per-module factory in Bun's loader. To prevent
33
+ // the mock from leaking across files in the same `bun test` run, we capture
34
+ // the real exports up front and re-register them in `afterAll` to restore
35
+ // the original module bindings.
24
36
  // ---------------------------------------------------------------------------
25
37
 
26
38
  import * as assistantConfig from "../lib/assistant-config.js";
@@ -28,83 +40,95 @@ import * as guardianToken from "../lib/guardian-token.js";
28
40
  import * as platformClient from "../lib/platform-client.js";
29
41
  import * as localRuntimeClient from "../lib/local-runtime-client.js";
30
42
 
31
- const findAssistantByNameMock = spyOn(
32
- assistantConfig,
33
- "findAssistantByName",
34
- ).mockReturnValue(null);
35
-
36
- const saveAssistantEntryMock = spyOn(
37
- assistantConfig,
38
- "saveAssistantEntry",
39
- ).mockImplementation(() => {});
40
-
41
- const loadAllAssistantsMock = spyOn(
42
- assistantConfig,
43
- "loadAllAssistants",
44
- ).mockReturnValue([]);
45
-
46
- const removeAssistantEntryMock = spyOn(
47
- assistantConfig,
48
- "removeAssistantEntry",
49
- ).mockImplementation(() => {});
50
-
51
- const loadGuardianTokenMock = spyOn(
52
- guardianToken,
53
- "loadGuardianToken",
54
- ).mockReturnValue({
55
- accessToken: "local-token",
56
- accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
57
- } as unknown as ReturnType<typeof guardianToken.loadGuardianToken>);
58
-
59
- const leaseGuardianTokenMock = spyOn(
60
- guardianToken,
61
- "leaseGuardianToken",
62
- ).mockResolvedValue({
63
- accessToken: "leased-token",
64
- accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
65
- } as unknown as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>);
66
-
67
- const computeDeviceIdMock = spyOn(
68
- guardianToken,
69
- "computeDeviceId",
70
- ).mockReturnValue("device-id-123");
71
-
72
- const readPlatformTokenMock = spyOn(
73
- platformClient,
74
- "readPlatformToken",
75
- ).mockReturnValue("platform-token");
76
-
77
- const getPlatformUrlMock = spyOn(
78
- platformClient,
79
- "getPlatformUrl",
80
- ).mockReturnValue("https://platform.vellum.ai");
81
-
82
- const hatchAssistantMock = spyOn(
83
- platformClient,
84
- "hatchAssistant",
85
- ).mockResolvedValue({
86
- assistant: {
87
- id: "platform-new-id",
88
- name: "platform-new",
89
- status: "active",
90
- },
91
- reusedExisting: false,
92
- });
43
+ // Snapshot the real exports before any `mock.module()` call so we can
44
+ // reliably restore them after this file's tests complete, regardless of
45
+ // how Bun's loader treats namespace bindings post-mock.
46
+ const realAssistantConfig = { ...assistantConfig };
47
+ const realGuardianToken = { ...guardianToken };
48
+ const realPlatformClient = { ...platformClient };
49
+ const realLocalRuntimeClient = { ...localRuntimeClient };
50
+
51
+ const findAssistantByNameMock = mock<
52
+ typeof assistantConfig.findAssistantByName
53
+ >(() => null);
54
+ const saveAssistantEntryMock = mock<typeof assistantConfig.saveAssistantEntry>(
55
+ () => {},
56
+ );
57
+ const loadAllAssistantsMock = mock<typeof assistantConfig.loadAllAssistants>(
58
+ () => [],
59
+ );
60
+ const removeAssistantEntryMock = mock<
61
+ typeof assistantConfig.removeAssistantEntry
62
+ >(() => {});
63
+
64
+ mock.module("../lib/assistant-config.js", () => ({
65
+ ...realAssistantConfig,
66
+ findAssistantByName: findAssistantByNameMock,
67
+ saveAssistantEntry: saveAssistantEntryMock,
68
+ loadAllAssistants: loadAllAssistantsMock,
69
+ removeAssistantEntry: removeAssistantEntryMock,
70
+ }));
71
+
72
+ const loadGuardianTokenMock = mock<typeof guardianToken.loadGuardianToken>(
73
+ () =>
74
+ ({
75
+ accessToken: "local-token",
76
+ accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
77
+ }) as unknown as ReturnType<typeof guardianToken.loadGuardianToken>,
78
+ );
79
+
80
+ const leaseGuardianTokenMock = mock<typeof guardianToken.leaseGuardianToken>(
81
+ async () =>
82
+ ({
83
+ accessToken: "leased-token",
84
+ accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
85
+ }) as unknown as Awaited<
86
+ ReturnType<typeof guardianToken.leaseGuardianToken>
87
+ >,
88
+ );
89
+
90
+ const computeDeviceIdMock = mock<typeof guardianToken.computeDeviceId>(
91
+ () => "device-id-123",
92
+ );
93
+
94
+ mock.module("../lib/guardian-token.js", () => ({
95
+ ...realGuardianToken,
96
+ loadGuardianToken: loadGuardianTokenMock,
97
+ leaseGuardianToken: leaseGuardianTokenMock,
98
+ computeDeviceId: computeDeviceIdMock,
99
+ }));
100
+
101
+ const readPlatformTokenMock = mock<typeof platformClient.readPlatformToken>(
102
+ () => "platform-token",
103
+ );
104
+
105
+ const getPlatformUrlMock = mock<typeof platformClient.getPlatformUrl>(
106
+ () => "https://platform.vellum.ai",
107
+ );
108
+
109
+ const hatchAssistantMock = mock<typeof platformClient.hatchAssistant>(
110
+ async () => ({
111
+ assistant: {
112
+ id: "platform-new-id",
113
+ name: "platform-new",
114
+ status: "active",
115
+ },
116
+ reusedExisting: false,
117
+ }),
118
+ );
93
119
 
94
- const platformPollJobStatusMock = spyOn(
95
- platformClient,
96
- "platformPollJobStatus",
97
- ).mockResolvedValue({
120
+ const platformPollJobStatusMock = mock<
121
+ typeof platformClient.platformPollJobStatus
122
+ >(async () => ({
98
123
  jobId: "platform-job-1",
99
124
  type: "export",
100
125
  status: "complete",
101
126
  bundleKey: "platform-bundle-key-abc",
102
- });
127
+ }));
103
128
 
104
- const platformRequestSignedUrlMock = spyOn(
105
- platformClient,
106
- "platformRequestSignedUrl",
107
- ).mockImplementation(async (params) => ({
129
+ const platformRequestSignedUrlMock = mock<
130
+ typeof platformClient.platformRequestSignedUrl
131
+ >(async (params) => ({
108
132
  url:
109
133
  params.operation === "upload"
110
134
  ? "https://storage.googleapis.com/bucket/signed-upload"
@@ -113,10 +137,9 @@ const platformRequestSignedUrlMock = spyOn(
113
137
  expiresAt: new Date(Date.now() + 3600_000).toISOString(),
114
138
  }));
115
139
 
116
- const platformImportBundleFromGcsMock = spyOn(
117
- platformClient,
118
- "platformImportBundleFromGcs",
119
- ).mockResolvedValue({
140
+ const platformImportBundleFromGcsMock = mock<
141
+ typeof platformClient.platformImportBundleFromGcs
142
+ >(async () => ({
120
143
  statusCode: 200,
121
144
  body: {
122
145
  success: true,
@@ -128,12 +151,11 @@ const platformImportBundleFromGcsMock = spyOn(
128
151
  backups_created: 1,
129
152
  },
130
153
  } as Record<string, unknown>,
131
- });
154
+ }));
132
155
 
133
- const platformImportPreflightFromGcsMock = spyOn(
134
- platformClient,
135
- "platformImportPreflightFromGcs",
136
- ).mockResolvedValue({
156
+ const platformImportPreflightFromGcsMock = mock<
157
+ typeof platformClient.platformImportPreflightFromGcs
158
+ >(async () => ({
137
159
  statusCode: 200,
138
160
  body: {
139
161
  can_import: true,
@@ -144,71 +166,84 @@ const platformImportPreflightFromGcsMock = spyOn(
144
166
  total_files: 3,
145
167
  },
146
168
  } as Record<string, unknown>,
147
- });
169
+ }));
148
170
 
149
- const checkExistingPlatformAssistantMock = spyOn(
150
- platformClient,
151
- "checkExistingPlatformAssistant",
152
- ).mockResolvedValue(null);
153
-
154
- const ensureSelfHostedLocalRegistrationMock = spyOn(
155
- platformClient,
156
- "ensureSelfHostedLocalRegistration",
157
- ).mockResolvedValue({
158
- assistant: { id: "platform-assistant-1", name: "my-assistant" },
159
- registration: {
160
- client_installation_id: "device-id-123",
161
- runtime_assistant_id: "target-local",
162
- client_platform: "cli",
163
- },
164
- assistant_api_key: "api-key-123",
165
- webhook_secret: "webhook-secret-123",
166
- } as unknown as Awaited<
167
- ReturnType<typeof platformClient.ensureSelfHostedLocalRegistration>
168
- >);
169
-
170
- const injectCredentialsIntoAssistantMock = spyOn(
171
- platformClient,
172
- "injectCredentialsIntoAssistant",
173
- ).mockResolvedValue(true);
174
-
175
- const fetchCurrentUserMock = spyOn(
176
- platformClient,
177
- "fetchCurrentUser",
178
- ).mockResolvedValue({
179
- id: "user-1",
180
- email: "test@example.com",
181
- display: "Test",
182
- } as unknown as Awaited<ReturnType<typeof platformClient.fetchCurrentUser>>);
183
-
184
- const fetchOrganizationIdMock = spyOn(
185
- platformClient,
186
- "fetchOrganizationId",
187
- ).mockResolvedValue("org-1");
188
-
189
- const localRuntimeExportToGcsMock = spyOn(
190
- localRuntimeClient,
191
- "localRuntimeExportToGcs",
192
- ).mockResolvedValue({ jobId: "local-export-job-1" });
193
-
194
- const localRuntimeImportFromGcsMock = spyOn(
195
- localRuntimeClient,
196
- "localRuntimeImportFromGcs",
197
- ).mockResolvedValue({ jobId: "local-import-job-1" });
171
+ const checkExistingPlatformAssistantMock = mock<
172
+ typeof platformClient.checkExistingPlatformAssistant
173
+ >(async () => null);
174
+
175
+ const ensureSelfHostedLocalRegistrationMock = mock<
176
+ typeof platformClient.ensureSelfHostedLocalRegistration
177
+ >(
178
+ async () =>
179
+ ({
180
+ assistant: { id: "platform-assistant-1", name: "my-assistant" },
181
+ registration: {
182
+ client_installation_id: "device-id-123",
183
+ runtime_assistant_id: "target-local",
184
+ client_platform: "cli",
185
+ },
186
+ assistant_api_key: "api-key-123",
187
+ webhook_secret: "webhook-secret-123",
188
+ }) as unknown as Awaited<
189
+ ReturnType<typeof platformClient.ensureSelfHostedLocalRegistration>
190
+ >,
191
+ );
192
+
193
+ const injectCredentialsIntoAssistantMock = mock<
194
+ typeof platformClient.injectCredentialsIntoAssistant
195
+ >(async () => true);
196
+
197
+ const fetchCurrentUserMock = mock<typeof platformClient.fetchCurrentUser>(
198
+ async () =>
199
+ ({
200
+ id: "user-1",
201
+ email: "test@example.com",
202
+ display: "Test",
203
+ }) as unknown as Awaited<
204
+ ReturnType<typeof platformClient.fetchCurrentUser>
205
+ >,
206
+ );
207
+
208
+ const fetchOrganizationIdMock = mock<typeof platformClient.fetchOrganizationId>(
209
+ async () => "org-1",
210
+ );
211
+
212
+ mock.module("../lib/platform-client.js", () => ({
213
+ ...realPlatformClient,
214
+ readPlatformToken: readPlatformTokenMock,
215
+ getPlatformUrl: getPlatformUrlMock,
216
+ hatchAssistant: hatchAssistantMock,
217
+ platformPollJobStatus: platformPollJobStatusMock,
218
+ platformRequestSignedUrl: platformRequestSignedUrlMock,
219
+ platformImportBundleFromGcs: platformImportBundleFromGcsMock,
220
+ platformImportPreflightFromGcs: platformImportPreflightFromGcsMock,
221
+ checkExistingPlatformAssistant: checkExistingPlatformAssistantMock,
222
+ ensureSelfHostedLocalRegistration: ensureSelfHostedLocalRegistrationMock,
223
+ injectCredentialsIntoAssistant: injectCredentialsIntoAssistantMock,
224
+ fetchCurrentUser: fetchCurrentUserMock,
225
+ fetchOrganizationId: fetchOrganizationIdMock,
226
+ }));
227
+
228
+ const localRuntimeExportToGcsMock = mock<
229
+ typeof localRuntimeClient.localRuntimeExportToGcs
230
+ >(async () => ({ jobId: "local-export-job-1" }));
231
+
232
+ const localRuntimeImportFromGcsMock = mock<
233
+ typeof localRuntimeClient.localRuntimeImportFromGcs
234
+ >(async () => ({ jobId: "local-import-job-1" }));
198
235
 
199
236
  // Default to a fixed version string. Tests that exercise the version-gate
200
237
  // surface override this mock per-case to assert the value flows from the
201
238
  // target runtime's `/v1/identity` (NOT from `cliPkg.version`) into the
202
239
  // download signed-URL request.
203
- const localRuntimeIdentityMock = spyOn(
204
- localRuntimeClient,
205
- "localRuntimeIdentity",
206
- ).mockResolvedValue({ version: "0.6.5" });
207
-
208
- const localRuntimePollJobStatusMock = spyOn(
209
- localRuntimeClient,
210
- "localRuntimePollJobStatus",
211
- ).mockImplementation(async (_runtimeUrl, _token, jobId) => ({
240
+ const localRuntimeIdentityMock = mock<
241
+ typeof localRuntimeClient.localRuntimeIdentity
242
+ >(async () => ({ version: "0.6.5" }));
243
+
244
+ const localRuntimePollJobStatusMock = mock<
245
+ typeof localRuntimeClient.localRuntimePollJobStatus
246
+ >(async (_runtimeUrl, _token, jobId) => ({
212
247
  jobId,
213
248
  type: jobId.includes("import") ? "import" : "export",
214
249
  status: "complete",
@@ -224,6 +259,14 @@ const localRuntimePollJobStatusMock = spyOn(
224
259
  },
225
260
  }));
226
261
 
262
+ mock.module("../lib/local-runtime-client.js", () => ({
263
+ ...realLocalRuntimeClient,
264
+ localRuntimeExportToGcs: localRuntimeExportToGcsMock,
265
+ localRuntimeImportFromGcs: localRuntimeImportFromGcsMock,
266
+ localRuntimeIdentity: localRuntimeIdentityMock,
267
+ localRuntimePollJobStatus: localRuntimePollJobStatusMock,
268
+ }));
269
+
227
270
  const hatchLocalMock = mock(async () => {});
228
271
 
229
272
  mock.module("../lib/hatch-local.js", () => ({
@@ -285,29 +328,13 @@ import type { AssistantEntry } from "../lib/assistant-config.js";
285
328
  // ---------------------------------------------------------------------------
286
329
 
287
330
  afterAll(() => {
288
- findAssistantByNameMock.mockRestore();
289
- saveAssistantEntryMock.mockRestore();
290
- loadAllAssistantsMock.mockRestore();
291
- removeAssistantEntryMock.mockRestore();
292
- loadGuardianTokenMock.mockRestore();
293
- leaseGuardianTokenMock.mockRestore();
294
- readPlatformTokenMock.mockRestore();
295
- getPlatformUrlMock.mockRestore();
296
- hatchAssistantMock.mockRestore();
297
- checkExistingPlatformAssistantMock.mockRestore();
298
- platformPollJobStatusMock.mockRestore();
299
- platformRequestSignedUrlMock.mockRestore();
300
- platformImportBundleFromGcsMock.mockRestore();
301
- platformImportPreflightFromGcsMock.mockRestore();
302
- ensureSelfHostedLocalRegistrationMock.mockRestore();
303
- injectCredentialsIntoAssistantMock.mockRestore();
304
- fetchCurrentUserMock.mockRestore();
305
- fetchOrganizationIdMock.mockRestore();
306
- computeDeviceIdMock.mockRestore();
307
- localRuntimeExportToGcsMock.mockRestore();
308
- localRuntimeImportFromGcsMock.mockRestore();
309
- localRuntimeIdentityMock.mockRestore();
310
- localRuntimePollJobStatusMock.mockRestore();
331
+ // Restore the real module exports so the mocks do not leak into sibling
332
+ // test files (e.g. guardian-token.test.ts, platform-client tests) that
333
+ // run in the same `bun test` invocation.
334
+ mock.module("../lib/assistant-config.js", () => realAssistantConfig);
335
+ mock.module("../lib/guardian-token.js", () => realGuardianToken);
336
+ mock.module("../lib/platform-client.js", () => realPlatformClient);
337
+ mock.module("../lib/local-runtime-client.js", () => realLocalRuntimeClient);
311
338
  rmSync(testDir, { recursive: true, force: true });
312
339
  delete process.env.VELLUM_LOCKFILE_DIR;
313
340
  });
@@ -6,6 +6,7 @@ import {
6
6
  findAssistantByName,
7
7
  getActiveAssistant,
8
8
  resolveAssistant,
9
+ saveAssistantEntry,
9
10
  } from "../lib/assistant-config";
10
11
  import {
11
12
  DAEMON_INTERNAL_ASSISTANT_ID,
@@ -21,6 +22,7 @@ import {
21
22
  } from "../lib/client-identity";
22
23
  import {
23
24
  fetchOrganizationId,
25
+ fetchPlatformAssistants,
24
26
  readPlatformToken,
25
27
  } from "../lib/platform-client";
26
28
  import { tuiLog } from "../lib/tui-log";
@@ -39,6 +41,7 @@ const FALLBACK_RUNTIME_URL = `http://127.0.0.1:${GATEWAY_PORT}`;
39
41
  interface ParsedArgs {
40
42
  runtimeUrl: string;
41
43
  assistantId: string;
44
+ assistantName?: string;
42
45
  species: Species;
43
46
  /** "vellum" for platform-hosted assistants, undefined for local. */
44
47
  cloud?: string;
@@ -52,6 +55,15 @@ interface ParsedArgs {
52
55
  zone?: string;
53
56
  }
54
57
 
58
+ function readAssistantName(
59
+ entry: ReturnType<typeof findAssistantByName>,
60
+ ): string | undefined {
61
+ const rawName = entry?.name ?? entry?.assistantName;
62
+ return typeof rawName === "string" && rawName.trim()
63
+ ? rawName.trim()
64
+ : undefined;
65
+ }
66
+
55
67
  function parseArgs(): ParsedArgs {
56
68
  const args = process.argv.slice(3);
57
69
 
@@ -112,6 +124,7 @@ function parseArgs(): ParsedArgs {
112
124
 
113
125
  let runtimeUrl = entry?.localUrl || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
114
126
  let assistantId = entry?.assistantId || DAEMON_INTERNAL_ASSISTANT_ID;
127
+ let assistantName = readAssistantName(entry);
115
128
  const cloud = entry?.cloud;
116
129
  const species: Species = (entry?.species as Species) ?? "vellum";
117
130
 
@@ -134,6 +147,7 @@ function parseArgs(): ParsedArgs {
134
147
  flagArgs[i + 1]
135
148
  ) {
136
149
  assistantId = flagArgs[++i];
150
+ assistantName = undefined;
137
151
  } else if ((flag === "--interface" || flag === "-i") && flagArgs[i + 1]) {
138
152
  const value = flagArgs[++i];
139
153
  if (!(SUPPORTED_INTERFACES as readonly string[]).includes(value)) {
@@ -149,6 +163,7 @@ function parseArgs(): ParsedArgs {
149
163
  return {
150
164
  runtimeUrl: maybeSwapToLocalhost(runtimeUrl.replace(/\/+$/, "")),
151
165
  assistantId,
166
+ assistantName,
152
167
  species,
153
168
  cloud,
154
169
  platformToken,
@@ -225,6 +240,39 @@ ${ANSI.bold}EXAMPLES:${ANSI.reset}
225
240
  `);
226
241
  }
227
242
 
243
+ async function maybeHydratePlatformAssistantName(
244
+ assistantId: string,
245
+ assistantName: string | undefined,
246
+ cloud: string | undefined,
247
+ platformToken: string | undefined,
248
+ ): Promise<string | undefined> {
249
+ if (cloud !== "vellum" || assistantName || !platformToken) {
250
+ return assistantName;
251
+ }
252
+
253
+ try {
254
+ const matchedAssistant = (
255
+ await fetchPlatformAssistants(platformToken)
256
+ ).find((assistant) => assistant.id === assistantId);
257
+ const hydratedName = matchedAssistant?.name.trim();
258
+ if (!hydratedName) {
259
+ return assistantName;
260
+ }
261
+
262
+ const entry = findAssistantByName(assistantId);
263
+ if (entry && entry.name !== hydratedName) {
264
+ saveAssistantEntry({
265
+ ...entry,
266
+ name: hydratedName,
267
+ });
268
+ }
269
+
270
+ return hydratedName;
271
+ } catch {
272
+ return assistantName;
273
+ }
274
+ }
275
+
228
276
  /**
229
277
  * Walk up from this file's location to find a sibling `clients/web` package.
230
278
  *
@@ -289,6 +337,7 @@ export async function client(): Promise<void> {
289
337
  const {
290
338
  runtimeUrl,
291
339
  assistantId,
340
+ assistantName: parsedAssistantName,
292
341
  species,
293
342
  cloud,
294
343
  platformToken,
@@ -312,6 +361,13 @@ export async function client(): Promise<void> {
312
361
  interfaceId,
313
362
  });
314
363
 
364
+ const assistantName = await maybeHydratePlatformAssistantName(
365
+ assistantId,
366
+ parsedAssistantName,
367
+ cloud,
368
+ platformToken,
369
+ );
370
+
315
371
  // Build pre-constructed request headers merged from auth + client registration.
316
372
  // Spreading into every fetch site ensures consistency across REST and SSE endpoints.
317
373
  let auth: Record<string, string> | undefined;
@@ -348,6 +404,6 @@ export async function client(): Promise<void> {
348
404
  console.log(`${ANSI.dim}Disconnected.${ANSI.reset}`);
349
405
  process.exit(0);
350
406
  },
351
- { auth, project, zone },
407
+ { auth, project, zone, assistantName },
352
408
  );
353
409
  }