@vellumai/assistant 0.6.0 → 0.6.1
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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +32 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/openapi.yaml +538 -3
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/checker.test.ts +38 -6
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +72 -105
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +62 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-tools.test.ts +17 -27
- package/src/__tests__/test-preload.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -26
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/agent/loop.ts +6 -0
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/cli/__tests__/run-assistant-command.ts +29 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/connect.ts +14 -5
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +1 -120
- package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +79 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +158 -65
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-process.ts +13 -7
- package/src/daemon/conversation-runtime-assembly.ts +300 -306
- package/src/daemon/conversation-tool-setup.ts +44 -14
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +18 -0
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +4 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +63 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +199 -157
- package/src/daemon/message-types/conversations.ts +25 -6
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +6 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +89 -9
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +136 -107
- package/src/memory/conversation-group-migration.ts +1 -1
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -15
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +70 -165
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +25 -4
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +264 -44
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export-routes.ts +23 -275
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -0
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +84 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -1,100 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
3
|
-
|
|
4
|
-
import { eq, like } from "drizzle-orm";
|
|
5
|
-
|
|
6
|
-
mock.module("../util/logger.js", () => ({
|
|
7
|
-
getLogger: () =>
|
|
8
|
-
new Proxy({} as Record<string, unknown>, {
|
|
9
|
-
get: () => () => {},
|
|
10
|
-
}),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
mock.module("../memory/qdrant-client.js", () => ({
|
|
14
|
-
getQdrantClient: () => ({
|
|
15
|
-
searchWithFilter: async () => [],
|
|
16
|
-
hybridSearch: async () => [],
|
|
17
|
-
upsertPoints: async () => {},
|
|
18
|
-
deletePoints: async () => {},
|
|
19
|
-
}),
|
|
20
|
-
initQdrantClient: () => {},
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
// Controllable mock for loadSkillCatalog used by seedCatalogSkillMemories
|
|
24
|
-
let mockLoadSkillCatalog: () => import("../config/skills.js").SkillSummary[] =
|
|
25
|
-
() => [];
|
|
26
|
-
|
|
27
|
-
mock.module("../config/skills.js", () => ({
|
|
28
|
-
loadSkillCatalog: (..._args: unknown[]) => mockLoadSkillCatalog(),
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
// Controllable mock for getCachedCatalogSync used by seedCatalogSkillMemories
|
|
32
|
-
let mockGetCachedCatalogSync: () => import("../skills/catalog-install.js").CatalogSkill[] =
|
|
33
|
-
() => [];
|
|
34
|
-
|
|
35
|
-
mock.module("../skills/catalog-cache.js", () => ({
|
|
36
|
-
getCachedCatalogSync: (..._args: unknown[]) => mockGetCachedCatalogSync(),
|
|
37
|
-
getCatalog: async () => mockGetCachedCatalogSync(),
|
|
38
|
-
invalidateCatalogCache: () => {},
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
// Controllable mock for isAssistantFeatureFlagEnabled used by resolveSkillStates
|
|
42
|
-
let mockIsFeatureFlagEnabled: (key: string) => boolean = () => true;
|
|
43
|
-
|
|
44
|
-
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
45
|
-
isAssistantFeatureFlagEnabled: (key: string, _config: unknown) =>
|
|
46
|
-
mockIsFeatureFlagEnabled(key),
|
|
47
|
-
getAssistantFeatureFlagDefaults: () => ({}),
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
51
|
-
|
|
52
|
-
const TEST_CONFIG = {
|
|
53
|
-
...DEFAULT_CONFIG,
|
|
54
|
-
memory: {
|
|
55
|
-
...DEFAULT_CONFIG.memory,
|
|
56
|
-
enabled: true,
|
|
57
|
-
extraction: {
|
|
58
|
-
...DEFAULT_CONFIG.memory.extraction,
|
|
59
|
-
useLLM: false,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
mock.module("../config/loader.js", () => ({
|
|
65
|
-
loadConfig: () => TEST_CONFIG,
|
|
66
|
-
getConfig: () => TEST_CONFIG,
|
|
67
|
-
loadRawConfig: () => ({}),
|
|
68
|
-
saveRawConfig: () => {},
|
|
69
|
-
invalidateConfigCache: () => {},
|
|
70
|
-
}));
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
71
2
|
|
|
72
3
|
import type { SkillSummary } from "../config/skills.js";
|
|
73
|
-
import {
|
|
74
|
-
import { memoryGraphNodes, memoryJobs } from "../memory/schema.js";
|
|
75
|
-
import {
|
|
76
|
-
buildCapabilityStatement,
|
|
77
|
-
deleteSkillCapabilityMemory,
|
|
78
|
-
fromSkillSummary,
|
|
79
|
-
seedCatalogSkillMemories,
|
|
80
|
-
type SkillCapabilityInput,
|
|
81
|
-
upsertSkillCapabilityMemory,
|
|
82
|
-
} from "../skills/skill-memory.js";
|
|
83
|
-
import { ensureDataDir, getDbPath } from "../util/platform.js";
|
|
84
|
-
|
|
85
|
-
ensureDataDir();
|
|
86
|
-
initializeDb();
|
|
87
|
-
|
|
88
|
-
afterAll(() => {
|
|
89
|
-
resetDb();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
function resetTables() {
|
|
93
|
-
const db = getDb();
|
|
94
|
-
db.run("DELETE FROM memory_embeddings");
|
|
95
|
-
db.run("DELETE FROM memory_graph_nodes");
|
|
96
|
-
db.run("DELETE FROM memory_jobs");
|
|
97
|
-
}
|
|
4
|
+
import { fromSkillSummary } from "../skills/skill-memory.js";
|
|
98
5
|
|
|
99
6
|
function makeSkillSummary(
|
|
100
7
|
overrides: Partial<SkillSummary> = {},
|
|
@@ -111,84 +18,6 @@ function makeSkillSummary(
|
|
|
111
18
|
};
|
|
112
19
|
}
|
|
113
20
|
|
|
114
|
-
// ─── buildCapabilityStatement ────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
describe("buildCapabilityStatement", () => {
|
|
117
|
-
test("includes display name, id, and description", () => {
|
|
118
|
-
const input: SkillCapabilityInput = {
|
|
119
|
-
id: "test-skill",
|
|
120
|
-
displayName: "My Skill",
|
|
121
|
-
description: "A skill for testing",
|
|
122
|
-
};
|
|
123
|
-
const result = buildCapabilityStatement(input);
|
|
124
|
-
expect(result).toContain('"My Skill"');
|
|
125
|
-
expect(result).toContain("(test-skill)");
|
|
126
|
-
expect(result).toContain("A skill for testing");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("includes activation hints when present", () => {
|
|
130
|
-
const input: SkillCapabilityInput = {
|
|
131
|
-
id: "test-skill",
|
|
132
|
-
displayName: "My Skill",
|
|
133
|
-
description: "A skill for testing",
|
|
134
|
-
activationHints: ["user asks to search", "needs web data"],
|
|
135
|
-
};
|
|
136
|
-
const result = buildCapabilityStatement(input);
|
|
137
|
-
expect(result).toContain("Use when:");
|
|
138
|
-
expect(result).toContain("user asks to search");
|
|
139
|
-
expect(result).toContain("needs web data");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("includes avoidWhen routing cues when present", () => {
|
|
143
|
-
const input: SkillCapabilityInput = {
|
|
144
|
-
id: "test-skill",
|
|
145
|
-
displayName: "My Skill",
|
|
146
|
-
description: "A skill for testing",
|
|
147
|
-
avoidWhen: ["user wants local files only", "offline mode"],
|
|
148
|
-
};
|
|
149
|
-
const result = buildCapabilityStatement(input);
|
|
150
|
-
expect(result).toContain("Avoid when:");
|
|
151
|
-
expect(result).toContain("user wants local files only");
|
|
152
|
-
expect(result).toContain("offline mode");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("includes both activationHints and avoidWhen when present", () => {
|
|
156
|
-
const input: SkillCapabilityInput = {
|
|
157
|
-
id: "test-skill",
|
|
158
|
-
displayName: "My Skill",
|
|
159
|
-
description: "A skill for testing",
|
|
160
|
-
activationHints: ["user asks to search"],
|
|
161
|
-
avoidWhen: ["offline mode"],
|
|
162
|
-
};
|
|
163
|
-
const result = buildCapabilityStatement(input);
|
|
164
|
-
expect(result).toContain("Use when: user asks to search.");
|
|
165
|
-
expect(result).toContain("Avoid when: offline mode.");
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("works with just name as displayName", () => {
|
|
169
|
-
const input: SkillCapabilityInput = {
|
|
170
|
-
id: "test-skill",
|
|
171
|
-
displayName: "Test Skill",
|
|
172
|
-
description: "A skill for testing",
|
|
173
|
-
};
|
|
174
|
-
const result = buildCapabilityStatement(input);
|
|
175
|
-
expect(result).toContain('"Test Skill"');
|
|
176
|
-
expect(result).toContain("(test-skill)");
|
|
177
|
-
expect(result).toContain("A skill for testing");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("truncates long statements to 500 chars", () => {
|
|
181
|
-
const longDesc = "x".repeat(600);
|
|
182
|
-
const input: SkillCapabilityInput = {
|
|
183
|
-
id: "test-skill",
|
|
184
|
-
displayName: "Test Skill",
|
|
185
|
-
description: longDesc,
|
|
186
|
-
};
|
|
187
|
-
const result = buildCapabilityStatement(input);
|
|
188
|
-
expect(result.length).toBe(500);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
21
|
// ─── fromSkillSummary ────────────────────────────────────────────────────────
|
|
193
22
|
|
|
194
23
|
describe("fromSkillSummary", () => {
|
|
@@ -234,613 +63,3 @@ describe("fromSkillSummary", () => {
|
|
|
234
63
|
expect(input.description).toBe("Does amazing things");
|
|
235
64
|
});
|
|
236
65
|
});
|
|
237
|
-
|
|
238
|
-
// ─── upsertSkillCapabilityMemory ─────────────────────────────────────────────
|
|
239
|
-
|
|
240
|
-
describe("upsertSkillCapabilityMemory", () => {
|
|
241
|
-
beforeEach(resetTables);
|
|
242
|
-
|
|
243
|
-
test("inserts with correct type, content, confidence, significance", () => {
|
|
244
|
-
const input = fromSkillSummary(makeSkillSummary());
|
|
245
|
-
upsertSkillCapabilityMemory("test-skill", input);
|
|
246
|
-
|
|
247
|
-
const db = getDb();
|
|
248
|
-
const items = db.select().from(memoryGraphNodes).all();
|
|
249
|
-
expect(items).toHaveLength(1);
|
|
250
|
-
expect(items[0].type).toBe("procedural");
|
|
251
|
-
expect(items[0].content).toMatch(/^skill:test-skill\n/);
|
|
252
|
-
expect(items[0].confidence).toBe(1.0);
|
|
253
|
-
expect(items[0].significance).toBe(0.7);
|
|
254
|
-
expect(items[0].fidelity).toBe("vivid");
|
|
255
|
-
expect(items[0].scopeId).toBe("default");
|
|
256
|
-
|
|
257
|
-
// Should also enqueue an embed_graph_node job
|
|
258
|
-
const jobs = db.select().from(memoryJobs).all();
|
|
259
|
-
expect(jobs).toHaveLength(1);
|
|
260
|
-
expect(jobs[0].type).toBe("embed_graph_node");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("is idempotent (same entry only touches lastAccessed)", () => {
|
|
264
|
-
const input = fromSkillSummary(makeSkillSummary());
|
|
265
|
-
upsertSkillCapabilityMemory("test-skill", input);
|
|
266
|
-
|
|
267
|
-
const db = getDb();
|
|
268
|
-
const before = db.select().from(memoryGraphNodes).all();
|
|
269
|
-
expect(before).toHaveLength(1);
|
|
270
|
-
const originalLastAccessed = before[0].lastAccessed;
|
|
271
|
-
|
|
272
|
-
// Upsert again
|
|
273
|
-
upsertSkillCapabilityMemory("test-skill", input);
|
|
274
|
-
|
|
275
|
-
const after = db.select().from(memoryGraphNodes).all();
|
|
276
|
-
expect(after).toHaveLength(1);
|
|
277
|
-
// Same content, so only lastAccessed changes
|
|
278
|
-
expect(after[0].content).toBe(before[0].content);
|
|
279
|
-
expect(after[0].lastAccessed).toBeGreaterThanOrEqual(originalLastAccessed);
|
|
280
|
-
|
|
281
|
-
// Should NOT enqueue a second embed job (only 1 from initial insert)
|
|
282
|
-
const jobs = db.select().from(memoryJobs).all();
|
|
283
|
-
expect(jobs).toHaveLength(1);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test("updates content when description changes", () => {
|
|
287
|
-
const input = fromSkillSummary(
|
|
288
|
-
makeSkillSummary({ description: "Original description" }),
|
|
289
|
-
);
|
|
290
|
-
upsertSkillCapabilityMemory("test-skill", input);
|
|
291
|
-
|
|
292
|
-
const db = getDb();
|
|
293
|
-
const before = db.select().from(memoryGraphNodes).all();
|
|
294
|
-
expect(before).toHaveLength(1);
|
|
295
|
-
expect(before[0].content).toContain("Original description");
|
|
296
|
-
|
|
297
|
-
// Change description
|
|
298
|
-
const updatedInput = fromSkillSummary(
|
|
299
|
-
makeSkillSummary({ description: "Updated description" }),
|
|
300
|
-
);
|
|
301
|
-
upsertSkillCapabilityMemory("test-skill", updatedInput);
|
|
302
|
-
|
|
303
|
-
const after = db.select().from(memoryGraphNodes).all();
|
|
304
|
-
expect(after).toHaveLength(1);
|
|
305
|
-
expect(after[0].content).toContain("Updated description");
|
|
306
|
-
expect(after[0].content).not.toBe(before[0].content);
|
|
307
|
-
|
|
308
|
-
// Should enqueue a second embed job
|
|
309
|
-
const jobs = db.select().from(memoryJobs).all();
|
|
310
|
-
expect(jobs).toHaveLength(2);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test("reactivates soft-deleted items", () => {
|
|
314
|
-
const input = fromSkillSummary(makeSkillSummary());
|
|
315
|
-
upsertSkillCapabilityMemory("test-skill", input);
|
|
316
|
-
|
|
317
|
-
const db = getDb();
|
|
318
|
-
// Soft-delete the item
|
|
319
|
-
db.update(memoryGraphNodes)
|
|
320
|
-
.set({ fidelity: "gone" })
|
|
321
|
-
.where(like(memoryGraphNodes.content, "skill:test-skill\n%"))
|
|
322
|
-
.run();
|
|
323
|
-
|
|
324
|
-
const deleted = db.select().from(memoryGraphNodes).all();
|
|
325
|
-
expect(deleted[0].fidelity).toBe("gone");
|
|
326
|
-
|
|
327
|
-
// Clear jobs from initial insert
|
|
328
|
-
db.run("DELETE FROM memory_jobs");
|
|
329
|
-
|
|
330
|
-
// Upsert again — should reactivate
|
|
331
|
-
upsertSkillCapabilityMemory("test-skill", input);
|
|
332
|
-
|
|
333
|
-
const reactivated = db.select().from(memoryGraphNodes).all();
|
|
334
|
-
expect(reactivated).toHaveLength(1);
|
|
335
|
-
expect(reactivated[0].fidelity).toBe("vivid");
|
|
336
|
-
|
|
337
|
-
// Should enqueue embed job for reactivated item
|
|
338
|
-
const jobs = db.select().from(memoryJobs).all();
|
|
339
|
-
expect(jobs).toHaveLength(1);
|
|
340
|
-
expect(jobs[0].type).toBe("embed_graph_node");
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
test("does not throw on DB error", () => {
|
|
344
|
-
// Close the DB connection to force errors, then reinitialize
|
|
345
|
-
resetDb();
|
|
346
|
-
// getDb() will create a new connection, but we can force a DB error by
|
|
347
|
-
// dropping the table it reads from. Use a fresh DB without initialization.
|
|
348
|
-
// Instead, verify the try/catch by closing and reopening:
|
|
349
|
-
// resetDb closes the connection; getDb lazily reconnects.
|
|
350
|
-
// We drop the memory_graph_nodes table to force an error on the next query.
|
|
351
|
-
const db = getDb();
|
|
352
|
-
db.run("DROP TABLE IF EXISTS memory_graph_nodes");
|
|
353
|
-
|
|
354
|
-
expect(() => {
|
|
355
|
-
upsertSkillCapabilityMemory(
|
|
356
|
-
"test-skill",
|
|
357
|
-
fromSkillSummary(makeSkillSummary()),
|
|
358
|
-
);
|
|
359
|
-
}).not.toThrow();
|
|
360
|
-
|
|
361
|
-
// Restore DB state for subsequent tests.
|
|
362
|
-
// Delete the entire DB so initializeDb recreates it from scratch — just
|
|
363
|
-
// resetting the connection leaves stale migration checkpoints that skip
|
|
364
|
-
// checkpoint-guarded ALTER TABLE migrations (e.g. source_type column).
|
|
365
|
-
resetDb();
|
|
366
|
-
const dbPath = getDbPath();
|
|
367
|
-
for (const ext of ["", "-wal", "-shm"]) {
|
|
368
|
-
rmSync(`${dbPath}${ext}`, { force: true });
|
|
369
|
-
}
|
|
370
|
-
initializeDb();
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
// ─── deleteSkillCapabilityMemory ─────────────────────────────────────────────
|
|
375
|
-
|
|
376
|
-
describe("deleteSkillCapabilityMemory", () => {
|
|
377
|
-
beforeEach(resetTables);
|
|
378
|
-
|
|
379
|
-
test("soft-deletes matching item", () => {
|
|
380
|
-
const input = fromSkillSummary(makeSkillSummary());
|
|
381
|
-
upsertSkillCapabilityMemory("test-skill", input);
|
|
382
|
-
|
|
383
|
-
const db = getDb();
|
|
384
|
-
const before = db.select().from(memoryGraphNodes).all();
|
|
385
|
-
expect(before).toHaveLength(1);
|
|
386
|
-
expect(before[0].fidelity).toBe("vivid");
|
|
387
|
-
|
|
388
|
-
deleteSkillCapabilityMemory("test-skill");
|
|
389
|
-
|
|
390
|
-
const after = db.select().from(memoryGraphNodes).all();
|
|
391
|
-
expect(after).toHaveLength(1);
|
|
392
|
-
expect(after[0].fidelity).toBe("gone");
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test("is no-op for missing item", () => {
|
|
396
|
-
// Should not throw when no matching item exists
|
|
397
|
-
expect(() => {
|
|
398
|
-
deleteSkillCapabilityMemory("nonexistent-skill");
|
|
399
|
-
}).not.toThrow();
|
|
400
|
-
|
|
401
|
-
const db = getDb();
|
|
402
|
-
const items = db.select().from(memoryGraphNodes).all();
|
|
403
|
-
expect(items).toHaveLength(0);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
test("does not throw on DB error", () => {
|
|
407
|
-
// Close and reopen DB, then drop the table to force a query error
|
|
408
|
-
resetDb();
|
|
409
|
-
const db = getDb();
|
|
410
|
-
db.run("DROP TABLE IF EXISTS memory_graph_nodes");
|
|
411
|
-
|
|
412
|
-
expect(() => {
|
|
413
|
-
deleteSkillCapabilityMemory("test-skill");
|
|
414
|
-
}).not.toThrow();
|
|
415
|
-
|
|
416
|
-
// Restore DB state for subsequent tests (see upsert "does not throw" test
|
|
417
|
-
// for rationale on why we delete the DB file).
|
|
418
|
-
resetDb();
|
|
419
|
-
const dbPath = getDbPath();
|
|
420
|
-
for (const ext of ["", "-wal", "-shm"]) {
|
|
421
|
-
rmSync(`${dbPath}${ext}`, { force: true });
|
|
422
|
-
}
|
|
423
|
-
initializeDb();
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// ─── seedCatalogSkillMemories ─────────────────────────────────────────────
|
|
428
|
-
|
|
429
|
-
describe("seedCatalogSkillMemories", () => {
|
|
430
|
-
beforeEach(() => {
|
|
431
|
-
resetTables();
|
|
432
|
-
// Reset mocks to defaults
|
|
433
|
-
mockLoadSkillCatalog = () => [];
|
|
434
|
-
mockIsFeatureFlagEnabled = () => true;
|
|
435
|
-
// Default: non-empty cache so pruning is allowed
|
|
436
|
-
mockGetCachedCatalogSync = () => [
|
|
437
|
-
{ id: "_sentinel", name: "_sentinel", description: "" },
|
|
438
|
-
];
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
test("upserts capability memories for all enabled skills", () => {
|
|
442
|
-
const skills: SkillSummary[] = [
|
|
443
|
-
makeSkillSummary({
|
|
444
|
-
id: "skill-a",
|
|
445
|
-
displayName: "Skill A",
|
|
446
|
-
description: "Does A",
|
|
447
|
-
}),
|
|
448
|
-
makeSkillSummary({
|
|
449
|
-
id: "skill-b",
|
|
450
|
-
displayName: "Skill B",
|
|
451
|
-
description: "Does B",
|
|
452
|
-
}),
|
|
453
|
-
makeSkillSummary({
|
|
454
|
-
id: "skill-c",
|
|
455
|
-
displayName: "Skill C",
|
|
456
|
-
description: "Does C",
|
|
457
|
-
}),
|
|
458
|
-
];
|
|
459
|
-
mockLoadSkillCatalog = () => skills;
|
|
460
|
-
|
|
461
|
-
seedCatalogSkillMemories();
|
|
462
|
-
|
|
463
|
-
const db = getDb();
|
|
464
|
-
const items = db
|
|
465
|
-
.select()
|
|
466
|
-
.from(memoryGraphNodes)
|
|
467
|
-
.where(eq(memoryGraphNodes.type, "procedural"))
|
|
468
|
-
.all();
|
|
469
|
-
expect(items).toHaveLength(3);
|
|
470
|
-
|
|
471
|
-
const contentPrefixes = items.map((i) => i.content.split("\n")[0]).sort();
|
|
472
|
-
expect(contentPrefixes).toEqual([
|
|
473
|
-
"skill:skill-a",
|
|
474
|
-
"skill:skill-b",
|
|
475
|
-
"skill:skill-c",
|
|
476
|
-
]);
|
|
477
|
-
|
|
478
|
-
// All should be vivid
|
|
479
|
-
for (const item of items) {
|
|
480
|
-
expect(item.fidelity).toBe("vivid");
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
test("includes bundled skills in seeded memories", () => {
|
|
485
|
-
const skills: SkillSummary[] = [
|
|
486
|
-
makeSkillSummary({
|
|
487
|
-
id: "managed-skill",
|
|
488
|
-
displayName: "Managed",
|
|
489
|
-
description: "A managed skill",
|
|
490
|
-
source: "managed",
|
|
491
|
-
}),
|
|
492
|
-
makeSkillSummary({
|
|
493
|
-
id: "bundled-skill",
|
|
494
|
-
displayName: "Bundled",
|
|
495
|
-
description: "A bundled skill",
|
|
496
|
-
source: "bundled",
|
|
497
|
-
bundled: true,
|
|
498
|
-
}),
|
|
499
|
-
];
|
|
500
|
-
mockLoadSkillCatalog = () => skills;
|
|
501
|
-
|
|
502
|
-
seedCatalogSkillMemories();
|
|
503
|
-
|
|
504
|
-
const db = getDb();
|
|
505
|
-
const items = db
|
|
506
|
-
.select()
|
|
507
|
-
.from(memoryGraphNodes)
|
|
508
|
-
.where(eq(memoryGraphNodes.type, "procedural"))
|
|
509
|
-
.all();
|
|
510
|
-
expect(items).toHaveLength(2);
|
|
511
|
-
|
|
512
|
-
const contentPrefixes = items.map((i) => i.content.split("\n")[0]).sort();
|
|
513
|
-
expect(contentPrefixes).toEqual(["skill:bundled-skill", "skill:managed-skill"]);
|
|
514
|
-
|
|
515
|
-
for (const item of items) {
|
|
516
|
-
expect(item.fidelity).toBe("vivid");
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
test("excludes bundled skills filtered by allowBundled config", () => {
|
|
521
|
-
const skills: SkillSummary[] = [
|
|
522
|
-
makeSkillSummary({
|
|
523
|
-
id: "allowed-bundled",
|
|
524
|
-
displayName: "Allowed Bundled",
|
|
525
|
-
description: "This bundled skill is allowed",
|
|
526
|
-
source: "bundled",
|
|
527
|
-
bundled: true,
|
|
528
|
-
}),
|
|
529
|
-
makeSkillSummary({
|
|
530
|
-
id: "blocked-bundled",
|
|
531
|
-
displayName: "Blocked Bundled",
|
|
532
|
-
description: "This bundled skill is not in allowBundled",
|
|
533
|
-
source: "bundled",
|
|
534
|
-
bundled: true,
|
|
535
|
-
}),
|
|
536
|
-
makeSkillSummary({
|
|
537
|
-
id: "managed-skill",
|
|
538
|
-
displayName: "Managed",
|
|
539
|
-
description: "A managed skill",
|
|
540
|
-
source: "managed",
|
|
541
|
-
}),
|
|
542
|
-
];
|
|
543
|
-
mockLoadSkillCatalog = () => skills;
|
|
544
|
-
|
|
545
|
-
// Override config to set allowBundled to only allow one bundled skill
|
|
546
|
-
const configWithAllowBundled = {
|
|
547
|
-
...TEST_CONFIG,
|
|
548
|
-
skills: {
|
|
549
|
-
...TEST_CONFIG.skills,
|
|
550
|
-
allowBundled: ["allowed-bundled"],
|
|
551
|
-
},
|
|
552
|
-
};
|
|
553
|
-
mock.module("../config/loader.js", () => ({
|
|
554
|
-
loadConfig: () => configWithAllowBundled,
|
|
555
|
-
getConfig: () => configWithAllowBundled,
|
|
556
|
-
loadRawConfig: () => ({}),
|
|
557
|
-
saveRawConfig: () => {},
|
|
558
|
-
invalidateConfigCache: () => {},
|
|
559
|
-
}));
|
|
560
|
-
|
|
561
|
-
seedCatalogSkillMemories();
|
|
562
|
-
|
|
563
|
-
const db = getDb();
|
|
564
|
-
const items = db
|
|
565
|
-
.select()
|
|
566
|
-
.from(memoryGraphNodes)
|
|
567
|
-
.where(eq(memoryGraphNodes.type, "procedural"))
|
|
568
|
-
.all();
|
|
569
|
-
|
|
570
|
-
// Only allowed-bundled and managed-skill should be seeded
|
|
571
|
-
expect(items).toHaveLength(2);
|
|
572
|
-
const contentPrefixes = items.map((i) => i.content.split("\n")[0]).sort();
|
|
573
|
-
expect(contentPrefixes).toEqual(["skill:allowed-bundled", "skill:managed-skill"]);
|
|
574
|
-
|
|
575
|
-
// Restore default config mock
|
|
576
|
-
mock.module("../config/loader.js", () => ({
|
|
577
|
-
loadConfig: () => TEST_CONFIG,
|
|
578
|
-
getConfig: () => TEST_CONFIG,
|
|
579
|
-
loadRawConfig: () => ({}),
|
|
580
|
-
saveRawConfig: () => {},
|
|
581
|
-
invalidateConfigCache: () => {},
|
|
582
|
-
}));
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
test("prunes stale capabilities for skills no longer enabled", () => {
|
|
586
|
-
// First seed with three skills
|
|
587
|
-
const initialSkills: SkillSummary[] = [
|
|
588
|
-
makeSkillSummary({
|
|
589
|
-
id: "skill-a",
|
|
590
|
-
displayName: "Skill A",
|
|
591
|
-
description: "Does A",
|
|
592
|
-
}),
|
|
593
|
-
makeSkillSummary({
|
|
594
|
-
id: "skill-b",
|
|
595
|
-
displayName: "Skill B",
|
|
596
|
-
description: "Does B",
|
|
597
|
-
}),
|
|
598
|
-
makeSkillSummary({
|
|
599
|
-
id: "skill-c",
|
|
600
|
-
displayName: "Skill C",
|
|
601
|
-
description: "Does C",
|
|
602
|
-
}),
|
|
603
|
-
];
|
|
604
|
-
mockLoadSkillCatalog = () => initialSkills;
|
|
605
|
-
seedCatalogSkillMemories();
|
|
606
|
-
|
|
607
|
-
const db = getDb();
|
|
608
|
-
const beforeItems = db
|
|
609
|
-
.select()
|
|
610
|
-
.from(memoryGraphNodes)
|
|
611
|
-
.where(eq(memoryGraphNodes.type, "procedural"))
|
|
612
|
-
.all();
|
|
613
|
-
expect(beforeItems).toHaveLength(3);
|
|
614
|
-
expect(beforeItems.every((i) => i.fidelity === "vivid")).toBe(true);
|
|
615
|
-
|
|
616
|
-
// Now seed with only skill-a — skill-b and skill-c should be pruned
|
|
617
|
-
mockLoadSkillCatalog = () => [
|
|
618
|
-
makeSkillSummary({
|
|
619
|
-
id: "skill-a",
|
|
620
|
-
displayName: "Skill A",
|
|
621
|
-
description: "Does A",
|
|
622
|
-
}),
|
|
623
|
-
];
|
|
624
|
-
seedCatalogSkillMemories();
|
|
625
|
-
|
|
626
|
-
const afterItems = db
|
|
627
|
-
.select()
|
|
628
|
-
.from(memoryGraphNodes)
|
|
629
|
-
.where(eq(memoryGraphNodes.type, "procedural"))
|
|
630
|
-
.all();
|
|
631
|
-
expect(afterItems).toHaveLength(3); // still 3 rows, but 2 are soft-deleted
|
|
632
|
-
|
|
633
|
-
const active = afterItems.filter((i) => i.fidelity === "vivid");
|
|
634
|
-
const deleted = afterItems.filter((i) => i.fidelity === "gone");
|
|
635
|
-
|
|
636
|
-
expect(active).toHaveLength(1);
|
|
637
|
-
expect(active[0].content).toMatch(/^skill:skill-a\n/);
|
|
638
|
-
|
|
639
|
-
expect(deleted).toHaveLength(2);
|
|
640
|
-
const deletedPrefixes = deleted.map((i) => i.content.split("\n")[0]).sort();
|
|
641
|
-
expect(deletedPrefixes).toEqual(["skill:skill-b", "skill:skill-c"]);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
test("handles empty catalog without errors", () => {
|
|
645
|
-
// Pre-populate a skill so we can verify it gets pruned
|
|
646
|
-
upsertSkillCapabilityMemory(
|
|
647
|
-
"existing-skill",
|
|
648
|
-
fromSkillSummary(makeSkillSummary({ id: "existing-skill" })),
|
|
649
|
-
);
|
|
650
|
-
|
|
651
|
-
const db = getDb();
|
|
652
|
-
const beforeItems = db.select().from(memoryGraphNodes).all();
|
|
653
|
-
expect(beforeItems).toHaveLength(1);
|
|
654
|
-
expect(beforeItems[0].fidelity).toBe("vivid");
|
|
655
|
-
|
|
656
|
-
// Seed with empty catalog
|
|
657
|
-
mockLoadSkillCatalog = () => [];
|
|
658
|
-
seedCatalogSkillMemories();
|
|
659
|
-
|
|
660
|
-
// The existing skill should be pruned (soft-deleted)
|
|
661
|
-
const afterItems = db.select().from(memoryGraphNodes).all();
|
|
662
|
-
expect(afterItems).toHaveLength(1);
|
|
663
|
-
expect(afterItems[0].fidelity).toBe("gone");
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
test("does not prune when catalog cache is empty", () => {
|
|
667
|
-
// Pre-populate a skill
|
|
668
|
-
upsertSkillCapabilityMemory(
|
|
669
|
-
"existing-skill",
|
|
670
|
-
fromSkillSummary(makeSkillSummary({ id: "existing-skill" })),
|
|
671
|
-
);
|
|
672
|
-
|
|
673
|
-
const db = getDb();
|
|
674
|
-
const beforeItems = db.select().from(memoryGraphNodes).all();
|
|
675
|
-
expect(beforeItems).toHaveLength(1);
|
|
676
|
-
expect(beforeItems[0].fidelity).toBe("vivid");
|
|
677
|
-
|
|
678
|
-
// Seed with empty catalog AND empty cache — pruning guard should skip
|
|
679
|
-
mockLoadSkillCatalog = () => [];
|
|
680
|
-
mockGetCachedCatalogSync = () => [];
|
|
681
|
-
seedCatalogSkillMemories();
|
|
682
|
-
|
|
683
|
-
// The existing skill should NOT be pruned because the cache is empty
|
|
684
|
-
const afterItems = db.select().from(memoryGraphNodes).all();
|
|
685
|
-
expect(afterItems).toHaveLength(1);
|
|
686
|
-
expect(afterItems[0].fidelity).toBe("vivid");
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
test("does not prune non-skill capability memories", () => {
|
|
690
|
-
// Pre-insert a non-skill capability memory directly into the DB
|
|
691
|
-
const db = getDb();
|
|
692
|
-
const now = Date.now();
|
|
693
|
-
db.insert(memoryGraphNodes)
|
|
694
|
-
.values({
|
|
695
|
-
id: "cli-doctor-item",
|
|
696
|
-
type: "procedural",
|
|
697
|
-
content: "cli:doctor\nThe doctor command diagnoses issues.",
|
|
698
|
-
fidelity: "vivid",
|
|
699
|
-
confidence: 1.0,
|
|
700
|
-
significance: 0.7,
|
|
701
|
-
sourceType: "inferred",
|
|
702
|
-
scopeId: "default",
|
|
703
|
-
created: now,
|
|
704
|
-
lastAccessed: now,
|
|
705
|
-
lastConsolidated: now,
|
|
706
|
-
emotionalCharge: '{"valence":0,"intensity":0.1,"decayCurve":"linear","decayRate":0.05,"originalIntensity":0.1}',
|
|
707
|
-
stability: 14,
|
|
708
|
-
reinforcementCount: 0,
|
|
709
|
-
lastReinforced: now,
|
|
710
|
-
sourceConversations: "[]",
|
|
711
|
-
narrativeRole: null,
|
|
712
|
-
partOfStory: null,
|
|
713
|
-
})
|
|
714
|
-
.run();
|
|
715
|
-
|
|
716
|
-
// Seed with empty catalog — skill pruner runs but should skip cli:* items
|
|
717
|
-
mockLoadSkillCatalog = () => [];
|
|
718
|
-
seedCatalogSkillMemories();
|
|
719
|
-
|
|
720
|
-
const item = db
|
|
721
|
-
.select()
|
|
722
|
-
.from(memoryGraphNodes)
|
|
723
|
-
.where(like(memoryGraphNodes.content, "cli:doctor\n%"))
|
|
724
|
-
.get();
|
|
725
|
-
expect(item).toBeDefined();
|
|
726
|
-
expect(item!.fidelity).toBe("vivid");
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
test("does not throw when loadSkillCatalog throws", () => {
|
|
730
|
-
mockLoadSkillCatalog = () => {
|
|
731
|
-
throw new Error("Catalog load failure");
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
// Best-effort: should not propagate the error
|
|
735
|
-
expect(() => seedCatalogSkillMemories()).not.toThrow();
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
test("skips skills whose feature flag is disabled", () => {
|
|
739
|
-
const skills: SkillSummary[] = [
|
|
740
|
-
makeSkillSummary({
|
|
741
|
-
id: "unflagged-skill",
|
|
742
|
-
displayName: "Unflagged",
|
|
743
|
-
description: "No flag",
|
|
744
|
-
}),
|
|
745
|
-
makeSkillSummary({
|
|
746
|
-
id: "flagged-skill",
|
|
747
|
-
displayName: "Flagged",
|
|
748
|
-
description: "Has flag",
|
|
749
|
-
featureFlag: "my_gated_feature",
|
|
750
|
-
}),
|
|
751
|
-
];
|
|
752
|
-
mockLoadSkillCatalog = () => skills;
|
|
753
|
-
|
|
754
|
-
// Disable the feature flag for the flagged skill
|
|
755
|
-
mockIsFeatureFlagEnabled = (key: string) => key !== "my_gated_feature";
|
|
756
|
-
|
|
757
|
-
seedCatalogSkillMemories();
|
|
758
|
-
|
|
759
|
-
const db = getDb();
|
|
760
|
-
const items = db
|
|
761
|
-
.select()
|
|
762
|
-
.from(memoryGraphNodes)
|
|
763
|
-
.where(eq(memoryGraphNodes.type, "procedural"))
|
|
764
|
-
.all();
|
|
765
|
-
|
|
766
|
-
// Only the unflagged skill should have a capability row
|
|
767
|
-
expect(items).toHaveLength(1);
|
|
768
|
-
expect(items[0].content).toMatch(/^skill:unflagged-skill\n/);
|
|
769
|
-
expect(items[0].fidelity).toBe("vivid");
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
test("prunes pre-existing capability for a skill whose flag becomes disabled", () => {
|
|
773
|
-
// First seed with both skills, all flags enabled
|
|
774
|
-
const skills: SkillSummary[] = [
|
|
775
|
-
makeSkillSummary({
|
|
776
|
-
id: "unflagged-skill",
|
|
777
|
-
displayName: "Unflagged",
|
|
778
|
-
description: "No flag",
|
|
779
|
-
}),
|
|
780
|
-
makeSkillSummary({
|
|
781
|
-
id: "flagged-skill",
|
|
782
|
-
displayName: "Flagged",
|
|
783
|
-
description: "Has flag",
|
|
784
|
-
featureFlag: "my_gated_feature",
|
|
785
|
-
}),
|
|
786
|
-
];
|
|
787
|
-
mockLoadSkillCatalog = () => skills;
|
|
788
|
-
mockIsFeatureFlagEnabled = () => true;
|
|
789
|
-
seedCatalogSkillMemories();
|
|
790
|
-
|
|
791
|
-
const db = getDb();
|
|
792
|
-
const beforeItems = db
|
|
793
|
-
.select()
|
|
794
|
-
.from(memoryGraphNodes)
|
|
795
|
-
.where(eq(memoryGraphNodes.type, "procedural"))
|
|
796
|
-
.all();
|
|
797
|
-
expect(beforeItems).toHaveLength(2);
|
|
798
|
-
expect(beforeItems.every((i) => i.fidelity === "vivid")).toBe(true);
|
|
799
|
-
|
|
800
|
-
// Now disable the flag — the flagged skill should be pruned
|
|
801
|
-
mockIsFeatureFlagEnabled = (key: string) => key !== "my_gated_feature";
|
|
802
|
-
seedCatalogSkillMemories();
|
|
803
|
-
|
|
804
|
-
const afterItems = db
|
|
805
|
-
.select()
|
|
806
|
-
.from(memoryGraphNodes)
|
|
807
|
-
.where(eq(memoryGraphNodes.type, "procedural"))
|
|
808
|
-
.all();
|
|
809
|
-
expect(afterItems).toHaveLength(2); // still 2 rows, but one soft-deleted
|
|
810
|
-
|
|
811
|
-
const active = afterItems.filter((i) => i.fidelity === "vivid");
|
|
812
|
-
const deleted = afterItems.filter((i) => i.fidelity === "gone");
|
|
813
|
-
|
|
814
|
-
expect(active).toHaveLength(1);
|
|
815
|
-
expect(active[0].content).toMatch(/^skill:unflagged-skill\n/);
|
|
816
|
-
|
|
817
|
-
expect(deleted).toHaveLength(1);
|
|
818
|
-
expect(deleted[0].content).toMatch(/^skill:flagged-skill\n/);
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
test("does not throw on DB error during pruning", () => {
|
|
822
|
-
mockLoadSkillCatalog = () => [
|
|
823
|
-
makeSkillSummary({
|
|
824
|
-
id: "skill-a",
|
|
825
|
-
displayName: "Skill A",
|
|
826
|
-
description: "Does A",
|
|
827
|
-
}),
|
|
828
|
-
];
|
|
829
|
-
|
|
830
|
-
// Drop memory_graph_nodes to force a DB error during the prune phase
|
|
831
|
-
resetDb();
|
|
832
|
-
const db = getDb();
|
|
833
|
-
db.run("DROP TABLE IF EXISTS memory_graph_nodes");
|
|
834
|
-
|
|
835
|
-
expect(() => seedCatalogSkillMemories()).not.toThrow();
|
|
836
|
-
|
|
837
|
-
// Restore DB state for subsequent tests (see upsert "does not throw" test
|
|
838
|
-
// for rationale on why we delete the DB file).
|
|
839
|
-
resetDb();
|
|
840
|
-
const dbPath = getDbPath();
|
|
841
|
-
for (const ext of ["", "-wal", "-shm"]) {
|
|
842
|
-
rmSync(`${dbPath}${ext}`, { force: true });
|
|
843
|
-
}
|
|
844
|
-
initializeDb();
|
|
845
|
-
});
|
|
846
|
-
});
|