@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
|
@@ -24,22 +24,30 @@
|
|
|
24
24
|
* content.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { beforeEach, describe, expect, test } from "bun:test";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
28
|
+
|
|
29
|
+
// This test exercises v1 PKB injection. `config.memory.v2.enabled`
|
|
30
|
+
// (default `true`) makes the PKB injector go silent — force it off here
|
|
31
|
+
// so the v1 injection chain assertions stay meaningful.
|
|
32
|
+
const realLoader = await import("../config/loader.js");
|
|
33
|
+
const realGetConfig = realLoader.getConfig;
|
|
34
|
+
mock.module("../config/loader.js", () => ({
|
|
35
|
+
...realLoader,
|
|
36
|
+
getConfig: () => {
|
|
37
|
+
const real = realGetConfig();
|
|
38
|
+
return {
|
|
39
|
+
...real,
|
|
40
|
+
memory: { ...real.memory, v2: { ...real.memory.v2, enabled: false } },
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const { applyRuntimeInjections, composeInjectorChain } = await import(
|
|
46
|
+
"../daemon/conversation-runtime-assembly.js"
|
|
47
|
+
);
|
|
48
|
+
const { DEFAULT_INJECTOR_ORDER, defaultInjectorsPlugin } = await import(
|
|
49
|
+
"../plugins/defaults/injectors.js"
|
|
50
|
+
);
|
|
43
51
|
import {
|
|
44
52
|
getInjectors,
|
|
45
53
|
registerPlugin,
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* v2 read-side cutover behavior for the PKB-derived default injectors.
|
|
3
3
|
*
|
|
4
|
-
* When `
|
|
4
|
+
* When `getConfig().memory.v2.enabled` is true:
|
|
5
5
|
* - `pkb-context` silences itself (concept pages own retrieval).
|
|
6
6
|
* - `pkb-reminder` still fires (its body is generic recall/remember
|
|
7
7
|
* guidance) but skips the PKB-search hints — those name PKB paths.
|
|
8
8
|
* - `now-md` fires unchanged (workspace state, independent of PKB).
|
|
9
9
|
*
|
|
10
|
-
* Mocks `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Mocks `getConfig` at the module level so each test can flip the effective
|
|
11
|
+
* gate state without standing up a full config stack. Mocks the PKB hybrid
|
|
12
|
+
* search so the reminder-with-hints branch can resolve deterministically
|
|
13
|
+
* when called.
|
|
14
14
|
*/
|
|
15
15
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
16
16
|
|
|
17
17
|
let v2Active = false;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const realLoader = await import("../config/loader.js");
|
|
20
|
+
|
|
21
|
+
mock.module("../config/loader.js", () => ({
|
|
22
|
+
...realLoader,
|
|
23
|
+
getConfig: () => ({ memory: { v2: { enabled: v2Active } } }),
|
|
21
24
|
}));
|
|
22
25
|
|
|
23
26
|
mock.module("../memory/pkb/pkb-search.js", () => ({
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the memory-v2 skill seed gate
|
|
3
|
-
*
|
|
2
|
+
* Tests for the memory-v2 skill seed gate and the v2 concept-page schema
|
|
3
|
+
* rebuild gate, both invoked from the daemon startup path
|
|
4
|
+
* (`assistant/src/daemon/memory-v2-startup.ts`).
|
|
4
5
|
*
|
|
5
|
-
* The
|
|
6
|
-
* import graph. Coverage matrix
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* The gates are exercised in isolation rather than mounting the full
|
|
7
|
+
* lifecycle import graph. Coverage matrix:
|
|
8
|
+
* - Skill seed (`maybeSeedMemoryV2Skills`): config gating, rejection
|
|
9
|
+
* swallowing.
|
|
10
|
+
* - Schema rebuild (`maybeRebuildMemoryV2Concepts`): config gating,
|
|
11
|
+
* drift-triggered reembed enqueue, empty-after-create reembed enqueue,
|
|
12
|
+
* no enqueue when collection is healthy, error swallowing.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
* never block startup or surface an exception.
|
|
14
|
+
* Both gates must never block startup or surface an exception.
|
|
15
15
|
*/
|
|
16
16
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
17
17
|
|
|
@@ -22,31 +22,36 @@ import type { AssistantConfig } from "../config/schema.js";
|
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
|
|
24
24
|
interface TestState {
|
|
25
|
-
flagOverrides: Record<string, boolean>;
|
|
26
25
|
seedCallCount: number;
|
|
27
26
|
seedShouldReject: Error | null;
|
|
28
27
|
warnCalls: Array<{ obj: unknown; msg: unknown }>;
|
|
28
|
+
infoCalls: Array<{ obj: unknown; msg: unknown }>;
|
|
29
|
+
// Rebuild-gate mocks (drive maybeRebuildMemoryV2Concepts).
|
|
30
|
+
ensureCollectionCallCount: number;
|
|
31
|
+
ensureCollectionResult: { migrated: boolean };
|
|
32
|
+
ensureCollectionThrows: Error | null;
|
|
33
|
+
countResult: number;
|
|
34
|
+
listPagesResult: string[];
|
|
35
|
+
enqueueCalls: Array<{ type: string; payload: Record<string, unknown> }>;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const state: TestState = {
|
|
32
|
-
flagOverrides: {},
|
|
33
39
|
seedCallCount: 0,
|
|
34
40
|
seedShouldReject: null,
|
|
35
41
|
warnCalls: [],
|
|
42
|
+
infoCalls: [],
|
|
43
|
+
ensureCollectionCallCount: 0,
|
|
44
|
+
ensureCollectionResult: { migrated: false },
|
|
45
|
+
ensureCollectionThrows: null,
|
|
46
|
+
countResult: 0,
|
|
47
|
+
listPagesResult: [],
|
|
48
|
+
enqueueCalls: [],
|
|
36
49
|
};
|
|
37
50
|
|
|
38
51
|
// ---------------------------------------------------------------------------
|
|
39
52
|
// Mocks — installed before the module under test is loaded.
|
|
40
53
|
// ---------------------------------------------------------------------------
|
|
41
54
|
|
|
42
|
-
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
43
|
-
isAssistantFeatureFlagEnabled: (key: string, _config: unknown): boolean => {
|
|
44
|
-
const explicit = state.flagOverrides[key];
|
|
45
|
-
if (typeof explicit === "boolean") return explicit;
|
|
46
|
-
return true; // undeclared flags default to enabled
|
|
47
|
-
},
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
55
|
mock.module("../memory/v2/skill-store.js", () => ({
|
|
51
56
|
seedV2SkillEntries: async (): Promise<void> => {
|
|
52
57
|
state.seedCallCount += 1;
|
|
@@ -54,18 +59,51 @@ mock.module("../memory/v2/skill-store.js", () => ({
|
|
|
54
59
|
},
|
|
55
60
|
}));
|
|
56
61
|
|
|
62
|
+
mock.module("../memory/v2/qdrant.js", () => ({
|
|
63
|
+
ensureConceptPageCollection: async (): Promise<{ migrated: boolean }> => {
|
|
64
|
+
state.ensureCollectionCallCount += 1;
|
|
65
|
+
if (state.ensureCollectionThrows) throw state.ensureCollectionThrows;
|
|
66
|
+
return state.ensureCollectionResult;
|
|
67
|
+
},
|
|
68
|
+
countConceptPagePoints: async (): Promise<number> => state.countResult,
|
|
69
|
+
// The rebuild gate does not call this, but the seed gate's fire-and-forget
|
|
70
|
+
// chain imports it; provide a no-op so the dynamic import resolves.
|
|
71
|
+
dropLegacySkillsCollection: async (): Promise<void> => {},
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
mock.module("../memory/v2/page-store.js", () => ({
|
|
75
|
+
hasConceptPages: async (): Promise<boolean> =>
|
|
76
|
+
state.listPagesResult.length > 0,
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
mock.module("../memory/jobs-store.js", () => ({
|
|
80
|
+
enqueueMemoryJob: (
|
|
81
|
+
type: string,
|
|
82
|
+
payload: Record<string, unknown>,
|
|
83
|
+
): string => {
|
|
84
|
+
state.enqueueCalls.push({ type, payload });
|
|
85
|
+
return "test-job-id";
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
mock.module("../util/platform.js", () => ({
|
|
90
|
+
getWorkspaceDir: () => "/tmp/test-workspace",
|
|
91
|
+
}));
|
|
92
|
+
|
|
57
93
|
mock.module("../util/logger.js", () => ({
|
|
58
94
|
getLogger: () => ({
|
|
59
95
|
warn: (obj: unknown, msg: unknown) => {
|
|
60
96
|
state.warnCalls.push({ obj, msg });
|
|
61
97
|
},
|
|
62
|
-
info: () => {
|
|
98
|
+
info: (obj: unknown, msg: unknown) => {
|
|
99
|
+
state.infoCalls.push({ obj, msg });
|
|
100
|
+
},
|
|
63
101
|
error: () => {},
|
|
64
102
|
debug: () => {},
|
|
65
103
|
}),
|
|
66
104
|
}));
|
|
67
105
|
|
|
68
|
-
const { maybeSeedMemoryV2Skills } =
|
|
106
|
+
const { maybeSeedMemoryV2Skills, maybeRebuildMemoryV2Concepts } =
|
|
69
107
|
await import("../daemon/memory-v2-startup.js");
|
|
70
108
|
|
|
71
109
|
// ---------------------------------------------------------------------------
|
|
@@ -99,65 +137,37 @@ async function flushMicrotasks(): Promise<void> {
|
|
|
99
137
|
// Tests
|
|
100
138
|
// ---------------------------------------------------------------------------
|
|
101
139
|
|
|
140
|
+
function resetState(): void {
|
|
141
|
+
state.seedCallCount = 0;
|
|
142
|
+
state.seedShouldReject = null;
|
|
143
|
+
state.warnCalls = [];
|
|
144
|
+
state.infoCalls = [];
|
|
145
|
+
state.ensureCollectionCallCount = 0;
|
|
146
|
+
state.ensureCollectionResult = { migrated: false };
|
|
147
|
+
state.ensureCollectionThrows = null;
|
|
148
|
+
state.countResult = 0;
|
|
149
|
+
state.listPagesResult = [];
|
|
150
|
+
state.enqueueCalls = [];
|
|
151
|
+
}
|
|
152
|
+
|
|
102
153
|
describe("maybeSeedMemoryV2Skills (daemon startup gate)", () => {
|
|
103
|
-
beforeEach(
|
|
104
|
-
state.flagOverrides = {};
|
|
105
|
-
state.seedCallCount = 0;
|
|
106
|
-
state.seedShouldReject = null;
|
|
107
|
-
state.warnCalls = [];
|
|
108
|
-
});
|
|
154
|
+
beforeEach(resetState);
|
|
109
155
|
|
|
110
|
-
test("invokes seedV2SkillEntries when
|
|
111
|
-
state.flagOverrides = { "memory-v2-enabled": true };
|
|
156
|
+
test("invokes seedV2SkillEntries when memory.v2.enabled is true", async () => {
|
|
112
157
|
maybeSeedMemoryV2Skills(makeConfig(true));
|
|
113
158
|
await flushMicrotasks();
|
|
114
159
|
expect(state.seedCallCount).toBe(1);
|
|
115
160
|
expect(state.warnCalls).toHaveLength(0);
|
|
116
161
|
});
|
|
117
162
|
|
|
118
|
-
test("does not invoke seedV2SkillEntries when
|
|
119
|
-
state.flagOverrides = { "memory-v2-enabled": false };
|
|
120
|
-
maybeSeedMemoryV2Skills(makeConfig(true));
|
|
121
|
-
await flushMicrotasks();
|
|
122
|
-
expect(state.seedCallCount).toBe(0);
|
|
123
|
-
expect(state.warnCalls).toHaveLength(0);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("does not invoke seedV2SkillEntries when config.memory.v2.enabled is off", async () => {
|
|
127
|
-
state.flagOverrides = { "memory-v2-enabled": true };
|
|
128
|
-
maybeSeedMemoryV2Skills(makeConfig(false));
|
|
129
|
-
await flushMicrotasks();
|
|
130
|
-
expect(state.seedCallCount).toBe(0);
|
|
131
|
-
expect(state.warnCalls).toHaveLength(0);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("does not invoke seedV2SkillEntries when both gates are off", async () => {
|
|
135
|
-
state.flagOverrides = { "memory-v2-enabled": false };
|
|
163
|
+
test("does not invoke seedV2SkillEntries when memory.v2.enabled is false", async () => {
|
|
136
164
|
maybeSeedMemoryV2Skills(makeConfig(false));
|
|
137
165
|
await flushMicrotasks();
|
|
138
166
|
expect(state.seedCallCount).toBe(0);
|
|
139
167
|
expect(state.warnCalls).toHaveLength(0);
|
|
140
168
|
});
|
|
141
169
|
|
|
142
|
-
test("re-invocation seeds after flag flips on (deferred-init race recovery)", async () => {
|
|
143
|
-
// Models the lifecycle-startup race: the synchronous seed call evaluates
|
|
144
|
-
// the flag while the gateway IPC override fetch is still in flight, falls
|
|
145
|
-
// through to the registry default (`false`), and skips. Once
|
|
146
|
-
// `initFeatureFlagOverrides()` resolves, the chained `.then` re-invokes
|
|
147
|
-
// the seed with the now-populated cache and the flag flips to `true`.
|
|
148
|
-
state.flagOverrides = { "memory-v2-enabled": false };
|
|
149
|
-
maybeSeedMemoryV2Skills(makeConfig(true));
|
|
150
|
-
await flushMicrotasks();
|
|
151
|
-
expect(state.seedCallCount).toBe(0);
|
|
152
|
-
|
|
153
|
-
state.flagOverrides = { "memory-v2-enabled": true };
|
|
154
|
-
maybeSeedMemoryV2Skills(makeConfig(true));
|
|
155
|
-
await flushMicrotasks();
|
|
156
|
-
expect(state.seedCallCount).toBe(1);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
170
|
test("swallows seedV2SkillEntries rejections and logs a warning", async () => {
|
|
160
|
-
state.flagOverrides = { "memory-v2-enabled": true };
|
|
161
171
|
state.seedShouldReject = new Error("seed failed");
|
|
162
172
|
|
|
163
173
|
// The gate must not throw — startup must not block on this.
|
|
@@ -172,3 +182,80 @@ describe("maybeSeedMemoryV2Skills (daemon startup gate)", () => {
|
|
|
172
182
|
expect(msg).toBe("Failed to seed v2 skill entries");
|
|
173
183
|
});
|
|
174
184
|
});
|
|
185
|
+
|
|
186
|
+
describe("maybeRebuildMemoryV2Concepts (daemon startup gate)", () => {
|
|
187
|
+
beforeEach(resetState);
|
|
188
|
+
|
|
189
|
+
test("does nothing when memory.v2.enabled is false", async () => {
|
|
190
|
+
await maybeRebuildMemoryV2Concepts(makeConfig(false));
|
|
191
|
+
|
|
192
|
+
expect(state.ensureCollectionCallCount).toBe(0);
|
|
193
|
+
expect(state.enqueueCalls).toEqual([]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("enqueues memory_v2_reembed when the collection was migrated", async () => {
|
|
197
|
+
state.ensureCollectionResult = { migrated: true };
|
|
198
|
+
|
|
199
|
+
await maybeRebuildMemoryV2Concepts(makeConfig(true));
|
|
200
|
+
|
|
201
|
+
expect(state.ensureCollectionCallCount).toBe(1);
|
|
202
|
+
expect(state.enqueueCalls).toEqual([
|
|
203
|
+
{ type: "memory_v2_reembed", payload: {} },
|
|
204
|
+
]);
|
|
205
|
+
// Migrated path skips the count probe — drift detection is the trigger.
|
|
206
|
+
// (The mock's countConceptPagePoints would silently return 0 either way,
|
|
207
|
+
// but keeping the path conditional keeps the lifecycle hook predictable.)
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("enqueues reembed when the collection is empty but pages exist on disk (crash-mid-rebuild recovery)", async () => {
|
|
211
|
+
state.ensureCollectionResult = { migrated: false };
|
|
212
|
+
state.countResult = 0;
|
|
213
|
+
state.listPagesResult = ["people/alice", "topics/zsh"];
|
|
214
|
+
|
|
215
|
+
await maybeRebuildMemoryV2Concepts(makeConfig(true));
|
|
216
|
+
|
|
217
|
+
expect(state.enqueueCalls).toEqual([
|
|
218
|
+
{ type: "memory_v2_reembed", payload: {} },
|
|
219
|
+
]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("does not enqueue when the collection is healthy and populated", async () => {
|
|
223
|
+
state.ensureCollectionResult = { migrated: false };
|
|
224
|
+
state.countResult = 1185;
|
|
225
|
+
state.listPagesResult = ["people/alice"];
|
|
226
|
+
|
|
227
|
+
await maybeRebuildMemoryV2Concepts(makeConfig(true));
|
|
228
|
+
|
|
229
|
+
expect(state.enqueueCalls).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("does not enqueue when the collection is empty AND no pages exist on disk (fresh workspace)", async () => {
|
|
233
|
+
state.ensureCollectionResult = { migrated: false };
|
|
234
|
+
state.countResult = 0;
|
|
235
|
+
state.listPagesResult = [];
|
|
236
|
+
|
|
237
|
+
await maybeRebuildMemoryV2Concepts(makeConfig(true));
|
|
238
|
+
|
|
239
|
+
expect(state.enqueueCalls).toEqual([]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("swallows ensureConceptPageCollection failures and logs a warning", async () => {
|
|
243
|
+
state.ensureCollectionThrows = new Error("Qdrant unreachable");
|
|
244
|
+
|
|
245
|
+
// Must not throw — startup never blocks on this gate.
|
|
246
|
+
let thrown: unknown = null;
|
|
247
|
+
try {
|
|
248
|
+
await maybeRebuildMemoryV2Concepts(makeConfig(true));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
thrown = err;
|
|
251
|
+
}
|
|
252
|
+
expect(thrown).toBeNull();
|
|
253
|
+
|
|
254
|
+
expect(state.enqueueCalls).toEqual([]);
|
|
255
|
+
expect(state.warnCalls.length).toBeGreaterThan(0);
|
|
256
|
+
const lastWarn = state.warnCalls[state.warnCalls.length - 1];
|
|
257
|
+
expect((lastWarn.obj as { err: Error }).err.message).toBe(
|
|
258
|
+
"Qdrant unreachable",
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -95,6 +95,97 @@ describe("notification decision fallback copy", () => {
|
|
|
95
95
|
);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
test("enforces guardian-facing popup copy for heartbeat alerts", async () => {
|
|
99
|
+
configuredProvider = { sendMessage: async () => ({}) };
|
|
100
|
+
extractedToolUse = {
|
|
101
|
+
input: {
|
|
102
|
+
shouldNotify: true,
|
|
103
|
+
selectedChannels: ["vellum"],
|
|
104
|
+
reasoningSummary: "Heartbeat found a useful follow-up.",
|
|
105
|
+
renderedCopy: {
|
|
106
|
+
vellum: {
|
|
107
|
+
title: "Heartbeat Follow-up",
|
|
108
|
+
body: "The daily tracker is ready; consider reminding the guardian to review it before the next check-in.",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
dedupeKey: "heartbeat:test",
|
|
112
|
+
confidence: 0.9,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const decision = await evaluateSignal(
|
|
117
|
+
makeSignal({
|
|
118
|
+
sourceEventName: "heartbeat.alert",
|
|
119
|
+
sourceChannel: "watcher",
|
|
120
|
+
contextPayload: {
|
|
121
|
+
summary:
|
|
122
|
+
"The daily tracker is ready; consider reminding the guardian to review it before the next check-in.",
|
|
123
|
+
conversationTitle: "Running Habit Tracking",
|
|
124
|
+
},
|
|
125
|
+
attentionHints: {
|
|
126
|
+
requiresAction: true,
|
|
127
|
+
urgency: "medium",
|
|
128
|
+
isAsyncBackground: true,
|
|
129
|
+
visibleInSourceNow: false,
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
["vellum"] as NotificationChannel[],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
136
|
+
expect(decision.renderedCopy.vellum?.title).toBe("Heartbeat Alert");
|
|
137
|
+
expect(decision.renderedCopy.vellum?.body).toBe(
|
|
138
|
+
"I found something worth your attention in a heartbeat check. Open the conversation for details.",
|
|
139
|
+
);
|
|
140
|
+
expect(decision.renderedCopy.vellum?.body).not.toContain(
|
|
141
|
+
"reminding the guardian",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("keeps direct guardian-facing heartbeat copy", async () => {
|
|
146
|
+
configuredProvider = { sendMessage: async () => ({}) };
|
|
147
|
+
extractedToolUse = {
|
|
148
|
+
input: {
|
|
149
|
+
shouldNotify: true,
|
|
150
|
+
selectedChannels: ["vellum"],
|
|
151
|
+
reasoningSummary: "Heartbeat found a useful follow-up.",
|
|
152
|
+
renderedCopy: {
|
|
153
|
+
vellum: {
|
|
154
|
+
title: "Tracker Ready",
|
|
155
|
+
body: "Your daily tracker is ready. Review it before the next check-in.",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
dedupeKey: "heartbeat:direct-copy-test",
|
|
159
|
+
confidence: 0.9,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const decision = await evaluateSignal(
|
|
164
|
+
makeSignal({
|
|
165
|
+
sourceEventName: "heartbeat.alert",
|
|
166
|
+
sourceChannel: "watcher",
|
|
167
|
+
contextPayload: {
|
|
168
|
+
summary:
|
|
169
|
+
"The daily tracker is ready; consider reminding the guardian to review it before the next check-in.",
|
|
170
|
+
conversationTitle: "Running Habit Tracking",
|
|
171
|
+
},
|
|
172
|
+
attentionHints: {
|
|
173
|
+
requiresAction: true,
|
|
174
|
+
urgency: "medium",
|
|
175
|
+
isAsyncBackground: true,
|
|
176
|
+
visibleInSourceNow: false,
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
["vellum"] as NotificationChannel[],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
183
|
+
expect(decision.renderedCopy.vellum?.title).toBe("Tracker Ready");
|
|
184
|
+
expect(decision.renderedCopy.vellum?.body).toBe(
|
|
185
|
+
"Your daily tracker is ready. Review it before the next check-in.",
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
98
189
|
test("enforces free-text answer instructions for guardian.question when requestCode exists", async () => {
|
|
99
190
|
const signal = makeSignal({
|
|
100
191
|
contextPayload: {
|
|
@@ -373,6 +373,28 @@ describe("notification decision strategy", () => {
|
|
|
373
373
|
"A guardian question needs your attention",
|
|
374
374
|
);
|
|
375
375
|
});
|
|
376
|
+
|
|
377
|
+
test("heartbeat.alert fallback avoids intermediary-instruction popup copy", () => {
|
|
378
|
+
const signal = makeSignal({
|
|
379
|
+
sourceEventName: "heartbeat.alert",
|
|
380
|
+
contextPayload: {
|
|
381
|
+
summary:
|
|
382
|
+
"The daily tracker is ready; consider reminding the guardian to review it before the next check-in.",
|
|
383
|
+
conversationTitle: "Running Habit Tracking",
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const copy = composeFallbackCopy(signal, channels);
|
|
388
|
+
expect(copy.vellum).toBeDefined();
|
|
389
|
+
expect(copy.vellum!.title).toBe("Heartbeat Alert");
|
|
390
|
+
expect(copy.vellum!.body).toBe(
|
|
391
|
+
"I found something worth your attention in a heartbeat check. Open the conversation for details.",
|
|
392
|
+
);
|
|
393
|
+
expect(copy.vellum!.conversationSeedMessage).toContain(
|
|
394
|
+
"consider reminding the guardian",
|
|
395
|
+
);
|
|
396
|
+
expect(copy.telegram!.deliveryText).toBe(copy.vellum!.body);
|
|
397
|
+
});
|
|
376
398
|
});
|
|
377
399
|
|
|
378
400
|
// -- NotificationChannel type correctness ----------------------------------
|
|
@@ -62,6 +62,13 @@ let mockUpsertAppImpl:
|
|
|
62
62
|
let mockOrchestrateOAuthConnect: (
|
|
63
63
|
opts: Record<string, unknown>,
|
|
64
64
|
) => Promise<Record<string, unknown>>;
|
|
65
|
+
let mockCliIpcCall: (
|
|
66
|
+
operationId: string,
|
|
67
|
+
params?: Record<string, unknown>,
|
|
68
|
+
) => Promise<Record<string, unknown>> = async () => ({
|
|
69
|
+
ok: false,
|
|
70
|
+
error: "Could not connect to assistant daemon (test default)",
|
|
71
|
+
});
|
|
65
72
|
let mockGetAppByProviderAndClientId: (
|
|
66
73
|
provider: string,
|
|
67
74
|
clientId: string,
|
|
@@ -335,6 +342,15 @@ mock.module("../oauth/connect-orchestrator.js", () => ({
|
|
|
335
342
|
mockOrchestrateOAuthConnect(opts),
|
|
336
343
|
}));
|
|
337
344
|
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Mock cli-client (IPC) — used by `oauth connect` for daemon-orchestrated flow
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
mock.module("../ipc/cli-client.js", () => ({
|
|
350
|
+
cliIpcCall: (operationId: string, params?: Record<string, unknown>) =>
|
|
351
|
+
mockCliIpcCall(operationId, params),
|
|
352
|
+
}));
|
|
353
|
+
|
|
338
354
|
mock.module("../oauth/seed-providers.js", () => ({
|
|
339
355
|
SEEDED_PROVIDER_KEYS: new Set([
|
|
340
356
|
"google",
|
|
@@ -1220,6 +1236,111 @@ describe("assistant oauth connect managed mode — platform 401/403 errors", ()
|
|
|
1220
1236
|
});
|
|
1221
1237
|
});
|
|
1222
1238
|
|
|
1239
|
+
// ---------------------------------------------------------------------------
|
|
1240
|
+
// `assistant oauth connect <provider>` BYO mode — daemon-unreachable behavior.
|
|
1241
|
+
//
|
|
1242
|
+
// We deleted the in-process `orchestrateOAuthConnect` fallback (the same
|
|
1243
|
+
// pattern as the MCP CLI consolidation in #29484). When the daemon is
|
|
1244
|
+
// unreachable, the CLI must surface a clear error and exit 1 — never
|
|
1245
|
+
// silently fall back to in-process flow.
|
|
1246
|
+
// ---------------------------------------------------------------------------
|
|
1247
|
+
|
|
1248
|
+
describe("assistant oauth connect <provider> — daemon unreachable (BYO mode)", () => {
|
|
1249
|
+
beforeEach(() => {
|
|
1250
|
+
// BYO provider with a registered app and no managed-mode wiring.
|
|
1251
|
+
mockGetProvider = () => ({
|
|
1252
|
+
provider: "github",
|
|
1253
|
+
authorizeUrl: "https://github.com/login/oauth/authorize",
|
|
1254
|
+
tokenExchangeUrl: "https://github.com/login/oauth/access_token",
|
|
1255
|
+
defaultScopes: "[]",
|
|
1256
|
+
availableScopes: null,
|
|
1257
|
+
authorizeParams: null,
|
|
1258
|
+
managedServiceConfigKey: null,
|
|
1259
|
+
requiresClientSecret: false,
|
|
1260
|
+
createdAt: Date.now(),
|
|
1261
|
+
updatedAt: Date.now(),
|
|
1262
|
+
});
|
|
1263
|
+
mockGetMostRecentAppByProvider = () => ({
|
|
1264
|
+
provider: "github",
|
|
1265
|
+
clientId: "test-client-id",
|
|
1266
|
+
clientSecretCredentialPath: "oauth_app/github/test/client_secret",
|
|
1267
|
+
});
|
|
1268
|
+
mockGetSecureKey = () => "test-secret";
|
|
1269
|
+
mockGetConfig = () => ({ services: {} });
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
test("daemon connect-refused → exit 1 with 'Is the assistant running?'", async () => {
|
|
1273
|
+
mockCliIpcCall = async () => ({
|
|
1274
|
+
ok: false,
|
|
1275
|
+
error: "Could not connect to assistant daemon: ECONNREFUSED",
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
const { exitCode, stdout } = await runCli([
|
|
1279
|
+
"connect",
|
|
1280
|
+
"github",
|
|
1281
|
+
"--no-browser",
|
|
1282
|
+
"--json",
|
|
1283
|
+
]);
|
|
1284
|
+
|
|
1285
|
+
expect(exitCode).toBe(1);
|
|
1286
|
+
const parsed = JSON.parse(stdout);
|
|
1287
|
+
expect(parsed.ok).toBe(false);
|
|
1288
|
+
expect(parsed.error).toContain("Could not reach the assistant");
|
|
1289
|
+
expect(parsed.error).toContain("Is the assistant running?");
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
test("daemon route missing (Unknown method) → exit 1, never falls through to in-process", async () => {
|
|
1293
|
+
let orchestratorCalls = 0;
|
|
1294
|
+
mockOrchestrateOAuthConnect = async () => {
|
|
1295
|
+
orchestratorCalls++;
|
|
1296
|
+
return { success: true, deferred: false, grantedScopes: [] };
|
|
1297
|
+
};
|
|
1298
|
+
mockCliIpcCall = async () => ({
|
|
1299
|
+
ok: false,
|
|
1300
|
+
error: "Unknown method: internal_oauth_connect_start",
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
const { exitCode } = await runCli([
|
|
1304
|
+
"connect",
|
|
1305
|
+
"github",
|
|
1306
|
+
"--no-browser",
|
|
1307
|
+
"--json",
|
|
1308
|
+
]);
|
|
1309
|
+
|
|
1310
|
+
expect(exitCode).toBe(1);
|
|
1311
|
+
// Critical regression guard: the in-process `orchestrateOAuthConnect`
|
|
1312
|
+
// must NOT be invoked from the CLI. The daemon-orchestrated path is
|
|
1313
|
+
// the sole code path; this is the same invariant #29484 established
|
|
1314
|
+
// for the MCP CLI.
|
|
1315
|
+
expect(orchestratorCalls).toBe(0);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
test("daemon HTTP error (statusCode set) → surfaces error verbatim, no fallback", async () => {
|
|
1319
|
+
let orchestratorCalls = 0;
|
|
1320
|
+
mockOrchestrateOAuthConnect = async () => {
|
|
1321
|
+
orchestratorCalls++;
|
|
1322
|
+
return { success: true, deferred: false, grantedScopes: [] };
|
|
1323
|
+
};
|
|
1324
|
+
mockCliIpcCall = async () => ({
|
|
1325
|
+
ok: false,
|
|
1326
|
+
statusCode: 400,
|
|
1327
|
+
error: "service must be registered before connecting",
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
const { exitCode, stdout } = await runCli([
|
|
1331
|
+
"connect",
|
|
1332
|
+
"github",
|
|
1333
|
+
"--no-browser",
|
|
1334
|
+
"--json",
|
|
1335
|
+
]);
|
|
1336
|
+
|
|
1337
|
+
expect(exitCode).toBe(1);
|
|
1338
|
+
const parsed = JSON.parse(stdout);
|
|
1339
|
+
expect(parsed.error).toContain("service must be registered");
|
|
1340
|
+
expect(orchestratorCalls).toBe(0);
|
|
1341
|
+
});
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1223
1344
|
// ---------------------------------------------------------------------------
|
|
1224
1345
|
// requirePlatformClient — improved error messages
|
|
1225
1346
|
// ---------------------------------------------------------------------------
|