@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.
- package/openapi.yaml +846 -27
- package/package.json +1 -1
- package/src/__tests__/config-watcher-skill-reseed.test.ts +1 -0
- package/src/__tests__/config-watcher.test.ts +22 -4
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -9
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -2
- package/src/cli/commands/__tests__/inference-models.test.ts +86 -0
- package/src/cli/commands/__tests__/inference-profiles.test.ts +254 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +171 -0
- package/src/cli/commands/__tests__/schedules.test.ts +133 -1
- package/src/cli/commands/__tests__/usage.test.ts +131 -0
- package/src/cli/commands/inference-callsites.ts +158 -0
- package/src/cli/commands/inference-models.ts +84 -0
- package/src/cli/commands/inference-profiles.ts +371 -0
- package/src/cli/commands/inference-providers-default.ts +89 -0
- package/src/cli/commands/inference-providers.ts +87 -16
- package/src/cli/commands/inference.ts +6 -0
- package/src/cli/commands/memory/nodes.ts +90 -50
- package/src/cli/commands/schedules.ts +17 -2
- package/src/cli/commands/usage.ts +59 -21
- package/src/cli/lib/__tests__/cli-output.test.ts +20 -0
- package/src/cli/lib/cli-output.ts +55 -0
- package/src/config/__tests__/loader-last-known-good.test.ts +229 -0
- package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +166 -0
- package/src/config/default-profile-catalog.ts +28 -0
- package/src/config/loader.ts +276 -46
- package/src/daemon/config-watcher.ts +65 -20
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/oauth/byo-connection.test.ts +1 -1
- package/src/oauth/connection-resolver.test.ts +140 -0
- package/src/oauth/connection-resolver.ts +48 -12
- package/src/plugins/defaults/memory/anisotropy.ts +1 -1
- package/src/plugins/defaults/memory/config.ts +1 -1
- package/src/plugins/defaults/memory/context-search/agent-protocol.ts +1 -1
- package/src/plugins/defaults/memory/context-search/search.ts +1 -1
- package/src/plugins/defaults/memory/graph/bootstrap.ts +1 -1
- package/src/plugins/defaults/memory/graph/consolidation.ts +1 -1
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +11 -1
- package/src/plugins/defaults/memory/graph/extraction.ts +1 -1
- package/src/plugins/defaults/memory/graph/narrative.ts +1 -1
- package/src/plugins/defaults/memory/graph/pattern-scan.ts +1 -1
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -1
- package/src/plugins/defaults/memory/graph/tools.ts +1 -1
- package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +43 -16
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +1 -1
- package/src/plugins/defaults/memory/host-utils.ts +80 -0
- package/src/plugins/defaults/memory/identity-context.ts +1 -1
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +2 -2
- package/src/plugins/defaults/memory/jobs-worker.ts +2 -9
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +1 -1
- package/src/plugins/defaults/memory/paths.ts +48 -0
- package/src/plugins/defaults/memory/pkb/autoinject.ts +1 -1
- package/src/plugins/defaults/memory/pkb/context.ts +2 -2
- package/src/plugins/defaults/memory/pkb/types.ts +1 -1
- package/src/plugins/defaults/memory/pkb-schedule.ts +2 -2
- package/src/plugins/defaults/memory/rerank-local.ts +2 -3
- package/src/plugins/defaults/memory/routes/memory-eval-routes.ts +1 -1
- package/src/plugins/defaults/memory/routes/memory-item-routes.test.ts +149 -1
- package/src/plugins/defaults/memory/routes/memory-item-routes.ts +144 -1
- package/src/plugins/defaults/memory/routes/memory-v2-routes.ts +1 -1
- package/src/plugins/defaults/memory/startup.ts +1 -1
- package/src/plugins/defaults/memory/v2/backfill-jobs.ts +1 -1
- package/src/plugins/defaults/memory/v2/consolidation-job.ts +2 -2
- package/src/plugins/defaults/memory/v2/injection.ts +1 -1
- package/src/plugins/defaults/memory/v2/memory-v2-startup.ts +1 -1
- package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +1 -1
- package/src/plugins/defaults/memory/v2/qdrant.ts +1 -1
- package/src/plugins/defaults/memory/v2/reranker.ts +1 -1
- package/src/plugins/defaults/memory/v2/static-context.ts +1 -1
- package/src/plugins/defaults/memory/v2/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/v3/candidate-match.ts +1 -1
- package/src/plugins/defaults/memory/v3/maintain-job.ts +1 -1
- package/src/plugins/defaults/memory/v3/page-content.ts +1 -1
- package/src/plugins/defaults/memory/v3/pool-select.ts +1 -2
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +1 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +2 -5
- package/src/plugins/defaults/memory/worker.ts +1 -1
- package/src/plugins/defaults/workspace/injectors.ts +17 -15
- package/src/plugins/types.ts +9 -14
- package/src/providers/inference/connection-availability.ts +193 -0
- package/src/runtime/routes/__tests__/inference-callsites-routes.test.ts +54 -0
- package/src/runtime/routes/__tests__/inference-models-routes.test.ts +54 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +405 -0
- package/src/runtime/routes/__tests__/inference-routes-registration.test.ts +42 -0
- package/src/runtime/routes/__tests__/theme-routes.test.ts +66 -0
- package/src/runtime/routes/conversation-query-routes.ts +5 -3
- package/src/runtime/routes/default-provider-routes.ts +5 -152
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/inference-callsites-routes.ts +211 -0
- package/src/runtime/routes/inference-models-routes.ts +104 -0
- package/src/runtime/routes/inference-profiles-routes.ts +717 -0
- package/src/runtime/routes/theme-routes.ts +36 -0
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/theme/workspace-theme.test.ts +269 -0
- package/src/theme/workspace-theme.ts +304 -0
- package/src/watcher/__tests__/engine-auth-notify.test.ts +316 -0
- package/src/watcher/engine.ts +87 -2
package/package.json
CHANGED
|
@@ -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([
|
|
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
|
|
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
|
-
"
|
|
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
|
+
});
|