@vellumai/assistant 0.7.3 → 0.8.0
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/ARCHITECTURE.md +29 -28
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/openapi.yaml +22 -4
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -26
- package/src/__tests__/context-search-pkb-source.test.ts +12 -6
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +3 -3
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -6
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
- package/src/__tests__/filing-service.test.ts +2 -19
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/injector-chain.test.ts +24 -16
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/relay-server.test.ts +46 -2
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-decision-primitive.ts +0 -13
- package/src/approvals/guardian-request-resolvers.ts +4 -32
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/memory-v2.ts +7 -7
- package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
- package/src/cli/commands/oauth/connect.ts +10 -52
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/loader.ts +72 -19
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
- package/src/daemon/conversation-agent-loop.ts +13 -10
- package/src/daemon/conversation-lifecycle.ts +22 -8
- package/src/daemon/conversation-surfaces.ts +16 -14
- package/src/daemon/conversation-tool-setup.ts +9 -5
- package/src/daemon/conversation.ts +1 -1
- package/src/daemon/handlers/shared.ts +26 -0
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +88 -73
- package/src/daemon/memory-v2-startup.ts +55 -14
- package/src/daemon/message-types/messages.ts +19 -1
- package/src/documents/document-store.ts +35 -1
- package/src/filing/filing-service.ts +2 -3
- package/src/heartbeat/heartbeat-service.ts +1 -1
- package/src/ipc/assistant-server.ts +93 -36
- package/src/ipc/skill-server.ts +99 -42
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
- package/src/memory/context-search/sources/memory-v2.ts +1 -17
- package/src/memory/context-search/sources/memory.ts +2 -2
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +32 -9
- package/src/memory/graph/graph-search.test.ts +6 -5
- package/src/memory/graph/graph-search.ts +3 -4
- package/src/memory/graph/retriever.test.ts +12 -7
- package/src/memory/graph/retriever.ts +4 -5
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +1 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-worker.ts +8 -4
- package/src/memory/pkb/pkb-search.test.ts +6 -5
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -0
- package/src/memory/search/semantic.ts +4 -5
- package/src/memory/v2/__tests__/activation.test.ts +35 -5
- package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
- package/src/memory/v2/__tests__/injection.test.ts +140 -23
- package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
- package/src/memory/v2/__tests__/sim.test.ts +118 -7
- package/src/memory/v2/__tests__/static-context.test.ts +1 -13
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/consolidation-job.ts +7 -8
- package/src/memory/v2/injection.ts +32 -12
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +5 -0
- package/src/memory/v2/qdrant.ts +209 -48
- package/src/memory/v2/sim.ts +67 -26
- package/src/memory/v2/static-context.ts +4 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +7 -0
- package/src/notifications/copy-composer.ts +46 -12
- package/src/notifications/decision-engine.ts +46 -0
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +1 -2
- package/src/proactive-artifact/job.test.ts +51 -4
- package/src/proactive-artifact/job.ts +16 -2
- package/src/proactive-artifact/message-copy.ts +18 -1
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/guardian-reply-router.ts +0 -10
- package/src/runtime/pending-interactions.ts +19 -15
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/debug-bash-routes.ts +2 -0
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
- package/src/runtime/routes/memory-item-routes.test.ts +3 -9
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +103 -17
- package/src/skills/include-graph.ts +35 -13
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/memory/register.test.ts +7 -5
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +19 -1
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- package/src/workspace/migrations/registry.ts +6 -0
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The CLI half mocks `cliIpcCall` and asserts the subcommand dispatches
|
|
6
6
|
* to `memory_v2_reembed_skills` with an empty body. The route half uses
|
|
7
|
-
* the real `loadConfig`
|
|
8
|
-
* `
|
|
9
|
-
* per-test `config.json` fixture in the temp workspace. We mock only
|
|
7
|
+
* the real `loadConfig` — `memory.v2.enabled` is toggled via a per-test
|
|
8
|
+
* `config.json` fixture in the temp workspace. We mock only
|
|
10
9
|
* `seedV2SkillEntries` so we can assert it was invoked without actually
|
|
11
10
|
* embedding skills.
|
|
12
11
|
*/
|
|
@@ -17,20 +16,14 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
17
16
|
|
|
18
17
|
import { Command } from "commander";
|
|
19
18
|
|
|
20
|
-
import {
|
|
21
|
-
_setOverridesForTesting,
|
|
22
|
-
clearFeatureFlagOverridesCache,
|
|
23
|
-
} from "../config/assistant-feature-flags.js";
|
|
24
19
|
import { invalidateConfigCache } from "../config/loader.js";
|
|
25
20
|
import { getWorkspaceDir } from "../util/platform.js";
|
|
26
21
|
|
|
27
22
|
// ---------------------------------------------------------------------------
|
|
28
|
-
// Module-level mocks — kept minimal. `loadConfig
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// for config) that exercise the same code paths the route handler runs in
|
|
33
|
-
// production.
|
|
23
|
+
// Module-level mocks — kept minimal. `loadConfig` and `getLogger` use their
|
|
24
|
+
// real implementations because we have first-class test hooks (a per-test
|
|
25
|
+
// workspace `config.json` for config) that exercise the same code paths
|
|
26
|
+
// the route handler runs in production.
|
|
34
27
|
// ---------------------------------------------------------------------------
|
|
35
28
|
|
|
36
29
|
let lastIpcCall: { method: string; params?: Record<string, unknown> } | null =
|
|
@@ -60,7 +53,7 @@ mock.module("../memory/v2/skill-store.js", () => ({
|
|
|
60
53
|
|
|
61
54
|
const { registerMemoryV2Command } =
|
|
62
55
|
await import("../cli/commands/memory-v2.js");
|
|
63
|
-
const { ROUTES: memoryV2Routes } =
|
|
56
|
+
const { ROUTES: memoryV2Routes, MEMORY_V2_DISABLED_CODE } =
|
|
64
57
|
await import("../runtime/routes/memory-v2-routes.js");
|
|
65
58
|
const { RouteError } = await import("../runtime/routes/errors.js");
|
|
66
59
|
|
|
@@ -128,17 +121,15 @@ beforeEach(() => {
|
|
|
128
121
|
seedCallCount = 0;
|
|
129
122
|
process.exitCode = 0;
|
|
130
123
|
|
|
131
|
-
//
|
|
132
|
-
_setOverridesForTesting({ "memory-v2-enabled": true });
|
|
124
|
+
// Default: v2 enabled so happy-path tests pass.
|
|
133
125
|
writeWorkspaceConfig({ memory: { v2: { enabled: true } } });
|
|
134
126
|
});
|
|
135
127
|
|
|
136
128
|
afterEach(() => {
|
|
137
|
-
// Roll back the workspace config
|
|
138
|
-
//
|
|
129
|
+
// Roll back the workspace config between cases so a gate-off test does
|
|
130
|
+
// not leak into the next case's setup.
|
|
139
131
|
rmSync(join(getWorkspaceDir(), "config.json"), { force: true });
|
|
140
132
|
invalidateConfigCache();
|
|
141
|
-
clearFeatureFlagOverridesCache();
|
|
142
133
|
});
|
|
143
134
|
|
|
144
135
|
// ---------------------------------------------------------------------------
|
|
@@ -188,15 +179,6 @@ describe("memory_v2_reembed_skills route", () => {
|
|
|
188
179
|
expect(seedCallCount).toBe(0);
|
|
189
180
|
});
|
|
190
181
|
|
|
191
|
-
test("throws RouteError when feature flag is off", async () => {
|
|
192
|
-
_setOverridesForTesting({ "memory-v2-enabled": false });
|
|
193
|
-
|
|
194
|
-
await expect(
|
|
195
|
-
reembedSkillsRoute!.handler({ body: {} }),
|
|
196
|
-
).rejects.toBeInstanceOf(RouteError);
|
|
197
|
-
expect(seedCallCount).toBe(0);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
182
|
test("throws RouteError when config.memory.v2.enabled is off", async () => {
|
|
201
183
|
writeWorkspaceConfig({ memory: { v2: { enabled: false } } });
|
|
202
184
|
|
|
@@ -206,3 +188,51 @@ describe("memory_v2_reembed_skills route", () => {
|
|
|
206
188
|
expect(seedCallCount).toBe(0);
|
|
207
189
|
});
|
|
208
190
|
});
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// All v2 routes share the same gate
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
describe("all memory v2 routes — MEMORY_V2_DISABLED gate", () => {
|
|
197
|
+
// Minimal bodies that satisfy each route's schema. The gate runs before
|
|
198
|
+
// schema validation so any body would surface the gate error, but using
|
|
199
|
+
// valid shapes keeps the assertion precise: we're confirming the gate
|
|
200
|
+
// (not zod) is what blocks the call.
|
|
201
|
+
const MINIMAL_BODIES: Record<string, Record<string, unknown>> = {
|
|
202
|
+
memory_v2_backfill: { op: "migrate" },
|
|
203
|
+
memory_v2_validate: {},
|
|
204
|
+
memory_v2_get_concept_page: { slug: "any" },
|
|
205
|
+
memory_v2_list_concept_pages: {},
|
|
206
|
+
memory_v2_rebuild_corpus_stats: {},
|
|
207
|
+
memory_v2_explain_similarity: { userText: "hello" },
|
|
208
|
+
memory_v2_concept_frequency: {},
|
|
209
|
+
memory_v2_fit_anisotropy: {},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const GATE_OFF_CASES = [
|
|
213
|
+
{
|
|
214
|
+
label: "config is off",
|
|
215
|
+
apply: () => writeWorkspaceConfig({ memory: { v2: { enabled: false } } }),
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
for (const [operationId, body] of Object.entries(MINIMAL_BODIES)) {
|
|
220
|
+
for (const { label, apply } of GATE_OFF_CASES) {
|
|
221
|
+
test(`${operationId} throws MEMORY_V2_DISABLED when ${label}`, async () => {
|
|
222
|
+
apply();
|
|
223
|
+
const route = memoryV2Routes.find((r) => r.operationId === operationId);
|
|
224
|
+
expect(route).toBeDefined();
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await route!.handler({ body });
|
|
228
|
+
throw new Error("expected handler to throw");
|
|
229
|
+
} catch (err) {
|
|
230
|
+
expect(err).toBeInstanceOf(RouteError);
|
|
231
|
+
expect((err as InstanceType<typeof RouteError>).code).toBe(
|
|
232
|
+
MEMORY_V2_DISABLED_CODE,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
@@ -58,6 +58,7 @@ afterAll(() => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
import { invalidateConfigCache, loadConfig } from "../config/loader.js";
|
|
61
|
+
import { applyContextDefaultsToRawConfig } from "../runtime/routes/conversation-query-routes.js";
|
|
61
62
|
import { _setStorePath } from "../security/encrypted-store.js";
|
|
62
63
|
|
|
63
64
|
// ---------------------------------------------------------------------------
|
|
@@ -184,7 +185,7 @@ describe("platform-managed config defaults", () => {
|
|
|
184
185
|
) + "\n",
|
|
185
186
|
);
|
|
186
187
|
|
|
187
|
-
loadConfig();
|
|
188
|
+
const config = loadConfig();
|
|
188
189
|
|
|
189
190
|
const written = readConfig() as { services?: Record<string, unknown> };
|
|
190
191
|
expect(written.services).toBeDefined();
|
|
@@ -192,5 +193,287 @@ describe("platform-managed config defaults", () => {
|
|
|
192
193
|
expect(
|
|
193
194
|
(written.services!["inference"] as { mode?: string })?.mode,
|
|
194
195
|
).toBe("your-own");
|
|
196
|
+
// ...and the in-memory config must mirror the explicit user choice (the
|
|
197
|
+
// fill-defaults pass must not override an explicit "your-own").
|
|
198
|
+
expect(
|
|
199
|
+
(config.services.inference as { mode: string }).mode,
|
|
200
|
+
).toBe("your-own");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("IS_PLATFORM=true, config file exists without a services key → in-memory config has all managed modes", () => {
|
|
204
|
+
// Regression guard for the platform-managed boot order: by the time
|
|
205
|
+
// `loadConfig()` runs, lifecycle steps such as `seedInferenceProfiles`
|
|
206
|
+
// have already written `config.json` (with `llm.profiles` etc.), so
|
|
207
|
+
// `configFileExisted` is true even on a brand-new platform-managed
|
|
208
|
+
// assistant. Deployment-context defaults must still be applied to the
|
|
209
|
+
// in-memory config for any leaf keys that are absent from disk.
|
|
210
|
+
process.env.IS_PLATFORM = "true";
|
|
211
|
+
|
|
212
|
+
writeFileSync(
|
|
213
|
+
CONFIG_PATH,
|
|
214
|
+
JSON.stringify(
|
|
215
|
+
{
|
|
216
|
+
llm: {
|
|
217
|
+
profiles: {
|
|
218
|
+
balanced: { provider: "anthropic", model: "claude-sonnet-4.5" },
|
|
219
|
+
},
|
|
220
|
+
activeProfile: "balanced",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
null,
|
|
224
|
+
2,
|
|
225
|
+
) + "\n",
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const config = loadConfig();
|
|
229
|
+
|
|
230
|
+
// In-memory config has the deployment-context defaults applied for the
|
|
231
|
+
// missing service-mode fields.
|
|
232
|
+
for (const svc of MANAGED_SERVICES) {
|
|
233
|
+
expect(
|
|
234
|
+
(
|
|
235
|
+
config.services as unknown as Record<
|
|
236
|
+
string,
|
|
237
|
+
{ mode: string }
|
|
238
|
+
>
|
|
239
|
+
)[svc]!.mode,
|
|
240
|
+
).toBe("managed");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// The on-disk file is NOT modified by the fill pass — disk reflects only
|
|
244
|
+
// what was already there. Existing-file branch never re-writes config.json.
|
|
245
|
+
const onDisk = readConfig() as Record<string, unknown>;
|
|
246
|
+
expect(onDisk["services"]).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("IS_PLATFORM=true, config file exists with a partial service subtree → preserves user fields, fills missing mode", () => {
|
|
250
|
+
process.env.IS_PLATFORM = "true";
|
|
251
|
+
|
|
252
|
+
// User has an image-generation provider configured but never explicitly
|
|
253
|
+
// chose a mode for that service. The fill pass must apply
|
|
254
|
+
// `mode: "managed"` without clobbering the user-supplied provider.
|
|
255
|
+
// (The inference schema dropped per-service model/provider in
|
|
256
|
+
// migration 039 — image-generation still carries them, so it's the
|
|
257
|
+
// right schema to exercise the partial-subtree case.)
|
|
258
|
+
writeFileSync(
|
|
259
|
+
CONFIG_PATH,
|
|
260
|
+
JSON.stringify(
|
|
261
|
+
{
|
|
262
|
+
services: {
|
|
263
|
+
"image-generation": { provider: "openai" },
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
null,
|
|
267
|
+
2,
|
|
268
|
+
) + "\n",
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const config = loadConfig();
|
|
272
|
+
|
|
273
|
+
const imageGen = (
|
|
274
|
+
config.services as unknown as Record<
|
|
275
|
+
string,
|
|
276
|
+
{ mode: string; provider?: string }
|
|
277
|
+
>
|
|
278
|
+
)["image-generation"]!;
|
|
279
|
+
expect(imageGen.mode).toBe("managed");
|
|
280
|
+
expect(imageGen.provider).toBe("openai");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("IS_PLATFORM=false, config file exists without services key → in-memory config keeps schema your-own defaults", () => {
|
|
284
|
+
// Sanity guard: deployment-context defaults are a no-op when IS_PLATFORM
|
|
285
|
+
// is not enabled, regardless of whether config.json existed.
|
|
286
|
+
process.env.IS_PLATFORM = "false";
|
|
287
|
+
|
|
288
|
+
writeFileSync(
|
|
289
|
+
CONFIG_PATH,
|
|
290
|
+
JSON.stringify(
|
|
291
|
+
{
|
|
292
|
+
llm: {
|
|
293
|
+
profiles: {
|
|
294
|
+
balanced: { provider: "anthropic", model: "claude-sonnet-4.5" },
|
|
295
|
+
},
|
|
296
|
+
activeProfile: "balanced",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
null,
|
|
300
|
+
2,
|
|
301
|
+
) + "\n",
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const config = loadConfig();
|
|
305
|
+
|
|
306
|
+
for (const svc of MANAGED_SERVICES) {
|
|
307
|
+
expect(
|
|
308
|
+
(
|
|
309
|
+
config.services as unknown as Record<
|
|
310
|
+
string,
|
|
311
|
+
{ mode: string }
|
|
312
|
+
>
|
|
313
|
+
)[svc]!.mode,
|
|
314
|
+
).toBe("your-own");
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Regression guard for the `handleGetConfig` route handler in
|
|
321
|
+
* `assistant/src/runtime/routes/conversation-query-routes.ts`. That handler
|
|
322
|
+
* returns the raw on-disk JSON to clients (macOS, web, CLI) via
|
|
323
|
+
* `GET /v1/config`, but first layers deployment-context defaults on top
|
|
324
|
+
* via the `applyContextDefaultsToRawConfig` helper.
|
|
325
|
+
*
|
|
326
|
+
* macOS's `loadServiceModes(config:)` only updates `inferenceMode` when
|
|
327
|
+
* `services.inference.mode` is present in the response — without the fill
|
|
328
|
+
* pass, freshly-hatched platform-managed assistants would have no `services`
|
|
329
|
+
* key on disk (only `llm.profiles` from `seedInferenceProfiles`) and macOS
|
|
330
|
+
* would fall back to its `@Published` default of "your-own". The helper is
|
|
331
|
+
* also responsible for guarding against `loadRawConfig()` returning a
|
|
332
|
+
* non-object payload from a malformed-but-parseable `config.json`.
|
|
333
|
+
*/
|
|
334
|
+
describe("GET /v1/config handler — context-default fill on raw response", () => {
|
|
335
|
+
const originalIsPlatform = process.env.IS_PLATFORM;
|
|
336
|
+
|
|
337
|
+
afterEach(() => {
|
|
338
|
+
if (originalIsPlatform === undefined) {
|
|
339
|
+
delete process.env.IS_PLATFORM;
|
|
340
|
+
} else {
|
|
341
|
+
process.env.IS_PLATFORM = originalIsPlatform;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("IS_PLATFORM=true, raw config has no services key → response includes managed defaults", () => {
|
|
346
|
+
process.env.IS_PLATFORM = "true";
|
|
347
|
+
|
|
348
|
+
// Mirrors the real-world fresh-hatch state: lifecycle wrote
|
|
349
|
+
// `llm.profiles` to disk, but never persisted any service modes.
|
|
350
|
+
const raw: Record<string, unknown> = {
|
|
351
|
+
llm: {
|
|
352
|
+
profiles: {
|
|
353
|
+
balanced: { provider: "anthropic", model: "claude-sonnet-4.5" },
|
|
354
|
+
},
|
|
355
|
+
activeProfile: "balanced",
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const result = applyContextDefaultsToRawConfig(raw) as Record<
|
|
360
|
+
string,
|
|
361
|
+
unknown
|
|
362
|
+
>;
|
|
363
|
+
const services = result["services"] as Record<string, { mode: string }>;
|
|
364
|
+
expect(services).toBeDefined();
|
|
365
|
+
for (const svc of MANAGED_SERVICES) {
|
|
366
|
+
expect(services[svc]!.mode).toBe("managed");
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("IS_PLATFORM=true, raw config has explicit services.inference.mode='your-own' → preserved", () => {
|
|
371
|
+
process.env.IS_PLATFORM = "true";
|
|
372
|
+
|
|
373
|
+
// User has explicitly chosen "your-own" via the macOS Save flow.
|
|
374
|
+
// The patch handler persisted that to disk; the fill pass must not
|
|
375
|
+
// override an explicit user choice.
|
|
376
|
+
const raw: Record<string, unknown> = {
|
|
377
|
+
services: {
|
|
378
|
+
inference: { mode: "your-own" },
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const result = applyContextDefaultsToRawConfig(raw) as Record<
|
|
383
|
+
string,
|
|
384
|
+
unknown
|
|
385
|
+
>;
|
|
386
|
+
const services = result["services"] as Record<string, { mode: string }>;
|
|
387
|
+
expect(services["inference"]!.mode).toBe("your-own");
|
|
388
|
+
// Other services were missing entirely → context defaults fill them in.
|
|
389
|
+
expect(services["image-generation"]!.mode).toBe("managed");
|
|
390
|
+
expect(services["web-search"]!.mode).toBe("managed");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("IS_PLATFORM=false, raw config has no services key → response is unchanged", () => {
|
|
394
|
+
process.env.IS_PLATFORM = "false";
|
|
395
|
+
|
|
396
|
+
const raw: Record<string, unknown> = {
|
|
397
|
+
llm: {
|
|
398
|
+
profiles: {
|
|
399
|
+
balanced: { provider: "anthropic", model: "claude-sonnet-4.5" },
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const result = applyContextDefaultsToRawConfig(raw) as Record<
|
|
405
|
+
string,
|
|
406
|
+
unknown
|
|
407
|
+
>;
|
|
408
|
+
expect(result["services"]).toBeUndefined();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("IS_PLATFORM=true, raw config has partial services.inference subtree → preserves user fields, fills missing mode", () => {
|
|
412
|
+
process.env.IS_PLATFORM = "true";
|
|
413
|
+
|
|
414
|
+
// User set image-generation.provider but never chose a mode for any
|
|
415
|
+
// service. The fill pass adds the missing modes without clobbering
|
|
416
|
+
// the user-supplied provider.
|
|
417
|
+
const raw: Record<string, unknown> = {
|
|
418
|
+
services: {
|
|
419
|
+
"image-generation": { provider: "openai" },
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const result = applyContextDefaultsToRawConfig(raw) as Record<
|
|
424
|
+
string,
|
|
425
|
+
unknown
|
|
426
|
+
>;
|
|
427
|
+
const services = result["services"] as Record<
|
|
428
|
+
string,
|
|
429
|
+
{ mode: string; provider?: string }
|
|
430
|
+
>;
|
|
431
|
+
expect(services["image-generation"]!.mode).toBe("managed");
|
|
432
|
+
expect(services["image-generation"]!.provider).toBe("openai");
|
|
433
|
+
// Inference, which was missing entirely, picks up the context default.
|
|
434
|
+
expect(services["inference"]!.mode).toBe("managed");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// -------------------------------------------------------------------------
|
|
438
|
+
// Malformed-but-parseable config.json — must not 500 the GET endpoint.
|
|
439
|
+
//
|
|
440
|
+
// `loadRawConfig()` is typed `Record<string, unknown>` but `JSON.parse`
|
|
441
|
+
// will happily return `null`, primitives, or arrays for a syntactically
|
|
442
|
+
// valid file like `null` / `42` / `[]`. The helper must return those
|
|
443
|
+
// payloads unchanged rather than throwing inside
|
|
444
|
+
// `fillContextDefaultsForMissingKeys`.
|
|
445
|
+
// -------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
test("IS_PLATFORM=true, raw config is null → returned unchanged (no throw)", () => {
|
|
448
|
+
process.env.IS_PLATFORM = "true";
|
|
449
|
+
expect(applyContextDefaultsToRawConfig(null)).toBe(null);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("IS_PLATFORM=true, raw config is a primitive number → returned unchanged (no throw)", () => {
|
|
453
|
+
process.env.IS_PLATFORM = "true";
|
|
454
|
+
expect(applyContextDefaultsToRawConfig(42)).toBe(42);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("IS_PLATFORM=true, raw config is an array → returned unchanged (no throw)", () => {
|
|
458
|
+
process.env.IS_PLATFORM = "true";
|
|
459
|
+
const raw: unknown[] = [{ foo: "bar" }];
|
|
460
|
+
const result = applyContextDefaultsToRawConfig(raw);
|
|
461
|
+
expect(result).toBe(raw);
|
|
462
|
+
// No `services` key was synthesized onto the array.
|
|
463
|
+
expect((result as { services?: unknown }).services).toBeUndefined();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("IS_PLATFORM=true, raw config is a string → returned unchanged (no throw)", () => {
|
|
467
|
+
process.env.IS_PLATFORM = "true";
|
|
468
|
+
expect(applyContextDefaultsToRawConfig("not-an-object")).toBe(
|
|
469
|
+
"not-an-object",
|
|
470
|
+
);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("IS_PLATFORM=false, raw config is null → returned unchanged (no throw)", () => {
|
|
474
|
+
// Sanity check: when there are no context defaults to apply, the helper
|
|
475
|
+
// also short-circuits cleanly on non-object payloads.
|
|
476
|
+
process.env.IS_PLATFORM = "false";
|
|
477
|
+
expect(applyContextDefaultsToRawConfig(null)).toBe(null);
|
|
195
478
|
});
|
|
196
479
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
|
|
4
3
|
import type { AssistantConfig } from "../config/schema.js";
|
|
5
4
|
import type {
|
|
6
5
|
RecallEvidence,
|
|
@@ -107,11 +106,7 @@ const v2Calls: Array<{
|
|
|
107
106
|
}> = [];
|
|
108
107
|
let v2EvidenceReturn: RecallEvidence[] = [];
|
|
109
108
|
|
|
110
|
-
const realMemoryV2 =
|
|
111
|
-
await import("../memory/context-search/sources/memory-v2.js");
|
|
112
|
-
|
|
113
109
|
mock.module(memoryV2SourceModule, () => ({
|
|
114
|
-
isMemoryV2ReadActive: realMemoryV2.isMemoryV2ReadActive,
|
|
115
110
|
searchMemoryV2Source: async (
|
|
116
111
|
query: string,
|
|
117
112
|
context: RecallSearchContext,
|
|
@@ -138,7 +133,6 @@ describe("searchMemorySource", () => {
|
|
|
138
133
|
getNodesByIdsCalls.length = 0;
|
|
139
134
|
v2Calls.length = 0;
|
|
140
135
|
v2EvidenceReturn = [];
|
|
141
|
-
_setOverridesForTesting({ "memory-v2-enabled": false });
|
|
142
136
|
});
|
|
143
137
|
|
|
144
138
|
test("hydrates graph hits into memory recall evidence", async () => {
|
|
@@ -298,8 +292,7 @@ describe("searchMemorySource", () => {
|
|
|
298
292
|
);
|
|
299
293
|
});
|
|
300
294
|
|
|
301
|
-
test("routes to v2 source when
|
|
302
|
-
_setOverridesForTesting({ "memory-v2-enabled": true });
|
|
295
|
+
test("routes to v2 source when memory.v2.enabled is on", async () => {
|
|
303
296
|
v2EvidenceReturn = [
|
|
304
297
|
{
|
|
305
298
|
id: "memory:v2:alice",
|
|
@@ -334,8 +327,7 @@ describe("searchMemorySource", () => {
|
|
|
334
327
|
]);
|
|
335
328
|
});
|
|
336
329
|
|
|
337
|
-
test("stays on legacy path when
|
|
338
|
-
_setOverridesForTesting({ "memory-v2-enabled": true });
|
|
330
|
+
test("stays on legacy path when memory.v2.enabled is off", async () => {
|
|
339
331
|
searchHits = [{ nodeId: "node-a", score: 0.7, text: "" }];
|
|
340
332
|
hydratedNodes = [makeNode({ id: "node-a", content: "Legacy hit" })];
|
|
341
333
|
|
|
@@ -348,21 +340,6 @@ describe("searchMemorySource", () => {
|
|
|
348
340
|
expect(v2Calls).toHaveLength(0);
|
|
349
341
|
expect(searchCalls).toHaveLength(1);
|
|
350
342
|
});
|
|
351
|
-
|
|
352
|
-
test("stays on legacy path when feature flag is off", async () => {
|
|
353
|
-
_setOverridesForTesting({ "memory-v2-enabled": false });
|
|
354
|
-
searchHits = [{ nodeId: "node-a", score: 0.7, text: "" }];
|
|
355
|
-
hydratedNodes = [makeNode({ id: "node-a", content: "Legacy hit" })];
|
|
356
|
-
|
|
357
|
-
await searchMemorySource(
|
|
358
|
-
"alice",
|
|
359
|
-
makeContext({ config: makeV2EnabledConfig() }),
|
|
360
|
-
5,
|
|
361
|
-
);
|
|
362
|
-
|
|
363
|
-
expect(v2Calls).toHaveLength(0);
|
|
364
|
-
expect(searchCalls).toHaveLength(1);
|
|
365
|
-
});
|
|
366
343
|
});
|
|
367
344
|
|
|
368
345
|
function makeV2EnabledConfig(): AssistantConfig {
|
|
@@ -387,7 +364,7 @@ function makeContext(
|
|
|
387
364
|
return {
|
|
388
365
|
workingDir: "/tmp/example-workspace",
|
|
389
366
|
conversationId: "conv-123",
|
|
390
|
-
config: {} as AssistantConfig,
|
|
367
|
+
config: { memory: { v2: { enabled: false } } } as AssistantConfig,
|
|
391
368
|
...overrides,
|
|
392
369
|
};
|
|
393
370
|
}
|
|
@@ -9,7 +9,6 @@ import { tmpdir } from "node:os";
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
11
|
|
|
12
|
-
import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
|
|
13
12
|
import type { AssistantConfig } from "../config/schema.js";
|
|
14
13
|
import type { RecallSearchContext } from "../memory/context-search/types.js";
|
|
15
14
|
import { PKB_WORKSPACE_SCOPE } from "../memory/pkb/types.js";
|
|
@@ -19,6 +18,16 @@ mock.module("../util/logger.js", () => ({
|
|
|
19
18
|
getLogger: () => makeMockLogger(),
|
|
20
19
|
}));
|
|
21
20
|
|
|
21
|
+
// Override `getConfig` so `searchPkbFiles`'s v2 short-circuit (which checks
|
|
22
|
+
// `getConfig().memory.v2.enabled`) stays inactive — these tests exercise
|
|
23
|
+
// the v1 path. Spread the real loader so other exports (loadConfig,
|
|
24
|
+
// applyNestedDefaults, etc.) keep working.
|
|
25
|
+
const realPkbLoader = await import("../config/loader.js");
|
|
26
|
+
mock.module("../config/loader.js", () => ({
|
|
27
|
+
...realPkbLoader,
|
|
28
|
+
getConfig: () => ({ memory: { v2: { enabled: false } } }),
|
|
29
|
+
}));
|
|
30
|
+
|
|
22
31
|
const embedCalls: Array<{
|
|
23
32
|
config: AssistantConfig;
|
|
24
33
|
texts: unknown[];
|
|
@@ -174,7 +183,7 @@ function makeContext(
|
|
|
174
183
|
return {
|
|
175
184
|
workingDir: "/workspace",
|
|
176
185
|
conversationId: "conv-xyz",
|
|
177
|
-
config: {} as AssistantConfig,
|
|
186
|
+
config: { memory: { v2: { enabled: false } } } as AssistantConfig,
|
|
178
187
|
...overrides,
|
|
179
188
|
};
|
|
180
189
|
}
|
|
@@ -193,7 +202,6 @@ describe("PKB context-search source", () => {
|
|
|
193
202
|
denseThrows = null;
|
|
194
203
|
pkbContext = null;
|
|
195
204
|
nowScratchpad = null;
|
|
196
|
-
_setOverridesForTesting({ "memory-v2-enabled": false });
|
|
197
205
|
});
|
|
198
206
|
|
|
199
207
|
test("converts PKB hits to recall evidence with snippets and scores", async () => {
|
|
@@ -443,8 +451,7 @@ describe("PKB context-search source", () => {
|
|
|
443
451
|
]);
|
|
444
452
|
});
|
|
445
453
|
|
|
446
|
-
test("short-circuits to empty when
|
|
447
|
-
_setOverridesForTesting({ "memory-v2-enabled": true });
|
|
454
|
+
test("short-circuits to empty when memory.v2.enabled is on", async () => {
|
|
448
455
|
denseResults = [
|
|
449
456
|
{
|
|
450
457
|
id: "dense-a",
|
|
@@ -471,7 +478,6 @@ describe("PKB context-search source", () => {
|
|
|
471
478
|
});
|
|
472
479
|
|
|
473
480
|
test("readPkbContextEvidence short-circuits when v2 read is active", () => {
|
|
474
|
-
_setOverridesForTesting({ "memory-v2-enabled": true });
|
|
475
481
|
pkbContext = "should not surface under v2";
|
|
476
482
|
nowScratchpad = "should not surface under v2";
|
|
477
483
|
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import type { AgentEvent } from "../agent/loop.js";
|
|
4
|
-
import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
|
|
5
4
|
import type {
|
|
6
5
|
ContentBlock,
|
|
7
6
|
Message,
|
|
8
7
|
ProviderResponse,
|
|
9
8
|
} from "../providers/types.js";
|
|
10
9
|
|
|
11
|
-
// This test exercises v1 conversation routing. The `memory-v2-enabled` flag
|
|
12
|
-
// (registry default `true`) flips memory routing to v2 — disable it here so
|
|
13
|
-
// the v1 paths under test stay active.
|
|
14
|
-
_setOverridesForTesting({ "memory-v2-enabled": false });
|
|
15
|
-
|
|
16
10
|
mock.module("../util/logger.js", () => ({
|
|
17
11
|
getLogger: () =>
|
|
18
12
|
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
@@ -61,6 +55,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
61
55
|
pricingOverrides: [],
|
|
62
56
|
},
|
|
63
57
|
rateLimit: { maxRequestsPerMinute: 0 },
|
|
58
|
+
memory: { v2: { enabled: false } },
|
|
64
59
|
daemon: {
|
|
65
60
|
startupSocketWaitMs: 5000,
|
|
66
61
|
stopTimeoutMs: 5000,
|
|
@@ -626,7 +626,7 @@ function makeCtx(
|
|
|
626
626
|
}),
|
|
627
627
|
|
|
628
628
|
graphMemory: {
|
|
629
|
-
onCompacted: () => {},
|
|
629
|
+
onCompacted: async () => {},
|
|
630
630
|
prepareMemory: async () => ({
|
|
631
631
|
runMessages: [],
|
|
632
632
|
injectedTokens: 0,
|
|
@@ -3652,11 +3652,11 @@ describe("session-agent-loop", () => {
|
|
|
3652
3652
|
expect(rendered).not.toContain("original root");
|
|
3653
3653
|
});
|
|
3654
3654
|
|
|
3655
|
-
test("applyCompactionResult records Slack timestamp watermark when provided", () => {
|
|
3655
|
+
test("applyCompactionResult records Slack timestamp watermark when provided", async () => {
|
|
3656
3656
|
const ctx = makeCtx();
|
|
3657
3657
|
const events: ServerMessage[] = [];
|
|
3658
3658
|
|
|
3659
|
-
applyCompactionResult(
|
|
3659
|
+
await applyCompactionResult(
|
|
3660
3660
|
ctx,
|
|
3661
3661
|
{
|
|
3662
3662
|
messages: [
|
|
@@ -280,21 +280,13 @@ function seedPendingConfirmation(
|
|
|
280
280
|
conversation: Conversation,
|
|
281
281
|
requestId: string,
|
|
282
282
|
): void {
|
|
283
|
+
// Access private ownedIds so denyAllPending/dispose can find this request.
|
|
284
|
+
// promptResolve/promptReject callbacks are stored in pendingInteractions via
|
|
285
|
+
// registerPendingInteraction, which is called separately in each test.
|
|
283
286
|
const prompter = conversation["prompter"] as unknown as {
|
|
284
|
-
|
|
285
|
-
string,
|
|
286
|
-
{
|
|
287
|
-
resolve: (...args: unknown[]) => void;
|
|
288
|
-
reject: (...args: unknown[]) => void;
|
|
289
|
-
timer: ReturnType<typeof setTimeout>;
|
|
290
|
-
}
|
|
291
|
-
>;
|
|
287
|
+
ownedIds: Set<string>;
|
|
292
288
|
};
|
|
293
|
-
prompter.
|
|
294
|
-
resolve: () => {},
|
|
295
|
-
reject: () => {},
|
|
296
|
-
timer: setTimeout(() => {}, 60_000),
|
|
297
|
-
});
|
|
289
|
+
prompter.ownedIds.add(requestId);
|
|
298
290
|
}
|
|
299
291
|
|
|
300
292
|
// ---------------------------------------------------------------------------
|
|
@@ -465,7 +465,7 @@ describe("End-to-end session creation benchmark", () => {
|
|
|
465
465
|
timings.push(performance.now() - start);
|
|
466
466
|
|
|
467
467
|
if (i === 0) {
|
|
468
|
-
expect(session.eventBus.
|
|
468
|
+
expect(session.eventBus.anyListenerCount()).toBeGreaterThan(0);
|
|
469
469
|
}
|
|
470
470
|
session.dispose();
|
|
471
471
|
}
|