recallnest 2.3.0 → 2.5.4
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/CHANGELOG.md +150 -0
- package/README.md +17 -31
- package/README_CN.md +12 -22
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +782 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +216 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +321 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +250 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +314 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +169 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +95 -6
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +9 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +31 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +1 -0
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -60
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/ingest-2026-04-13.log +0 -87
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { persistSkill, retrieveSkills } from "../skill-engine.js";
|
|
4
|
+
import { SkillInputSchema } from "../skill-schema.js";
|
|
5
|
+
import type { MemoryEntry, MemorySearchResult } from "../store.js";
|
|
6
|
+
|
|
7
|
+
const TEST_SCOPE = "project:test";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Mock Helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
function createMockStore() {
|
|
14
|
+
const entries: MemoryEntry[] = [];
|
|
15
|
+
let seq = 1;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
entries,
|
|
19
|
+
store: {
|
|
20
|
+
async store(entry: Omit<MemoryEntry, "id" | "timestamp"> & { id?: string }): Promise<MemoryEntry> {
|
|
21
|
+
const stored: MemoryEntry = {
|
|
22
|
+
...entry,
|
|
23
|
+
id: entry.id || `auto-${String(seq).padStart(12, "0")}`,
|
|
24
|
+
timestamp: 1_700_000_000_000 + seq,
|
|
25
|
+
metadata: entry.metadata || "{}",
|
|
26
|
+
};
|
|
27
|
+
seq += 1;
|
|
28
|
+
entries.push(stored);
|
|
29
|
+
return stored;
|
|
30
|
+
},
|
|
31
|
+
async update(id: string, updates: Partial<MemoryEntry>): Promise<MemoryEntry | null> {
|
|
32
|
+
const index = entries.findIndex((e) => e.id === id);
|
|
33
|
+
if (index < 0) return null;
|
|
34
|
+
entries[index] = {
|
|
35
|
+
...entries[index],
|
|
36
|
+
...updates,
|
|
37
|
+
timestamp: updates.timestamp ?? entries[index].timestamp,
|
|
38
|
+
};
|
|
39
|
+
return entries[index];
|
|
40
|
+
},
|
|
41
|
+
async getById(id: string): Promise<MemoryEntry | null> {
|
|
42
|
+
return entries.find((e) => e.id === id) || null;
|
|
43
|
+
},
|
|
44
|
+
async vectorSearch(
|
|
45
|
+
_vector: number[],
|
|
46
|
+
limit = 5,
|
|
47
|
+
_minScore = 0.3,
|
|
48
|
+
_scopeFilter?: string[],
|
|
49
|
+
): Promise<MemorySearchResult[]> {
|
|
50
|
+
// Return all entries as results with descending mock scores
|
|
51
|
+
return entries
|
|
52
|
+
.slice(0, limit)
|
|
53
|
+
.map((entry, index) => ({
|
|
54
|
+
entry,
|
|
55
|
+
score: 0.95 - index * 0.1,
|
|
56
|
+
}));
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createMockEmbedder() {
|
|
63
|
+
return {
|
|
64
|
+
async embedPassage(text: string): Promise<number[]> {
|
|
65
|
+
return [text.length, 1, 0];
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validSkillInput(overrides: Record<string, unknown> = {}) {
|
|
71
|
+
return {
|
|
72
|
+
name: "deploy_production",
|
|
73
|
+
description: "Deploy to production environment with safety checks",
|
|
74
|
+
triggerPattern: "When user says 'deploy to prod' or 'release'",
|
|
75
|
+
implementationType: "instruction_sequence" as const,
|
|
76
|
+
implementation: "1. Confirm production target. 2. Run pre-deploy checks. 3. Tag release. 4. Notify channel.",
|
|
77
|
+
scope: TEST_SCOPE,
|
|
78
|
+
source: "agent" as const,
|
|
79
|
+
tags: ["deploy", "production"],
|
|
80
|
+
...overrides,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Tests: SkillInputSchema validation
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
describe("SkillInputSchema validation", () => {
|
|
89
|
+
it("accepts valid skill input", () => {
|
|
90
|
+
const input = validSkillInput();
|
|
91
|
+
const parsed = SkillInputSchema.parse(input);
|
|
92
|
+
expect(parsed.name).toBe("deploy_production");
|
|
93
|
+
expect(parsed.implementationType).toBe("instruction_sequence");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("rejects empty name", () => {
|
|
97
|
+
expect(() => SkillInputSchema.parse(validSkillInput({ name: "" }))).toThrow();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("rejects name exceeding max length", () => {
|
|
101
|
+
const longName = "x".repeat(121);
|
|
102
|
+
expect(() => SkillInputSchema.parse(validSkillInput({ name: longName }))).toThrow();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("rejects empty description", () => {
|
|
106
|
+
expect(() => SkillInputSchema.parse(validSkillInput({ description: "" }))).toThrow();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("rejects description exceeding max length", () => {
|
|
110
|
+
const longDesc = "x".repeat(501);
|
|
111
|
+
expect(() => SkillInputSchema.parse(validSkillInput({ description: longDesc }))).toThrow();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("rejects empty implementation", () => {
|
|
115
|
+
expect(() => SkillInputSchema.parse(validSkillInput({ implementation: "" }))).toThrow();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("rejects implementation exceeding max length", () => {
|
|
119
|
+
const longImpl = "x".repeat(5001);
|
|
120
|
+
expect(() => SkillInputSchema.parse(validSkillInput({ implementation: longImpl }))).toThrow();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("rejects invalid implementationType", () => {
|
|
124
|
+
expect(() => SkillInputSchema.parse(validSkillInput({ implementationType: "ruby" }))).toThrow();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("accepts only instruction_sequence after v2.5 schema 收缩", () => {
|
|
128
|
+
const parsed = SkillInputSchema.parse(validSkillInput({ implementationType: "instruction_sequence" }));
|
|
129
|
+
expect(parsed.implementationType).toBe("instruction_sequence");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("rejects pre-v2.5 implementationType values (bash/python/mcp_tool_chain)", () => {
|
|
133
|
+
// v2.5 (2026-05-27) schema 收缩——这些旧值不再被新写入接受
|
|
134
|
+
// 但 parseSkillFromEntry 仍能读出老 metadata 里的 bash/python(backward compat,见下面 retrieveSkills 测试)
|
|
135
|
+
for (const oldType of ["bash", "python", "mcp_tool_chain"]) {
|
|
136
|
+
expect(() => SkillInputSchema.parse(validSkillInput({ implementationType: oldType }))).toThrow();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("accepts optional inputSchema", () => {
|
|
141
|
+
const parsed = SkillInputSchema.parse(validSkillInput({
|
|
142
|
+
inputSchema: { type: "object", properties: { env: { type: "string" } } },
|
|
143
|
+
}));
|
|
144
|
+
expect(parsed.inputSchema).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("accepts optional verification", () => {
|
|
148
|
+
const parsed = SkillInputSchema.parse(validSkillInput({
|
|
149
|
+
verification: "curl -f https://prod.example.com/health",
|
|
150
|
+
}));
|
|
151
|
+
expect(parsed.verification).toBe("curl -f https://prod.example.com/health");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("defaults source to manual", () => {
|
|
155
|
+
const input = { ...validSkillInput() };
|
|
156
|
+
delete (input as Record<string, unknown>).source;
|
|
157
|
+
const parsed = SkillInputSchema.parse(input);
|
|
158
|
+
expect(parsed.source).toBe("manual");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("defaults tags to empty array", () => {
|
|
162
|
+
const input = { ...validSkillInput() };
|
|
163
|
+
delete (input as Record<string, unknown>).tags;
|
|
164
|
+
const parsed = SkillInputSchema.parse(input);
|
|
165
|
+
expect(parsed.tags).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("rejects too many tags", () => {
|
|
169
|
+
expect(() => SkillInputSchema.parse(validSkillInput({
|
|
170
|
+
tags: ["a", "b", "c", "d", "e", "f", "g"],
|
|
171
|
+
}))).toThrow();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Tests: persistSkill
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
describe("persistSkill", () => {
|
|
180
|
+
it("stores a new skill and returns StoredSkillRecord", async () => {
|
|
181
|
+
const { store } = createMockStore();
|
|
182
|
+
const embedder = createMockEmbedder();
|
|
183
|
+
const result = await persistSkill(store, embedder, validSkillInput());
|
|
184
|
+
|
|
185
|
+
expect(result.name).toBe("deploy_production");
|
|
186
|
+
expect(result.implementationType).toBe("instruction_sequence");
|
|
187
|
+
expect(result.scope).toBe(TEST_SCOPE);
|
|
188
|
+
expect(result.id).toBeTruthy();
|
|
189
|
+
expect(result.storedAt).toBeTruthy();
|
|
190
|
+
expect(result.successCount).toBe(0);
|
|
191
|
+
expect(result.failureCount).toBe(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("stores with category 'patterns' and importance 0.85", async () => {
|
|
195
|
+
const { store, entries } = createMockStore();
|
|
196
|
+
const embedder = createMockEmbedder();
|
|
197
|
+
await persistSkill(store, embedder, validSkillInput());
|
|
198
|
+
|
|
199
|
+
expect(entries).toHaveLength(1);
|
|
200
|
+
expect(entries[0].category).toBe("patterns");
|
|
201
|
+
expect(entries[0].importance).toBe(0.85);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("builds full text with skill structure", async () => {
|
|
205
|
+
const { store, entries } = createMockStore();
|
|
206
|
+
const embedder = createMockEmbedder();
|
|
207
|
+
await persistSkill(store, embedder, validSkillInput());
|
|
208
|
+
|
|
209
|
+
const text = entries[0].text;
|
|
210
|
+
expect(text).toContain("Skill: deploy_production");
|
|
211
|
+
expect(text).toContain("Description: Deploy to production environment");
|
|
212
|
+
expect(text).toContain("Trigger: When user says");
|
|
213
|
+
expect(text).toContain("Type: instruction_sequence");
|
|
214
|
+
expect(text).toContain("Implementation: 1. Confirm production target");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("includes skill metadata in entry", async () => {
|
|
218
|
+
const { store, entries } = createMockStore();
|
|
219
|
+
const embedder = createMockEmbedder();
|
|
220
|
+
await persistSkill(store, embedder, validSkillInput());
|
|
221
|
+
|
|
222
|
+
const meta = JSON.parse(entries[0].metadata || "{}");
|
|
223
|
+
expect(meta.skill).toBeDefined();
|
|
224
|
+
expect(meta.skill.name).toBe("deploy_production");
|
|
225
|
+
expect(meta.skill.implementationType).toBe("instruction_sequence");
|
|
226
|
+
expect(meta.capture).toBe("skill_schema_v1");
|
|
227
|
+
expect(meta.boundary.layer).toBe("durable");
|
|
228
|
+
expect(meta.evolution).toBeDefined();
|
|
229
|
+
expect(meta.evolution.version).toBe(1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("uses deterministic ID from scope + name (idempotent)", async () => {
|
|
233
|
+
const { store } = createMockStore();
|
|
234
|
+
const embedder = createMockEmbedder();
|
|
235
|
+
|
|
236
|
+
const first = await persistSkill(store, embedder, validSkillInput());
|
|
237
|
+
const second = await persistSkill(store, embedder, validSkillInput());
|
|
238
|
+
|
|
239
|
+
// Same scope + name should produce same ID
|
|
240
|
+
expect(first.id).toBe(second.id);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("updates existing skill on second store (version bump)", async () => {
|
|
244
|
+
const { store, entries } = createMockStore();
|
|
245
|
+
const embedder = createMockEmbedder();
|
|
246
|
+
|
|
247
|
+
await persistSkill(store, embedder, validSkillInput());
|
|
248
|
+
expect(entries).toHaveLength(1);
|
|
249
|
+
|
|
250
|
+
await persistSkill(store, embedder, validSkillInput({
|
|
251
|
+
implementation: "#!/bin/bash\necho 'new deploy...'",
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
// Still only 1 entry (updated, not duplicated)
|
|
255
|
+
expect(entries).toHaveLength(1);
|
|
256
|
+
|
|
257
|
+
const meta = JSON.parse(entries[0].metadata || "{}");
|
|
258
|
+
expect(meta.evolution.version).toBe(2);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("preserves different skills with different names", async () => {
|
|
262
|
+
const { store, entries } = createMockStore();
|
|
263
|
+
const embedder = createMockEmbedder();
|
|
264
|
+
|
|
265
|
+
await persistSkill(store, embedder, validSkillInput({ name: "skill_a" }));
|
|
266
|
+
await persistSkill(store, embedder, validSkillInput({ name: "skill_b" }));
|
|
267
|
+
|
|
268
|
+
expect(entries).toHaveLength(2);
|
|
269
|
+
expect(entries[0].id).not.toBe(entries[1].id);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("includes canonicalKey in metadata", async () => {
|
|
273
|
+
const { store, entries } = createMockStore();
|
|
274
|
+
const embedder = createMockEmbedder();
|
|
275
|
+
await persistSkill(store, embedder, validSkillInput());
|
|
276
|
+
|
|
277
|
+
const meta = JSON.parse(entries[0].metadata || "{}");
|
|
278
|
+
expect(meta.canonicalKey).toBe("patterns:skill:deploy_production");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("throws on invalid input", async () => {
|
|
282
|
+
const { store } = createMockStore();
|
|
283
|
+
const embedder = createMockEmbedder();
|
|
284
|
+
|
|
285
|
+
await expect(persistSkill(store, embedder, { name: "" })).rejects.toThrow();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Tests: retrieveSkills
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
describe("retrieveSkills", () => {
|
|
294
|
+
it("returns matching skills with scores", async () => {
|
|
295
|
+
const { store, entries } = createMockStore();
|
|
296
|
+
const embedder = createMockEmbedder();
|
|
297
|
+
|
|
298
|
+
// Manually add a skill-like entry
|
|
299
|
+
await persistSkill(
|
|
300
|
+
{ store: store.store, update: store.update, getById: store.getById },
|
|
301
|
+
embedder,
|
|
302
|
+
validSkillInput(),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const results = await retrieveSkills(store, embedder, "deploy to production");
|
|
306
|
+
|
|
307
|
+
expect(results).toHaveLength(1);
|
|
308
|
+
expect(results[0].skill.name).toBe("deploy_production");
|
|
309
|
+
expect(results[0].score).toBeGreaterThan(0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("filters out non-skill pattern entries", async () => {
|
|
313
|
+
const { store, entries } = createMockStore();
|
|
314
|
+
const embedder = createMockEmbedder();
|
|
315
|
+
|
|
316
|
+
// Add a non-skill patterns entry (no skill metadata)
|
|
317
|
+
entries.push({
|
|
318
|
+
id: "non-skill-entry",
|
|
319
|
+
text: "Workflow pattern: some pattern",
|
|
320
|
+
vector: [10, 1, 0],
|
|
321
|
+
category: "patterns",
|
|
322
|
+
scope: TEST_SCOPE,
|
|
323
|
+
importance: 0.8,
|
|
324
|
+
timestamp: Date.now(),
|
|
325
|
+
metadata: JSON.stringify({ source: "agent", capture: "workflow_pattern_schema_v1" }),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const results = await retrieveSkills(store, embedder, "some query");
|
|
329
|
+
expect(results).toHaveLength(0);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("filters out non-patterns category entries", async () => {
|
|
333
|
+
const { store, entries } = createMockStore();
|
|
334
|
+
const embedder = createMockEmbedder();
|
|
335
|
+
|
|
336
|
+
// Add an entry with a different category
|
|
337
|
+
entries.push({
|
|
338
|
+
id: "events-entry",
|
|
339
|
+
text: "Some event happened",
|
|
340
|
+
vector: [5, 1, 0],
|
|
341
|
+
category: "events",
|
|
342
|
+
scope: TEST_SCOPE,
|
|
343
|
+
importance: 0.7,
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
metadata: "{}",
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const results = await retrieveSkills(store, embedder, "event query");
|
|
349
|
+
expect(results).toHaveLength(0);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("returns empty array when no skills match", async () => {
|
|
353
|
+
const { store } = createMockStore();
|
|
354
|
+
const embedder = createMockEmbedder();
|
|
355
|
+
|
|
356
|
+
// No entries at all
|
|
357
|
+
const results = await retrieveSkills(store, embedder, "anything");
|
|
358
|
+
expect(results).toEqual([]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("respects limit parameter", async () => {
|
|
362
|
+
const { store, entries } = createMockStore();
|
|
363
|
+
const embedder = createMockEmbedder();
|
|
364
|
+
|
|
365
|
+
// Add multiple skill entries
|
|
366
|
+
for (let i = 0; i < 5; i++) {
|
|
367
|
+
entries.push({
|
|
368
|
+
id: `skill-${i}`,
|
|
369
|
+
text: `Skill: skill_${i}`,
|
|
370
|
+
vector: [i, 1, 0],
|
|
371
|
+
category: "patterns",
|
|
372
|
+
scope: TEST_SCOPE,
|
|
373
|
+
importance: 0.85,
|
|
374
|
+
timestamp: Date.now() + i,
|
|
375
|
+
metadata: JSON.stringify({
|
|
376
|
+
source: "agent",
|
|
377
|
+
capture: "skill_schema_v1",
|
|
378
|
+
skill: {
|
|
379
|
+
name: `skill_${i}`,
|
|
380
|
+
description: `Skill ${i}`,
|
|
381
|
+
triggerPattern: "trigger",
|
|
382
|
+
implementationType: "bash",
|
|
383
|
+
implementation: `echo ${i}`,
|
|
384
|
+
successCount: 0,
|
|
385
|
+
failureCount: 0,
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const results = await retrieveSkills(store, embedder, "skill query", undefined, 2);
|
|
392
|
+
expect(results).toHaveLength(2);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("defaults limit to 3", async () => {
|
|
396
|
+
const { store, entries } = createMockStore();
|
|
397
|
+
const embedder = createMockEmbedder();
|
|
398
|
+
|
|
399
|
+
// Add 5 skill entries
|
|
400
|
+
for (let i = 0; i < 5; i++) {
|
|
401
|
+
entries.push({
|
|
402
|
+
id: `skill-${i}`,
|
|
403
|
+
text: `Skill: skill_${i}`,
|
|
404
|
+
vector: [i, 1, 0],
|
|
405
|
+
category: "patterns",
|
|
406
|
+
scope: TEST_SCOPE,
|
|
407
|
+
importance: 0.85,
|
|
408
|
+
timestamp: Date.now() + i,
|
|
409
|
+
metadata: JSON.stringify({
|
|
410
|
+
source: "agent",
|
|
411
|
+
capture: "skill_schema_v1",
|
|
412
|
+
skill: {
|
|
413
|
+
name: `skill_${i}`,
|
|
414
|
+
description: `Skill ${i}`,
|
|
415
|
+
triggerPattern: "trigger",
|
|
416
|
+
implementationType: "bash",
|
|
417
|
+
implementation: `echo ${i}`,
|
|
418
|
+
successCount: 0,
|
|
419
|
+
failureCount: 0,
|
|
420
|
+
},
|
|
421
|
+
}),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const results = await retrieveSkills(store, embedder, "skill query");
|
|
426
|
+
expect(results).toHaveLength(3);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("clamps limit to range [1, 10]", async () => {
|
|
430
|
+
const { store, entries } = createMockStore();
|
|
431
|
+
const embedder = createMockEmbedder();
|
|
432
|
+
|
|
433
|
+
// Add 1 entry
|
|
434
|
+
entries.push({
|
|
435
|
+
id: "skill-0",
|
|
436
|
+
text: "Skill: test_skill",
|
|
437
|
+
vector: [1, 1, 0],
|
|
438
|
+
category: "patterns",
|
|
439
|
+
scope: TEST_SCOPE,
|
|
440
|
+
importance: 0.85,
|
|
441
|
+
timestamp: Date.now(),
|
|
442
|
+
metadata: JSON.stringify({
|
|
443
|
+
source: "agent",
|
|
444
|
+
capture: "skill_schema_v1",
|
|
445
|
+
skill: {
|
|
446
|
+
name: "test_skill",
|
|
447
|
+
description: "A test skill",
|
|
448
|
+
triggerPattern: "trigger",
|
|
449
|
+
implementationType: "bash",
|
|
450
|
+
implementation: "echo test",
|
|
451
|
+
successCount: 0,
|
|
452
|
+
failureCount: 0,
|
|
453
|
+
},
|
|
454
|
+
}),
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// limit=0 should clamp to 1
|
|
458
|
+
const results = await retrieveSkills(store, embedder, "test", undefined, 0);
|
|
459
|
+
expect(results).toHaveLength(1);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("parses skill record fields correctly from metadata", async () => {
|
|
463
|
+
const { store, entries } = createMockStore();
|
|
464
|
+
const embedder = createMockEmbedder();
|
|
465
|
+
|
|
466
|
+
entries.push({
|
|
467
|
+
id: "skill-full",
|
|
468
|
+
text: "Skill: full_skill",
|
|
469
|
+
vector: [1, 1, 0],
|
|
470
|
+
category: "patterns",
|
|
471
|
+
scope: TEST_SCOPE,
|
|
472
|
+
importance: 0.85,
|
|
473
|
+
timestamp: 1_700_000_000_000,
|
|
474
|
+
metadata: JSON.stringify({
|
|
475
|
+
source: "manual",
|
|
476
|
+
tags: ["ci", "deploy"],
|
|
477
|
+
capture: "skill_schema_v1",
|
|
478
|
+
skill: {
|
|
479
|
+
name: "full_skill",
|
|
480
|
+
description: "A full featured skill",
|
|
481
|
+
triggerPattern: "When deploying",
|
|
482
|
+
implementationType: "python",
|
|
483
|
+
implementation: "print('hello')",
|
|
484
|
+
inputSchema: { type: "object" },
|
|
485
|
+
verification: "check output",
|
|
486
|
+
successCount: 5,
|
|
487
|
+
failureCount: 1,
|
|
488
|
+
lastRefinedAt: "2025-01-01T00:00:00.000Z",
|
|
489
|
+
},
|
|
490
|
+
}),
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const results = await retrieveSkills(store, embedder, "deploy");
|
|
494
|
+
expect(results).toHaveLength(1);
|
|
495
|
+
|
|
496
|
+
const skill = results[0].skill;
|
|
497
|
+
expect(skill.name).toBe("full_skill");
|
|
498
|
+
expect(skill.description).toBe("A full featured skill");
|
|
499
|
+
expect(skill.triggerPattern).toBe("When deploying");
|
|
500
|
+
expect(skill.implementationType).toBe("python");
|
|
501
|
+
expect(skill.implementation).toBe("print('hello')");
|
|
502
|
+
expect(skill.inputSchema).toEqual({ type: "object" });
|
|
503
|
+
expect(skill.verification).toBe("check output");
|
|
504
|
+
expect(skill.source).toBe("manual");
|
|
505
|
+
expect(skill.tags).toEqual(["ci", "deploy"]);
|
|
506
|
+
expect(skill.successCount).toBe(5);
|
|
507
|
+
expect(skill.failureCount).toBe(1);
|
|
508
|
+
expect(skill.lastRefinedAt).toBe("2025-01-01T00:00:00.000Z");
|
|
509
|
+
expect(skill.storedAt).toBeTruthy();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("skips entries with malformed metadata", async () => {
|
|
513
|
+
const { store, entries } = createMockStore();
|
|
514
|
+
const embedder = createMockEmbedder();
|
|
515
|
+
|
|
516
|
+
entries.push({
|
|
517
|
+
id: "bad-meta",
|
|
518
|
+
text: "Skill: broken",
|
|
519
|
+
vector: [1, 1, 0],
|
|
520
|
+
category: "patterns",
|
|
521
|
+
scope: TEST_SCOPE,
|
|
522
|
+
importance: 0.85,
|
|
523
|
+
timestamp: Date.now(),
|
|
524
|
+
metadata: "not-valid-json{{{",
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const results = await retrieveSkills(store, embedder, "broken");
|
|
528
|
+
expect(results).toHaveLength(0);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("skips entries where skill object lacks required fields", async () => {
|
|
532
|
+
const { store, entries } = createMockStore();
|
|
533
|
+
const embedder = createMockEmbedder();
|
|
534
|
+
|
|
535
|
+
entries.push({
|
|
536
|
+
id: "incomplete-skill",
|
|
537
|
+
text: "Skill: incomplete",
|
|
538
|
+
vector: [1, 1, 0],
|
|
539
|
+
category: "patterns",
|
|
540
|
+
scope: TEST_SCOPE,
|
|
541
|
+
importance: 0.85,
|
|
542
|
+
timestamp: Date.now(),
|
|
543
|
+
metadata: JSON.stringify({
|
|
544
|
+
skill: { name: "incomplete" },
|
|
545
|
+
// missing implementation
|
|
546
|
+
}),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const results = await retrieveSkills(store, embedder, "incomplete");
|
|
550
|
+
expect(results).toHaveLength(0);
|
|
551
|
+
});
|
|
552
|
+
});
|