@vellumai/assistant 0.8.2 → 0.8.3
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 +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `assistant/src/memory/v2/cli-command-store.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Coverage matrix:
|
|
5
|
+
* - `seedV2CliCommandEntries` enumerates the program tree and upserts one
|
|
6
|
+
* `cli-commands/<name>` point per top-level subcommand.
|
|
7
|
+
* - It skips the auto-injected `help` builtin.
|
|
8
|
+
* - It calls `pruneSlugsWithPrefixExcept("cli-commands/", ...)` with the
|
|
9
|
+
* current id list under the `cli-command` kind so stale rows clear.
|
|
10
|
+
* - The legacy `kind` backfill runs once per process before pruning.
|
|
11
|
+
* - It populates the `entries` cache so `getCliCommandCapability` resolves
|
|
12
|
+
* both bare names and unified-collection slugs.
|
|
13
|
+
* - It swallows embedding-backend errors and leaves prior cache intact.
|
|
14
|
+
* - Stale in-flight results yield to the latest requested generation.
|
|
15
|
+
*
|
|
16
|
+
* Hermetic by design: the embedding backend, Qdrant module, and CLI program
|
|
17
|
+
* tree are module-mocked so the suite never touches a real backend or the
|
|
18
|
+
* full Commander wire-up.
|
|
19
|
+
*/
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
21
|
+
|
|
22
|
+
import { Command } from "commander";
|
|
23
|
+
|
|
24
|
+
import { makeMockLogger } from "../../../__tests__/helpers/mock-logger.js";
|
|
25
|
+
|
|
26
|
+
mock.module("../../../util/logger.js", () => ({
|
|
27
|
+
getLogger: () => makeMockLogger(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Programmable test state
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
interface UpsertCall {
|
|
35
|
+
slug: string;
|
|
36
|
+
dense: number[];
|
|
37
|
+
sparse: { indices: number[]; values: number[] };
|
|
38
|
+
updatedAt: number;
|
|
39
|
+
kind?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PruneCall {
|
|
43
|
+
prefix: string;
|
|
44
|
+
activeSuffixes: readonly string[];
|
|
45
|
+
options?: { kind?: string };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface BackfillCall {
|
|
49
|
+
prefix: string;
|
|
50
|
+
kind: string;
|
|
51
|
+
allowedSuffixes: ReadonlySet<string>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CommandSpec {
|
|
55
|
+
name: string;
|
|
56
|
+
description: string;
|
|
57
|
+
helpText: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface TestState {
|
|
61
|
+
commands: CommandSpec[];
|
|
62
|
+
embedThrows: Error | null;
|
|
63
|
+
embedReturn: number[][];
|
|
64
|
+
sparseReturn: { indices: number[]; values: number[] };
|
|
65
|
+
upsertCalls: UpsertCall[];
|
|
66
|
+
pruneCalls: PruneCall[];
|
|
67
|
+
upsertThrows: Error | null;
|
|
68
|
+
backfillCalls: BackfillCall[];
|
|
69
|
+
backfillThrows: Error | null;
|
|
70
|
+
callSequence: Array<"upsert" | "prune" | "backfill">;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const state: TestState = {
|
|
74
|
+
commands: [],
|
|
75
|
+
embedThrows: null,
|
|
76
|
+
embedReturn: [],
|
|
77
|
+
sparseReturn: { indices: [1], values: [1] },
|
|
78
|
+
upsertCalls: [],
|
|
79
|
+
pruneCalls: [],
|
|
80
|
+
upsertThrows: null,
|
|
81
|
+
backfillCalls: [],
|
|
82
|
+
backfillThrows: null,
|
|
83
|
+
callSequence: [],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
mock.module("../../../config/loader.js", () => ({
|
|
87
|
+
getConfig: () => ({
|
|
88
|
+
memory: {
|
|
89
|
+
qdrant: { url: "http://127.0.0.1:6333", vectorSize: 3, onDisk: false },
|
|
90
|
+
v2: { bm25_k1: 1.2, bm25_b: 0.75 },
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
// Stub the CLI program tree so tests don't need to wire the entire Commander
|
|
96
|
+
// registration graph. Each test stages `state.commands`; the mock returns a
|
|
97
|
+
// fresh `Command` tree whose children carry the staged help text.
|
|
98
|
+
mock.module("../../../cli/program.js", () => ({
|
|
99
|
+
buildCliProgramTree: () => {
|
|
100
|
+
const program = new Command();
|
|
101
|
+
program.name("assistant");
|
|
102
|
+
for (const spec of state.commands) {
|
|
103
|
+
const child = program.command(spec.name).description(spec.description);
|
|
104
|
+
// helpInformation() is built from Commander state — stub it directly so
|
|
105
|
+
// the seeded content matches the test fixture verbatim.
|
|
106
|
+
child.helpInformation = () => spec.helpText;
|
|
107
|
+
}
|
|
108
|
+
return program;
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
mock.module("../../embedding-backend.js", () => ({
|
|
113
|
+
embedWithBackend: async (_config: unknown, inputs: unknown[]) => {
|
|
114
|
+
if (state.embedThrows) throw state.embedThrows;
|
|
115
|
+
const vectors = state.embedReturn.length
|
|
116
|
+
? state.embedReturn
|
|
117
|
+
: inputs.map(() => [0.1, 0.2, 0.3]);
|
|
118
|
+
return { provider: "local", model: "test-model", vectors };
|
|
119
|
+
},
|
|
120
|
+
generateSparseEmbedding: () => state.sparseReturn,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
mock.module("../sparse-bm25.js", () => ({
|
|
124
|
+
generateBm25DocEmbedding: () => state.sparseReturn,
|
|
125
|
+
getConceptPageCorpusStats: () => null,
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
mock.module("../anisotropy.js", () => ({
|
|
129
|
+
applyCorrectionIfCalibrated: async (v: number[]) => v,
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
mock.module("../page-index.js", () => ({
|
|
133
|
+
invalidatePageIndex: () => {},
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
mock.module("../qdrant.js", () => ({
|
|
137
|
+
upsertConceptPageEmbedding: async (params: UpsertCall) => {
|
|
138
|
+
if (state.upsertThrows) throw state.upsertThrows;
|
|
139
|
+
state.callSequence.push("upsert");
|
|
140
|
+
state.upsertCalls.push(params);
|
|
141
|
+
},
|
|
142
|
+
pruneSlugsWithPrefixExcept: async (
|
|
143
|
+
prefix: string,
|
|
144
|
+
activeSuffixes: readonly string[],
|
|
145
|
+
options?: { kind?: string },
|
|
146
|
+
) => {
|
|
147
|
+
state.callSequence.push("prune");
|
|
148
|
+
state.pruneCalls.push({ prefix, activeSuffixes, options });
|
|
149
|
+
},
|
|
150
|
+
backfillKindOnPointsWithPrefix: async (
|
|
151
|
+
prefix: string,
|
|
152
|
+
kind: string,
|
|
153
|
+
allowedSuffixes: ReadonlySet<string>,
|
|
154
|
+
) => {
|
|
155
|
+
if (state.backfillThrows) throw state.backfillThrows;
|
|
156
|
+
state.callSequence.push("backfill");
|
|
157
|
+
state.backfillCalls.push({ prefix, kind, allowedSuffixes });
|
|
158
|
+
return 0;
|
|
159
|
+
},
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const {
|
|
163
|
+
seedV2CliCommandEntries,
|
|
164
|
+
getCliCommandCapability,
|
|
165
|
+
listCliCommandEntries,
|
|
166
|
+
isCliCommandSlug,
|
|
167
|
+
_resetCliCommandStoreForTests,
|
|
168
|
+
} = await import("../cli-command-store.js");
|
|
169
|
+
|
|
170
|
+
function resetState(): void {
|
|
171
|
+
state.commands = [];
|
|
172
|
+
state.embedThrows = null;
|
|
173
|
+
state.embedReturn = [];
|
|
174
|
+
state.sparseReturn = { indices: [1], values: [1] };
|
|
175
|
+
state.upsertCalls.length = 0;
|
|
176
|
+
state.pruneCalls.length = 0;
|
|
177
|
+
state.upsertThrows = null;
|
|
178
|
+
state.backfillCalls.length = 0;
|
|
179
|
+
state.backfillThrows = null;
|
|
180
|
+
state.callSequence.length = 0;
|
|
181
|
+
_resetCliCommandStoreForTests();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
beforeEach(resetState);
|
|
185
|
+
afterEach(resetState);
|
|
186
|
+
|
|
187
|
+
describe("seedV2CliCommandEntries", () => {
|
|
188
|
+
test("upserts each top-level command under cli-commands/<name>", async () => {
|
|
189
|
+
state.commands = [
|
|
190
|
+
{
|
|
191
|
+
name: "attachment",
|
|
192
|
+
description: "Manage file attachments for conversations",
|
|
193
|
+
helpText: "Usage: assistant attachment ...",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "browser",
|
|
197
|
+
description: "Control the browser via the running assistant.",
|
|
198
|
+
helpText: "Usage: assistant browser ...",
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
state.embedReturn = [
|
|
202
|
+
[0.1, 0.2, 0.3],
|
|
203
|
+
[0.4, 0.5, 0.6],
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
await seedV2CliCommandEntries();
|
|
207
|
+
|
|
208
|
+
expect(state.upsertCalls).toHaveLength(2);
|
|
209
|
+
const slugs = state.upsertCalls.map((c) => c.slug).sort();
|
|
210
|
+
expect(slugs).toEqual(["cli-commands/attachment", "cli-commands/browser"]);
|
|
211
|
+
expect(state.upsertCalls.every((c) => c.kind === "cli-command")).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("skips Commander's auto-injected `help` builtin", async () => {
|
|
215
|
+
state.commands = [
|
|
216
|
+
{
|
|
217
|
+
name: "attachment",
|
|
218
|
+
description: "Manage file attachments",
|
|
219
|
+
helpText: "Usage: assistant attachment ...",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "help",
|
|
223
|
+
description: "display help for command",
|
|
224
|
+
helpText: "Usage: assistant help ...",
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
228
|
+
|
|
229
|
+
await seedV2CliCommandEntries();
|
|
230
|
+
|
|
231
|
+
expect(state.upsertCalls.map((c) => c.slug)).toEqual([
|
|
232
|
+
"cli-commands/attachment",
|
|
233
|
+
]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("calls pruneSlugsWithPrefixExcept with kind: 'cli-command'", async () => {
|
|
237
|
+
state.commands = [
|
|
238
|
+
{
|
|
239
|
+
name: "config",
|
|
240
|
+
description: "Manage configuration",
|
|
241
|
+
helpText: "...",
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
245
|
+
|
|
246
|
+
await seedV2CliCommandEntries();
|
|
247
|
+
|
|
248
|
+
expect(state.pruneCalls).toHaveLength(1);
|
|
249
|
+
expect(state.pruneCalls[0].prefix).toBe("cli-commands/");
|
|
250
|
+
expect([...state.pruneCalls[0].activeSuffixes]).toEqual(["config"]);
|
|
251
|
+
expect(state.pruneCalls[0].options).toEqual({ kind: "cli-command" });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("runs backfill before prune so legacy kindless points are reachable", async () => {
|
|
255
|
+
state.commands = [
|
|
256
|
+
{
|
|
257
|
+
name: "config",
|
|
258
|
+
description: "Manage configuration",
|
|
259
|
+
helpText: "...",
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
263
|
+
|
|
264
|
+
await seedV2CliCommandEntries();
|
|
265
|
+
|
|
266
|
+
expect(state.backfillCalls).toHaveLength(1);
|
|
267
|
+
expect(state.backfillCalls[0].prefix).toBe("cli-commands/");
|
|
268
|
+
expect(state.backfillCalls[0].kind).toBe("cli-command");
|
|
269
|
+
expect([...state.backfillCalls[0].allowedSuffixes]).toEqual(["config"]);
|
|
270
|
+
expect(state.callSequence.filter((s) => s !== "upsert")).toEqual([
|
|
271
|
+
"backfill",
|
|
272
|
+
"prune",
|
|
273
|
+
]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("backfill only runs once across repeated seeds in the same process", async () => {
|
|
277
|
+
state.commands = [
|
|
278
|
+
{ name: "config", description: "Manage configuration", helpText: "..." },
|
|
279
|
+
];
|
|
280
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
281
|
+
|
|
282
|
+
await seedV2CliCommandEntries();
|
|
283
|
+
state.embedReturn = [[0.4, 0.5, 0.6]];
|
|
284
|
+
await seedV2CliCommandEntries();
|
|
285
|
+
|
|
286
|
+
expect(state.backfillCalls).toHaveLength(1);
|
|
287
|
+
expect(state.pruneCalls).toHaveLength(2);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("populates the entries cache so getCliCommandCapability resolves both forms", async () => {
|
|
291
|
+
state.commands = [
|
|
292
|
+
{
|
|
293
|
+
name: "attachment",
|
|
294
|
+
description: "Manage file attachments",
|
|
295
|
+
helpText: "Usage: assistant attachment ...",
|
|
296
|
+
},
|
|
297
|
+
];
|
|
298
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
299
|
+
|
|
300
|
+
expect(getCliCommandCapability("attachment")).toBeNull();
|
|
301
|
+
|
|
302
|
+
await seedV2CliCommandEntries();
|
|
303
|
+
|
|
304
|
+
const byId = getCliCommandCapability("attachment");
|
|
305
|
+
const bySlug = getCliCommandCapability("cli-commands/attachment");
|
|
306
|
+
expect(byId).not.toBeNull();
|
|
307
|
+
expect(byId?.id).toBe("attachment");
|
|
308
|
+
expect(byId?.description).toBe("Manage file attachments");
|
|
309
|
+
expect(byId?.content).toContain('"assistant attachment"');
|
|
310
|
+
expect(byId?.content).toContain("Manage file attachments");
|
|
311
|
+
expect(byId?.content).toContain("Usage: assistant attachment ...");
|
|
312
|
+
expect(bySlug).toEqual(byId);
|
|
313
|
+
|
|
314
|
+
expect(getCliCommandCapability("unknown-command")).toBeNull();
|
|
315
|
+
expect(getCliCommandCapability("cli-commands/unknown")).toBeNull();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("isCliCommandSlug recognises the prefix", () => {
|
|
319
|
+
expect(isCliCommandSlug("cli-commands/attachment")).toBe(true);
|
|
320
|
+
expect(isCliCommandSlug("skills/example")).toBe(false);
|
|
321
|
+
expect(isCliCommandSlug("alice")).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("listCliCommandEntries returns frozen sorted snapshots", async () => {
|
|
325
|
+
state.commands = [
|
|
326
|
+
{ name: "browser", description: "Control browser", helpText: "..." },
|
|
327
|
+
{ name: "attachment", description: "Manage attachments", helpText: "." },
|
|
328
|
+
];
|
|
329
|
+
state.embedReturn = [
|
|
330
|
+
[0.1, 0.2, 0.3],
|
|
331
|
+
[0.4, 0.5, 0.6],
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
await seedV2CliCommandEntries();
|
|
335
|
+
|
|
336
|
+
const snapshot = listCliCommandEntries();
|
|
337
|
+
expect(snapshot.map((e) => e.id)).toEqual(["attachment", "browser"]);
|
|
338
|
+
expect(Object.isFrozen(snapshot[0])).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("swallows embedWithBackend errors and leaves prior cache intact", async () => {
|
|
342
|
+
state.commands = [
|
|
343
|
+
{ name: "config", description: "Manage configuration", helpText: "..." },
|
|
344
|
+
];
|
|
345
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
346
|
+
|
|
347
|
+
await seedV2CliCommandEntries();
|
|
348
|
+
const before = getCliCommandCapability("config");
|
|
349
|
+
expect(before).not.toBeNull();
|
|
350
|
+
|
|
351
|
+
state.upsertCalls.length = 0;
|
|
352
|
+
state.pruneCalls.length = 0;
|
|
353
|
+
state.embedThrows = new Error("backend exploded");
|
|
354
|
+
|
|
355
|
+
await expect(seedV2CliCommandEntries()).resolves.toBeUndefined();
|
|
356
|
+
|
|
357
|
+
expect(state.upsertCalls).toHaveLength(0);
|
|
358
|
+
expect(state.pruneCalls).toHaveLength(0);
|
|
359
|
+
const after = getCliCommandCapability("config");
|
|
360
|
+
expect(after).toEqual(before);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("propagates embedding errors when throwOnError is set", async () => {
|
|
364
|
+
state.commands = [
|
|
365
|
+
{ name: "config", description: "Manage configuration", helpText: "..." },
|
|
366
|
+
];
|
|
367
|
+
state.embedThrows = new Error("backend exploded");
|
|
368
|
+
|
|
369
|
+
await expect(
|
|
370
|
+
seedV2CliCommandEntries({ throwOnError: true }),
|
|
371
|
+
).rejects.toThrow("backend exploded");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("skips stale in-flight seed results when a newer refresh is requested", async () => {
|
|
375
|
+
state.commands = [
|
|
376
|
+
{ name: "config", description: "Manage configuration", helpText: "..." },
|
|
377
|
+
];
|
|
378
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
379
|
+
|
|
380
|
+
const firstSeed = seedV2CliCommandEntries();
|
|
381
|
+
state.commands = [
|
|
382
|
+
{ name: "browser", description: "Control browser", helpText: "..." },
|
|
383
|
+
];
|
|
384
|
+
const secondSeed = seedV2CliCommandEntries();
|
|
385
|
+
|
|
386
|
+
await Promise.all([firstSeed, secondSeed]);
|
|
387
|
+
|
|
388
|
+
expect(state.upsertCalls.map((c) => c.slug)).toEqual([
|
|
389
|
+
"cli-commands/browser",
|
|
390
|
+
]);
|
|
391
|
+
expect(getCliCommandCapability("config")).toBeNull();
|
|
392
|
+
expect(getCliCommandCapability("browser")).not.toBeNull();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("empty command set still calls prune to clear stale rows", async () => {
|
|
396
|
+
state.commands = [];
|
|
397
|
+
|
|
398
|
+
await seedV2CliCommandEntries();
|
|
399
|
+
|
|
400
|
+
expect(state.upsertCalls).toHaveLength(0);
|
|
401
|
+
expect(state.pruneCalls).toHaveLength(1);
|
|
402
|
+
expect(state.pruneCalls[0].activeSuffixes).toEqual([]);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Tests for `assistant/src/memory/v2/frontmatter-sweep.ts`.
|
|
3
3
|
*
|
|
4
4
|
* Coverage:
|
|
5
|
+
* - v2 disabled → returns early, no warns, no throw.
|
|
5
6
|
* - Empty workspace → no warns, no throw.
|
|
6
7
|
* - One bad page (unknown frontmatter key) → exactly one warn carrying
|
|
7
8
|
* `errCode: "unrecognized_keys"` and the offending slug.
|
|
@@ -13,6 +14,8 @@ import { tmpdir } from "node:os";
|
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
15
16
|
|
|
17
|
+
import type { AssistantConfig } from "../../../config/schema.js";
|
|
18
|
+
|
|
16
19
|
const warnCalls: Array<{ data: Record<string, unknown>; msg: string }> = [];
|
|
17
20
|
const recordingLogger = {
|
|
18
21
|
warn: (data: Record<string, unknown>, msg: string) => {
|
|
@@ -32,6 +35,17 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
32
35
|
|
|
33
36
|
const { sweepConceptPageFrontmatter } = await import("../frontmatter-sweep.js");
|
|
34
37
|
|
|
38
|
+
/** Minimal config shape the sweep touches; cast to AssistantConfig at the boundary. */
|
|
39
|
+
function makeConfig(v2Enabled: boolean): AssistantConfig {
|
|
40
|
+
return {
|
|
41
|
+
memory: {
|
|
42
|
+
v2: { enabled: v2Enabled },
|
|
43
|
+
},
|
|
44
|
+
} as unknown as AssistantConfig;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const v2On = makeConfig(true);
|
|
48
|
+
|
|
35
49
|
function makeWorkspace(pages: Record<string, string>): string {
|
|
36
50
|
const dir = mkdtempSync(join(tmpdir(), "frontmatter-sweep-"));
|
|
37
51
|
const conceptsDir = join(dir, "memory", "concepts");
|
|
@@ -55,10 +69,17 @@ describe("sweepConceptPageFrontmatter", () => {
|
|
|
55
69
|
warnCalls.length = 0;
|
|
56
70
|
});
|
|
57
71
|
|
|
72
|
+
test("does nothing when memory.v2.enabled is false", async () => {
|
|
73
|
+
// Pass a non-existent dir to prove the gate short-circuits BEFORE any I/O:
|
|
74
|
+
// if the body ran, listPages would surface a warn for the unreadable path.
|
|
75
|
+
await sweepConceptPageFrontmatter(makeConfig(false), "/nonexistent/path");
|
|
76
|
+
expect(warnCalls).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
58
79
|
test("empty workspace: no warns, no throw", async () => {
|
|
59
80
|
const dir = makeWorkspace({});
|
|
60
81
|
try {
|
|
61
|
-
await sweepConceptPageFrontmatter(dir);
|
|
82
|
+
await sweepConceptPageFrontmatter(v2On, dir);
|
|
62
83
|
expect(warnCalls).toHaveLength(0);
|
|
63
84
|
} finally {
|
|
64
85
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -68,7 +89,7 @@ describe("sweepConceptPageFrontmatter", () => {
|
|
|
68
89
|
test("one bad page emits exactly one unrecognized_keys warn", async () => {
|
|
69
90
|
const dir = makeWorkspace({ "bad-one": badPage });
|
|
70
91
|
try {
|
|
71
|
-
await sweepConceptPageFrontmatter(dir);
|
|
92
|
+
await sweepConceptPageFrontmatter(v2On, dir);
|
|
72
93
|
expect(warnCalls).toHaveLength(1);
|
|
73
94
|
expect(warnCalls[0].data.slug).toBe("bad-one");
|
|
74
95
|
expect(warnCalls[0].data.errCode).toBe("unrecognized_keys");
|
|
@@ -85,7 +106,7 @@ describe("sweepConceptPageFrontmatter", () => {
|
|
|
85
106
|
"bad-b": badPage,
|
|
86
107
|
});
|
|
87
108
|
try {
|
|
88
|
-
await sweepConceptPageFrontmatter(dir);
|
|
109
|
+
await sweepConceptPageFrontmatter(v2On, dir);
|
|
89
110
|
const slugs = warnCalls.map((c) => c.data.slug).sort();
|
|
90
111
|
expect(slugs).toEqual(["bad-a", "bad-b"]);
|
|
91
112
|
for (const call of warnCalls) {
|
|
@@ -101,7 +122,7 @@ describe("sweepConceptPageFrontmatter", () => {
|
|
|
101
122
|
mangled: `---\nedges: [unterminated\n---\nbody\n`,
|
|
102
123
|
});
|
|
103
124
|
try {
|
|
104
|
-
await sweepConceptPageFrontmatter(dir);
|
|
125
|
+
await sweepConceptPageFrontmatter(v2On, dir);
|
|
105
126
|
expect(warnCalls.length).toBeGreaterThanOrEqual(1);
|
|
106
127
|
expect(warnCalls.some((c) => c.data.slug === "mangled")).toBe(true);
|
|
107
128
|
} finally {
|