@vellumai/assistant 0.10.7-dev.202607100313.fffdfea → 0.10.7-dev.202607100720.fba456e

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.
Files changed (99) hide show
  1. package/openapi.yaml +846 -27
  2. package/package.json +1 -1
  3. package/src/__tests__/config-watcher-skill-reseed.test.ts +1 -0
  4. package/src/__tests__/config-watcher.test.ts +22 -4
  5. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  6. package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -9
  7. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -2
  8. package/src/cli/commands/__tests__/inference-models.test.ts +86 -0
  9. package/src/cli/commands/__tests__/inference-profiles.test.ts +254 -0
  10. package/src/cli/commands/__tests__/inference-providers.test.ts +171 -0
  11. package/src/cli/commands/__tests__/schedules.test.ts +133 -1
  12. package/src/cli/commands/__tests__/usage.test.ts +131 -0
  13. package/src/cli/commands/inference-callsites.ts +158 -0
  14. package/src/cli/commands/inference-models.ts +84 -0
  15. package/src/cli/commands/inference-profiles.ts +371 -0
  16. package/src/cli/commands/inference-providers-default.ts +89 -0
  17. package/src/cli/commands/inference-providers.ts +87 -16
  18. package/src/cli/commands/inference.ts +6 -0
  19. package/src/cli/commands/memory/nodes.ts +90 -50
  20. package/src/cli/commands/schedules.ts +17 -2
  21. package/src/cli/commands/usage.ts +59 -21
  22. package/src/cli/lib/__tests__/cli-output.test.ts +20 -0
  23. package/src/cli/lib/cli-output.ts +55 -0
  24. package/src/config/__tests__/loader-last-known-good.test.ts +229 -0
  25. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +166 -0
  26. package/src/config/default-profile-catalog.ts +28 -0
  27. package/src/config/loader.ts +276 -46
  28. package/src/daemon/config-watcher.ts +65 -20
  29. package/src/daemon/message-types/sync.ts +1 -0
  30. package/src/oauth/byo-connection.test.ts +1 -1
  31. package/src/oauth/connection-resolver.test.ts +140 -0
  32. package/src/oauth/connection-resolver.ts +48 -12
  33. package/src/plugins/defaults/memory/anisotropy.ts +1 -1
  34. package/src/plugins/defaults/memory/config.ts +1 -1
  35. package/src/plugins/defaults/memory/context-search/agent-protocol.ts +1 -1
  36. package/src/plugins/defaults/memory/context-search/search.ts +1 -1
  37. package/src/plugins/defaults/memory/graph/bootstrap.ts +1 -1
  38. package/src/plugins/defaults/memory/graph/consolidation.ts +1 -1
  39. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +11 -1
  40. package/src/plugins/defaults/memory/graph/extraction.ts +1 -1
  41. package/src/plugins/defaults/memory/graph/narrative.ts +1 -1
  42. package/src/plugins/defaults/memory/graph/pattern-scan.ts +1 -1
  43. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -1
  44. package/src/plugins/defaults/memory/graph/tools.ts +1 -1
  45. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +43 -16
  46. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +1 -1
  47. package/src/plugins/defaults/memory/host-utils.ts +80 -0
  48. package/src/plugins/defaults/memory/identity-context.ts +1 -1
  49. package/src/plugins/defaults/memory/injectors.ts +1 -1
  50. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +2 -2
  51. package/src/plugins/defaults/memory/jobs-worker.ts +2 -9
  52. package/src/plugins/defaults/memory/memory-retrospective-state.ts +1 -1
  53. package/src/plugins/defaults/memory/paths.ts +48 -0
  54. package/src/plugins/defaults/memory/pkb/autoinject.ts +1 -1
  55. package/src/plugins/defaults/memory/pkb/context.ts +2 -2
  56. package/src/plugins/defaults/memory/pkb/types.ts +1 -1
  57. package/src/plugins/defaults/memory/pkb-schedule.ts +2 -2
  58. package/src/plugins/defaults/memory/rerank-local.ts +2 -3
  59. package/src/plugins/defaults/memory/routes/memory-eval-routes.ts +1 -1
  60. package/src/plugins/defaults/memory/routes/memory-item-routes.test.ts +149 -1
  61. package/src/plugins/defaults/memory/routes/memory-item-routes.ts +144 -1
  62. package/src/plugins/defaults/memory/routes/memory-v2-routes.ts +1 -1
  63. package/src/plugins/defaults/memory/startup.ts +1 -1
  64. package/src/plugins/defaults/memory/v2/backfill-jobs.ts +1 -1
  65. package/src/plugins/defaults/memory/v2/consolidation-job.ts +2 -2
  66. package/src/plugins/defaults/memory/v2/injection.ts +1 -1
  67. package/src/plugins/defaults/memory/v2/memory-v2-startup.ts +1 -1
  68. package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +1 -1
  69. package/src/plugins/defaults/memory/v2/qdrant.ts +1 -1
  70. package/src/plugins/defaults/memory/v2/reranker.ts +1 -1
  71. package/src/plugins/defaults/memory/v2/static-context.ts +1 -1
  72. package/src/plugins/defaults/memory/v2/sweep-job.ts +1 -1
  73. package/src/plugins/defaults/memory/v3/candidate-match.ts +1 -1
  74. package/src/plugins/defaults/memory/v3/maintain-job.ts +1 -1
  75. package/src/plugins/defaults/memory/v3/page-content.ts +1 -1
  76. package/src/plugins/defaults/memory/v3/pool-select.ts +1 -2
  77. package/src/plugins/defaults/memory/v3/selection-log-store.ts +1 -1
  78. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +2 -5
  79. package/src/plugins/defaults/memory/worker.ts +1 -1
  80. package/src/plugins/defaults/workspace/injectors.ts +17 -15
  81. package/src/plugins/types.ts +9 -14
  82. package/src/providers/inference/connection-availability.ts +193 -0
  83. package/src/runtime/routes/__tests__/inference-callsites-routes.test.ts +54 -0
  84. package/src/runtime/routes/__tests__/inference-models-routes.test.ts +54 -0
  85. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +405 -0
  86. package/src/runtime/routes/__tests__/inference-routes-registration.test.ts +42 -0
  87. package/src/runtime/routes/__tests__/theme-routes.test.ts +66 -0
  88. package/src/runtime/routes/conversation-query-routes.ts +5 -3
  89. package/src/runtime/routes/default-provider-routes.ts +5 -152
  90. package/src/runtime/routes/index.ts +8 -0
  91. package/src/runtime/routes/inference-callsites-routes.ts +211 -0
  92. package/src/runtime/routes/inference-models-routes.ts +104 -0
  93. package/src/runtime/routes/inference-profiles-routes.ts +717 -0
  94. package/src/runtime/routes/theme-routes.ts +36 -0
  95. package/src/runtime/sync/resource-sync-events.ts +4 -0
  96. package/src/theme/workspace-theme.test.ts +269 -0
  97. package/src/theme/workspace-theme.ts +304 -0
  98. package/src/watcher/__tests__/engine-auth-notify.test.ts +316 -0
  99. package/src/watcher/engine.ts +87 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.7-dev.202607100313.fffdfea",
3
+ "version": "0.10.7-dev.202607100720.fba456e",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -136,6 +136,7 @@ mock.module("../runtime/sync/resource-sync-events.js", () => ({
136
136
  publishConfigChanged: () => {},
137
137
  publishSoundsConfigUpdated: () => {},
138
138
  publishAvatarChanged: () => {},
139
+ publishWorkspaceThemeChanged: () => {},
139
140
  }));
140
141
 
141
142
  mock.module("../platform/sync-identity.js", () => ({
@@ -147,6 +147,7 @@ mock.module("../signals/cancel.js", () => ({
147
147
  // or client broadcasts.
148
148
  let evictCallCount = 0;
149
149
  let identityCallCount = 0;
150
+ let themeCallCount = 0;
150
151
 
151
152
  mock.module("../daemon/conversation-store.js", () => ({
152
153
  evictConversationsForReload: () => {
@@ -161,6 +162,9 @@ mock.module("../runtime/sync/resource-sync-events.js", () => ({
161
162
  publishConfigChanged: () => {},
162
163
  publishSoundsConfigUpdated: () => {},
163
164
  publishAvatarChanged: () => {},
165
+ publishWorkspaceThemeChanged: () => {
166
+ themeCallCount++;
167
+ },
164
168
  }));
165
169
 
166
170
  mock.module("../daemon/skill-memory-refresh.js", () => ({
@@ -190,7 +194,12 @@ function findFileWatch(filePath: string): CapturedFileWatch | undefined {
190
194
  return capturedFileWatches.find((w) => w.filePath === filePath);
191
195
  }
192
196
 
193
- const WORKSPACE_FILES = new Set(["config.json", "SOUL.md", "IDENTITY.md"]);
197
+ const WORKSPACE_FILES = new Set([
198
+ "config.json",
199
+ "SOUL.md",
200
+ "IDENTITY.md",
201
+ "ui/theme.json",
202
+ ]);
194
203
 
195
204
  // Each call advances the inode + mtime so the listener's early-return guard
196
205
  // (curr.ino === prev.ino && curr.mtimeMs === prev.mtimeMs) doesn't fire.
@@ -242,6 +251,7 @@ beforeEach(() => {
242
251
  inoMap.clear();
243
252
  evictCallCount = 0;
244
253
  identityCallCount = 0;
254
+ themeCallCount = 0;
245
255
  watcher = new ConfigWatcher(undefined, TEST_DEBOUNCE_MS);
246
256
  });
247
257
 
@@ -273,13 +283,21 @@ describe("ConfigWatcher workspace file handlers", () => {
273
283
 
274
284
  test("unregistered workspace files are not subscribed (only the registered handler set is)", () => {
275
285
  watcher.start();
276
- // Per-file watching only registers config.json, SOUL.md, IDENTITY.md.
277
- // The whole workspace dir must not be watched either — that was the
278
- // ENXIO-on-Unix-sockets bug.
286
+ // Per-file watching only registers config.json, SOUL.md, IDENTITY.md,
287
+ // and ui/theme.json. The whole workspace dir must not be watched
288
+ // either — that was the ENXIO-on-Unix-sockets bug.
279
289
  expect(findFileWatch(join(WORKSPACE_DIR, "OTHER.md"))).toBeUndefined();
280
290
  expect(findWatcher(WORKSPACE_DIR)).toBeUndefined();
281
291
  });
282
292
 
293
+ test("ui/theme.json change publishes the workspace-theme invalidation", async () => {
294
+ watcher.start();
295
+ simulateFileChange(WORKSPACE_DIR, "ui/theme.json");
296
+ await new Promise((r) => setTimeout(r, WAIT_MS));
297
+ expect(themeCallCount).toBe(1);
298
+ expect(evictCallCount).toBe(0);
299
+ });
300
+
283
301
  test("config.json change calls refreshConfigFromSources", async () => {
284
302
  let refreshCalled = false;
285
303
  watcher.refreshConfigFromSources = async () => {
@@ -234,7 +234,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
234
234
  "tools/executor.ts", // CES approval bridge resolves the CES RPC client via getCesClient
235
235
  "tools/network/web-fetch.ts", // Firecrawl /scrape BYOK fetch provider API key lookup (firecrawl provider key)
236
236
  "workspace/default-provider-ensure.ts", // legacy anthropic echo disambiguation (vault key presence check)
237
- "runtime/routes/default-provider-routes.ts", // default-provider availability status (credential presence check only; value never leaves the handler)
237
+ "providers/inference/connection-availability.ts", // shared (provider, connection) availability status (credential presence check only; value never leaves the helper)
238
238
  ]);
239
239
 
240
240
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -88,7 +88,6 @@ const BASELINE: Record<string, readonly string[]> = {
88
88
  "../../../../../persistence/raw-query.js",
89
89
  "../../../../../persistence/schema/index.js",
90
90
  "../../../../../security/untrusted-content.js",
91
- "../../../../../util/platform.js",
92
91
  "../../../../agent/image-optimize.js",
93
92
  "../../../../api/responses/memory-v3-selection-log.js",
94
93
  "../../../../channels/types.js",
@@ -137,14 +136,6 @@ const BASELINE: Record<string, readonly string[]> = {
137
136
  "../../../../skills/catalog-cache.js",
138
137
  "../../../../telemetry/watchdog-events-store.js",
139
138
  "../../../../tools/skills/delete-managed.js",
140
- "../../../../util/abort-reasons.js",
141
- "../../../../util/errors.js",
142
- "../../../../util/log-redact.js",
143
- "../../../../util/platform.js",
144
- "../../../../util/process-liveness.js",
145
- "../../../../util/retry.js",
146
- "../../../../util/strip-comment-lines.js",
147
- "../../../../util/truncate.js",
148
139
  "../../../channels/types.js",
149
140
  "../../../config/assistant-feature-flags.js",
150
141
  "../../../config/loader.js",
@@ -203,11 +194,17 @@ const BASELINE: Record<string, readonly string[]> = {
203
194
  "../../../tools/registry.js",
204
195
  "../../../tools/types.js",
205
196
  "../../../types.js",
197
+ "../../../util/abort-reasons.js",
198
+ "../../../util/errors.js",
199
+ "../../../util/log-redact.js",
206
200
  "../../../util/logger.js",
207
201
  "../../../util/platform.js",
202
+ "../../../util/process-liveness.js",
208
203
  "../../../util/promise-guard.js",
204
+ "../../../util/retry.js",
209
205
  "../../../util/sqlite-retry.js",
210
206
  "../../../util/strip-comment-lines.js",
207
+ "../../../util/truncate.js",
211
208
  "../../../util/worker-memory.js",
212
209
  "../../types.js",
213
210
  "../injection-presence.js",
@@ -81,7 +81,6 @@ const BASELINE: Record<string, readonly string[]> = {
81
81
  "src/cli/commands/memory/memory-v2-compare-render.ts",
82
82
  "src/cli/commands/memory/memory-v2.ts",
83
83
  "src/cli/commands/memory/memory-v3.ts",
84
- "src/cli/commands/memory/nodes.ts",
85
84
  "src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts",
86
85
  "src/config/bundled-skills/playbooks/tools/playbook-create.ts",
87
86
  "src/config/bundled-skills/playbooks/tools/playbook-delete.ts",
@@ -104,7 +103,6 @@ const BASELINE: Record<string, readonly string[]> = {
104
103
  "src/persistence/conversation-crud.ts",
105
104
  "src/persistence/steps.ts",
106
105
  "src/persistence/worker-control.ts",
107
- "src/plugins/types.ts",
108
106
  "src/prompts/system-prompt.ts",
109
107
  "src/runtime/routes/consolidation-routes.ts",
110
108
  "src/runtime/routes/conversation-query-routes.ts",
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Tests for `assistant inference models list` CLI arg parsing / IPC wiring.
3
+ */
4
+
5
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
6
+
7
+ import { Command } from "commander";
8
+
9
+ let lastIpcCall: { method: string; params?: any } | null = null;
10
+ let mockIpcResult: { ok: boolean; result?: unknown; error?: string } = {
11
+ ok: true,
12
+ result: { models: [] },
13
+ };
14
+
15
+ mock.module("../../../ipc/cli-client.js", () => ({
16
+ cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
17
+ lastIpcCall = { method, params };
18
+ return mockIpcResult;
19
+ },
20
+ }));
21
+
22
+ mock.module("../../../util/logger.js", () => ({
23
+ getLogger: () =>
24
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
25
+ getCliLogger: () =>
26
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
27
+ }));
28
+
29
+ const { attachModelsSubcommand } = await import("../inference-models.js");
30
+
31
+ beforeEach(() => {
32
+ lastIpcCall = null;
33
+ mockIpcResult = { ok: true, result: { models: [] } };
34
+ process.exitCode = 0;
35
+ });
36
+
37
+ async function run(args: string[]): Promise<string> {
38
+ const originalWrite = process.stdout.write.bind(process.stdout);
39
+ const chunks: string[] = [];
40
+ process.stdout.write = ((chunk: unknown) => {
41
+ chunks.push(typeof chunk === "string" ? chunk : String(chunk));
42
+ return true;
43
+ }) as typeof process.stdout.write;
44
+ try {
45
+ const program = new Command();
46
+ program.exitOverride();
47
+ const inference = program.command("inference");
48
+ attachModelsSubcommand(inference);
49
+ await program.parseAsync(["node", "assistant", "inference", ...args]);
50
+ } catch {
51
+ // swallow commander exits
52
+ } finally {
53
+ process.stdout.write = originalWrite;
54
+ }
55
+ return chunks.join("");
56
+ }
57
+
58
+ describe("models list", () => {
59
+ test("no filter → empty queryParams", async () => {
60
+ await run(["models", "list"]);
61
+ expect(lastIpcCall?.method).toBe("inference_models_list");
62
+ expect(lastIpcCall?.params).toEqual({ queryParams: {} });
63
+ });
64
+
65
+ test("--provider forwards the filter", async () => {
66
+ await run(["models", "list", "--provider", "anthropic"]);
67
+ expect(lastIpcCall?.params).toEqual({
68
+ queryParams: { provider: "anthropic" },
69
+ });
70
+ });
71
+
72
+ test("--json emits structured output", async () => {
73
+ mockIpcResult = {
74
+ ok: true,
75
+ result: {
76
+ models: [
77
+ { provider: "anthropic", id: "claude-opus-4-8", displayName: "X" },
78
+ ],
79
+ },
80
+ };
81
+ const out = await run(["models", "list", "--json"]);
82
+ const parsed = JSON.parse(out.trim());
83
+ expect(parsed.ok).toBe(true);
84
+ expect(parsed.models[0].id).toBe("claude-opus-4-8");
85
+ });
86
+ });
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Tests for `assistant inference profiles` CLI arg parsing / IPC wiring.
3
+ *
4
+ * Validates that flags map onto the correct IPC method + body, that create
5
+ * requires --provider/--model, that --thinking parses to a boolean, and that
6
+ * `active` reads via config_get and writes via config_patch.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ import { Command } from "commander";
12
+
13
+ let lastIpcCall: { method: string; params?: any } | null = null;
14
+ let mockIpcResult: { ok: boolean; result?: unknown; error?: string } = {
15
+ ok: true,
16
+ result: {},
17
+ };
18
+
19
+ mock.module("../../../ipc/cli-client.js", () => ({
20
+ cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
21
+ lastIpcCall = { method, params };
22
+ return mockIpcResult;
23
+ },
24
+ }));
25
+
26
+ mock.module("../../../util/logger.js", () => ({
27
+ getLogger: () =>
28
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
29
+ getCliLogger: () =>
30
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
31
+ }));
32
+
33
+ const { attachProfilesSubcommand } = await import("../inference-profiles.js");
34
+
35
+ beforeEach(() => {
36
+ lastIpcCall = null;
37
+ mockIpcResult = { ok: true, result: {} };
38
+ process.exitCode = 0;
39
+ });
40
+
41
+ afterEach(() => {
42
+ process.exitCode = 0;
43
+ });
44
+
45
+ async function run(
46
+ args: string[],
47
+ ): Promise<{ stdout: string; exitCode: number }> {
48
+ const originalWrite = process.stdout.write.bind(process.stdout);
49
+ const chunks: string[] = [];
50
+ process.stdout.write = ((chunk: unknown) => {
51
+ chunks.push(typeof chunk === "string" ? chunk : String(chunk));
52
+ return true;
53
+ }) as typeof process.stdout.write;
54
+
55
+ const prevExit = process.exitCode;
56
+ process.exitCode = 0;
57
+ try {
58
+ const program = new Command();
59
+ program.exitOverride();
60
+ const inference = program.command("inference");
61
+ attachProfilesSubcommand(inference);
62
+ await program.parseAsync(["node", "assistant", "inference", ...args]);
63
+ } catch (err: unknown) {
64
+ if (!(err instanceof Error && err.message.startsWith("(outputHelp)"))) {
65
+ // swallow commander exits
66
+ }
67
+ } finally {
68
+ process.stdout.write = originalWrite;
69
+ }
70
+ const stdout = chunks.join("");
71
+ const exitCode = (process.exitCode as number) ?? 0;
72
+ process.exitCode = prevExit;
73
+ return { stdout, exitCode };
74
+ }
75
+
76
+ describe("profiles list", () => {
77
+ test("calls inference_profiles_list", async () => {
78
+ mockIpcResult = { ok: true, result: { profiles: [] } };
79
+ await run(["profiles", "list"]);
80
+ expect(lastIpcCall?.method).toBe("inference_profiles_list");
81
+ });
82
+ });
83
+
84
+ describe("profiles create", () => {
85
+ test("requires --provider", async () => {
86
+ const { exitCode } = await run(["profiles", "create", "p", "--model", "m"]);
87
+ expect(exitCode).toBe(1);
88
+ expect(lastIpcCall).toBeNull();
89
+ });
90
+
91
+ test("requires --model", async () => {
92
+ const { exitCode } = await run([
93
+ "profiles",
94
+ "create",
95
+ "p",
96
+ "--provider",
97
+ "anthropic",
98
+ ]);
99
+ expect(exitCode).toBe(1);
100
+ expect(lastIpcCall).toBeNull();
101
+ });
102
+
103
+ test("maps flags onto the create body with thinking→boolean and numeric parse", async () => {
104
+ mockIpcResult = {
105
+ ok: true,
106
+ result: { ok: true, name: "p", entry: {}, warnings: [] },
107
+ };
108
+ await run([
109
+ "profiles",
110
+ "create",
111
+ "p",
112
+ "--provider",
113
+ "anthropic",
114
+ "--model",
115
+ "claude-opus-4-8",
116
+ "--connection",
117
+ "anthropic-personal",
118
+ "--effort",
119
+ "high",
120
+ "--max-tokens",
121
+ "8000",
122
+ "--temperature",
123
+ "0.5",
124
+ "--thinking",
125
+ "on",
126
+ "--allow-unlisted",
127
+ ]);
128
+ expect(lastIpcCall?.method).toBe("inference_profiles_create");
129
+ expect(lastIpcCall?.params?.body).toEqual({
130
+ name: "p",
131
+ provider: "anthropic",
132
+ model: "claude-opus-4-8",
133
+ connection: "anthropic-personal",
134
+ effort: "high",
135
+ maxTokens: 8000,
136
+ temperature: 0.5,
137
+ thinking: true,
138
+ allowUnlisted: true,
139
+ });
140
+ });
141
+
142
+ test("prints the verification hint on success", async () => {
143
+ mockIpcResult = {
144
+ ok: true,
145
+ result: { ok: true, name: "p", entry: {}, warnings: [] },
146
+ };
147
+ const { stdout } = await run([
148
+ "profiles",
149
+ "create",
150
+ "p",
151
+ "--provider",
152
+ "anthropic",
153
+ "--model",
154
+ "claude-opus-4-8",
155
+ ]);
156
+ expect(stdout).toContain("Verify it works:");
157
+ expect(stdout).toContain("assistant inference send --profile p");
158
+ });
159
+
160
+ test("rejects a non-numeric --max-tokens before calling the daemon", async () => {
161
+ const { exitCode } = await run([
162
+ "profiles",
163
+ "create",
164
+ "p",
165
+ "--provider",
166
+ "anthropic",
167
+ "--model",
168
+ "m",
169
+ "--max-tokens",
170
+ "lots",
171
+ ]);
172
+ expect(exitCode).toBe(1);
173
+ expect(lastIpcCall).toBeNull();
174
+ });
175
+
176
+ test("rejects an invalid --thinking value", async () => {
177
+ const { exitCode } = await run([
178
+ "profiles",
179
+ "create",
180
+ "p",
181
+ "--provider",
182
+ "anthropic",
183
+ "--model",
184
+ "m",
185
+ "--thinking",
186
+ "maybe",
187
+ ]);
188
+ expect(exitCode).toBe(1);
189
+ expect(lastIpcCall).toBeNull();
190
+ });
191
+ });
192
+
193
+ describe("profiles update", () => {
194
+ test("sends pathParams + partial body", async () => {
195
+ mockIpcResult = {
196
+ ok: true,
197
+ result: { ok: true, name: "p", entry: {}, warnings: [] },
198
+ };
199
+ await run(["profiles", "update", "p", "--effort", "low"]);
200
+ expect(lastIpcCall?.method).toBe("inference_profiles_update");
201
+ expect(lastIpcCall?.params).toEqual({
202
+ pathParams: { name: "p" },
203
+ body: { effort: "low" },
204
+ });
205
+ });
206
+
207
+ test("refuses an empty update", async () => {
208
+ const { exitCode } = await run(["profiles", "update", "p"]);
209
+ expect(exitCode).toBe(1);
210
+ expect(lastIpcCall).toBeNull();
211
+ });
212
+ });
213
+
214
+ describe("profiles delete", () => {
215
+ test("sends the name as a path param", async () => {
216
+ mockIpcResult = { ok: true, result: { ok: true, name: "p" } };
217
+ await run(["profiles", "delete", "p"]);
218
+ expect(lastIpcCall?.method).toBe("inference_profiles_delete");
219
+ expect(lastIpcCall?.params).toEqual({ pathParams: { name: "p" } });
220
+ });
221
+ });
222
+
223
+ describe("profiles active", () => {
224
+ test("no arg reads via config_get", async () => {
225
+ mockIpcResult = {
226
+ ok: true,
227
+ result: { llm: { activeProfile: "balanced" } },
228
+ };
229
+ const { stdout } = await run(["profiles", "active"]);
230
+ expect(lastIpcCall?.method).toBe("config_get");
231
+ expect(stdout).toContain("balanced");
232
+ });
233
+
234
+ test("with a name writes via the validated set-active route", async () => {
235
+ mockIpcResult = {
236
+ ok: true,
237
+ result: { ok: true, activeProfile: "my-fast" },
238
+ };
239
+ const { stdout } = await run(["profiles", "active", "my-fast"]);
240
+ expect(lastIpcCall?.method).toBe("inference_profiles_set_active");
241
+ expect(lastIpcCall?.params).toEqual({ body: { name: "my-fast" } });
242
+ expect(stdout).toContain("active profile set to my-fast");
243
+ });
244
+
245
+ test("surfaces the daemon's rejection for an unknown name", async () => {
246
+ mockIpcResult = {
247
+ ok: false,
248
+ error: 'Profile "balancd" does not exist. Valid profiles: balanced, ...',
249
+ };
250
+ const { exitCode } = await run(["profiles", "active", "balancd"]);
251
+ expect(lastIpcCall?.method).toBe("inference_profiles_set_active");
252
+ expect(exitCode).toBe(1);
253
+ });
254
+ });
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Tests for `assistant inference providers connections` CLI arg parsing / IPC
3
+ * wiring — focused on the openai-compatible `--base-url` / `--model` flags that
4
+ * forward to the connection route's `base_url` + `models` fields.
5
+ */
6
+
7
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
8
+
9
+ import { Command } from "commander";
10
+
11
+ let lastIpcCall: { method: string; params?: any } | null = null;
12
+ let mockIpcResult: { ok: boolean; result?: unknown; error?: string } = {
13
+ ok: true,
14
+ result: {},
15
+ };
16
+
17
+ mock.module("../../../ipc/cli-client.js", () => ({
18
+ cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
19
+ lastIpcCall = { method, params };
20
+ return mockIpcResult;
21
+ },
22
+ }));
23
+
24
+ mock.module("../../../util/logger.js", () => ({
25
+ getLogger: () =>
26
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
27
+ getCliLogger: () =>
28
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
29
+ }));
30
+
31
+ const { attachProvidersSubcommand } = await import("../inference-providers.js");
32
+
33
+ const CONNECTION_RESULT = {
34
+ name: "local-llm",
35
+ provider: "openai-compatible",
36
+ auth: { type: "none" },
37
+ createdAt: Date.now(),
38
+ updatedAt: Date.now(),
39
+ };
40
+
41
+ beforeEach(() => {
42
+ lastIpcCall = null;
43
+ mockIpcResult = { ok: true, result: CONNECTION_RESULT };
44
+ process.exitCode = 0;
45
+ });
46
+
47
+ afterEach(() => {
48
+ process.exitCode = 0;
49
+ });
50
+
51
+ async function run(
52
+ args: string[],
53
+ ): Promise<{ stdout: string; exitCode: number }> {
54
+ const originalWrite = process.stdout.write.bind(process.stdout);
55
+ const chunks: string[] = [];
56
+ process.stdout.write = ((chunk: unknown) => {
57
+ chunks.push(typeof chunk === "string" ? chunk : String(chunk));
58
+ return true;
59
+ }) as typeof process.stdout.write;
60
+
61
+ const prevExit = process.exitCode;
62
+ process.exitCode = 0;
63
+ try {
64
+ const program = new Command();
65
+ program.exitOverride();
66
+ const inference = program.command("inference");
67
+ attachProvidersSubcommand(inference);
68
+ await program.parseAsync(["node", "assistant", "inference", ...args]);
69
+ } catch {
70
+ // swallow commander exits
71
+ } finally {
72
+ process.stdout.write = originalWrite;
73
+ }
74
+ const stdout = chunks.join("");
75
+ const exitCode = (process.exitCode as number) ?? 0;
76
+ process.exitCode = prevExit;
77
+ return { stdout, exitCode };
78
+ }
79
+
80
+ describe("providers connections create — openai-compatible", () => {
81
+ test("forwards base_url and collected --model list to the route", async () => {
82
+ await run([
83
+ "providers",
84
+ "connections",
85
+ "create",
86
+ "local-llm",
87
+ "--provider",
88
+ "openai-compatible",
89
+ "--auth",
90
+ "none",
91
+ "--base-url",
92
+ "http://localhost:1234/v1",
93
+ "--model",
94
+ "model-a",
95
+ "--model",
96
+ "model-b",
97
+ ]);
98
+ expect(lastIpcCall?.method).toBe("inference_provider_connections_create");
99
+ expect(lastIpcCall?.params?.body).toEqual({
100
+ name: "local-llm",
101
+ provider: "openai-compatible",
102
+ auth: { type: "none" },
103
+ base_url: "http://localhost:1234/v1",
104
+ models: [{ id: "model-a" }, { id: "model-b" }],
105
+ });
106
+ });
107
+
108
+ test("rejects openai-compatible without --base-url before calling the daemon", async () => {
109
+ const { exitCode } = await run([
110
+ "providers",
111
+ "connections",
112
+ "create",
113
+ "local-llm",
114
+ "--provider",
115
+ "openai-compatible",
116
+ "--auth",
117
+ "none",
118
+ "--model",
119
+ "model-a",
120
+ ]);
121
+ expect(exitCode).toBe(1);
122
+ expect(lastIpcCall).toBeNull();
123
+ });
124
+
125
+ test("omits base_url/models for a standard provider", async () => {
126
+ await run([
127
+ "providers",
128
+ "connections",
129
+ "create",
130
+ "anthropic-personal",
131
+ "--provider",
132
+ "anthropic",
133
+ "--auth",
134
+ "api_key",
135
+ "--credential",
136
+ "credential/anthropic/api_key",
137
+ ]);
138
+ expect(lastIpcCall?.params?.body).toEqual({
139
+ name: "anthropic-personal",
140
+ provider: "anthropic",
141
+ auth: { type: "api_key", credential: "credential/anthropic/api_key" },
142
+ });
143
+ });
144
+ });
145
+
146
+ describe("providers connections update — openai-compatible", () => {
147
+ test("forwards base_url and models alongside auth", async () => {
148
+ mockIpcResult = { ok: true, result: CONNECTION_RESULT };
149
+ await run([
150
+ "providers",
151
+ "connections",
152
+ "update",
153
+ "local-llm",
154
+ "--auth",
155
+ "none",
156
+ "--base-url",
157
+ "http://localhost:5678/v1",
158
+ "--model",
159
+ "model-c",
160
+ ]);
161
+ expect(lastIpcCall?.method).toBe("inference_provider_connections_update");
162
+ expect(lastIpcCall?.params).toEqual({
163
+ pathParams: { name: "local-llm" },
164
+ body: {
165
+ auth: { type: "none" },
166
+ base_url: "http://localhost:5678/v1",
167
+ models: [{ id: "model-c" }],
168
+ },
169
+ });
170
+ });
171
+ });