@cleocode/core 2026.4.36 → 2026.4.38
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/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/task-hooks.js +11 -0
- package/dist/hooks/handlers/task-hooks.js.map +1 -1
- package/dist/index.js +647 -34
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +3 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -1
- package/dist/internal.js.map +1 -1
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/decisions.js +18 -0
- package/dist/memory/decisions.js.map +1 -1
- package/dist/memory/engine-compat.d.ts +17 -0
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/engine-compat.js +36 -0
- package/dist/memory/engine-compat.js.map +1 -1
- package/dist/memory/graph-memory-bridge.d.ts +158 -0
- package/dist/memory/graph-memory-bridge.d.ts.map +1 -0
- package/dist/memory/graph-memory-bridge.js +519 -0
- package/dist/memory/graph-memory-bridge.js.map +1 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +2 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/learnings.d.ts.map +1 -1
- package/dist/memory/learnings.js +18 -0
- package/dist/memory/learnings.js.map +1 -1
- package/dist/memory/llm-extraction.d.ts.map +1 -1
- package/dist/memory/llm-extraction.js.map +1 -1
- package/dist/memory/patterns.d.ts.map +1 -1
- package/dist/memory/patterns.js +18 -0
- package/dist/memory/patterns.js.map +1 -1
- package/dist/memory/quality-feedback.d.ts +129 -0
- package/dist/memory/quality-feedback.d.ts.map +1 -0
- package/dist/memory/quality-feedback.js +449 -0
- package/dist/memory/quality-feedback.js.map +1 -0
- package/dist/memory/sleep-consolidation.d.ts +98 -0
- package/dist/memory/sleep-consolidation.d.ts.map +1 -0
- package/dist/memory/sleep-consolidation.js +706 -0
- package/dist/memory/sleep-consolidation.js.map +1 -0
- package/dist/memory/temporal-supersession.d.ts +155 -0
- package/dist/memory/temporal-supersession.d.ts.map +1 -0
- package/dist/memory/temporal-supersession.js +406 -0
- package/dist/memory/temporal-supersession.js.map +1 -0
- package/package.json +6 -6
- package/src/hooks/handlers/task-hooks.ts +11 -0
- package/src/internal.ts +12 -0
- package/src/memory/__tests__/graph-memory-bridge.test.ts +357 -0
- package/src/memory/__tests__/llm-extraction.test.ts +17 -0
- package/src/memory/__tests__/quality-feedback.test.ts +418 -0
- package/src/memory/__tests__/sleep-consolidation.test.ts +790 -0
- package/src/memory/__tests__/temporal-supersession.test.ts +534 -0
- package/src/memory/decisions.ts +24 -0
- package/src/memory/engine-compat.ts +37 -0
- package/src/memory/graph-memory-bridge.ts +751 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/learnings.ts +24 -0
- package/src/memory/patterns.ts +24 -0
- package/src/memory/quality-feedback.ts +640 -0
- package/src/memory/sleep-consolidation.ts +932 -0
- package/src/memory/temporal-supersession.ts +568 -0
- package/src/store/__tests__/performance-safety.test.ts +4 -4
|
@@ -69,6 +69,17 @@ export async function handleToolComplete(
|
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
// T555: Correlate retrieval outcomes against this task completion.
|
|
73
|
+
// Fire-and-forget: quality score adjustments must never block the response.
|
|
74
|
+
setImmediate(async () => {
|
|
75
|
+
try {
|
|
76
|
+
const { correlateOutcomes } = await import('../../memory/quality-feedback.js');
|
|
77
|
+
await correlateOutcomes(projectRoot);
|
|
78
|
+
} catch {
|
|
79
|
+
// Quality correlation errors must never surface to the task complete flow
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
72
83
|
// T138: Refresh memory bridge after task completes (best-effort)
|
|
73
84
|
await maybeRefreshMemoryBridge(projectRoot);
|
|
74
85
|
}
|
package/src/internal.ts
CHANGED
|
@@ -218,6 +218,7 @@ export {
|
|
|
218
218
|
memoryPatternFind,
|
|
219
219
|
memoryPatternStats,
|
|
220
220
|
memoryPatternStore,
|
|
221
|
+
memoryQualityReport,
|
|
221
222
|
memoryReasonSimilar,
|
|
222
223
|
memoryReasonWhy,
|
|
223
224
|
memorySearchHybrid,
|
|
@@ -244,6 +245,17 @@ export {
|
|
|
244
245
|
pipelineManifestStats,
|
|
245
246
|
readManifestEntries,
|
|
246
247
|
} from './memory/pipeline-manifest-sqlite.js';
|
|
248
|
+
export type {
|
|
249
|
+
CorrelateOutcomesResult,
|
|
250
|
+
MemoryOutcome,
|
|
251
|
+
MemoryQualityReport,
|
|
252
|
+
} from './memory/quality-feedback.js';
|
|
253
|
+
// Memory — quality feedback loop (T555)
|
|
254
|
+
export {
|
|
255
|
+
correlateOutcomes,
|
|
256
|
+
getMemoryQualityReport,
|
|
257
|
+
trackMemoryUsage,
|
|
258
|
+
} from './memory/quality-feedback.js';
|
|
247
259
|
// Metrics
|
|
248
260
|
export {
|
|
249
261
|
autoRecordDispatchTokenUsage,
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for graph-memory-bridge.ts
|
|
3
|
+
*
|
|
4
|
+
* Validates the four public functions:
|
|
5
|
+
* - linkMemoryToCode (manual edge creation)
|
|
6
|
+
* - autoLinkMemories (entity extraction + auto-linking)
|
|
7
|
+
* - queryMemoriesForCode (traverse code→memory edges)
|
|
8
|
+
* - queryCodeForMemory (traverse memory→code edges)
|
|
9
|
+
* - listCodeLinks (list all code_reference edges)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
|
+
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
|
|
19
|
+
describe('graph-memory-bridge', () => {
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cleo-gmb-'));
|
|
22
|
+
await mkdir(join(tempDir, '.cleo'), { recursive: true });
|
|
23
|
+
process.env['CLEO_DIR'] = join(tempDir, '.cleo');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
const { closeBrainDb } = await import('../../store/brain-sqlite.js');
|
|
28
|
+
const { resetNexusDbState } = await import('../../store/nexus-sqlite.js');
|
|
29
|
+
closeBrainDb();
|
|
30
|
+
resetNexusDbState();
|
|
31
|
+
delete process.env['CLEO_DIR'];
|
|
32
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/** Seed a brain page node directly. */
|
|
40
|
+
async function seedBrainNode(
|
|
41
|
+
id: string,
|
|
42
|
+
label: string,
|
|
43
|
+
nodeType = 'observation',
|
|
44
|
+
qualityScore = 0.7,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
const { getBrainDb } = await import('../../store/brain-sqlite.js');
|
|
47
|
+
const { brainPageNodes } = await import('../../store/brain-schema.js');
|
|
48
|
+
const db = await getBrainDb(tempDir);
|
|
49
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
50
|
+
await db
|
|
51
|
+
.insert(brainPageNodes)
|
|
52
|
+
.values({
|
|
53
|
+
id,
|
|
54
|
+
nodeType: nodeType as import('../../store/brain-schema.js').BrainNodeType,
|
|
55
|
+
label,
|
|
56
|
+
qualityScore,
|
|
57
|
+
contentHash: null,
|
|
58
|
+
lastActivityAt: now,
|
|
59
|
+
createdAt: now,
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
})
|
|
62
|
+
.onConflictDoNothing();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Seed a nexus node directly. */
|
|
66
|
+
async function seedNexusNode(
|
|
67
|
+
id: string,
|
|
68
|
+
label: string,
|
|
69
|
+
name: string | null,
|
|
70
|
+
filePath: string | null,
|
|
71
|
+
kind = 'function',
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
const { getNexusDb } = await import('../../store/nexus-sqlite.js');
|
|
74
|
+
const { nexusNodes } = await import('../../store/nexus-schema.js');
|
|
75
|
+
|
|
76
|
+
const db = await getNexusDb();
|
|
77
|
+
await db
|
|
78
|
+
.insert(nexusNodes)
|
|
79
|
+
.values({
|
|
80
|
+
id,
|
|
81
|
+
projectId: 'test-project',
|
|
82
|
+
kind: kind as import('../../store/nexus-schema.js').NexusNodeKind,
|
|
83
|
+
label,
|
|
84
|
+
name,
|
|
85
|
+
filePath,
|
|
86
|
+
})
|
|
87
|
+
.onConflictDoNothing();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// linkMemoryToCode
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
describe('linkMemoryToCode', () => {
|
|
95
|
+
it('returns false when nexus node does not exist', async () => {
|
|
96
|
+
const { linkMemoryToCode } = await import('../graph-memory-bridge.js');
|
|
97
|
+
const result = await linkMemoryToCode(tempDir, 'observation:O-test', 'nonexistent::symbol');
|
|
98
|
+
expect(result).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('creates a code_reference edge when both nodes exist', async () => {
|
|
102
|
+
const { linkMemoryToCode } = await import('../graph-memory-bridge.js');
|
|
103
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
104
|
+
|
|
105
|
+
await seedBrainNode('observation:O-abc', 'Test observation');
|
|
106
|
+
await seedNexusNode(
|
|
107
|
+
'src/store/brain-sqlite.ts::getBrainDb',
|
|
108
|
+
'getBrainDb',
|
|
109
|
+
'getBrainDb',
|
|
110
|
+
'src/store/brain-sqlite.ts',
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const result = await linkMemoryToCode(
|
|
114
|
+
tempDir,
|
|
115
|
+
'observation:O-abc',
|
|
116
|
+
'src/store/brain-sqlite.ts::getBrainDb',
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(result).toBe(true);
|
|
120
|
+
|
|
121
|
+
// Verify the edge was written to brain.db
|
|
122
|
+
const brainNative = getBrainNativeDb();
|
|
123
|
+
const edge = brainNative
|
|
124
|
+
?.prepare(
|
|
125
|
+
`SELECT from_id, to_id, edge_type, weight FROM brain_page_edges
|
|
126
|
+
WHERE from_id = ? AND to_id = ? AND edge_type = 'code_reference'`,
|
|
127
|
+
)
|
|
128
|
+
.get('observation:O-abc', 'src/store/brain-sqlite.ts::getBrainDb') as
|
|
129
|
+
| { from_id: string; to_id: string; edge_type: string; weight: number }
|
|
130
|
+
| undefined;
|
|
131
|
+
|
|
132
|
+
expect(edge).toBeDefined();
|
|
133
|
+
expect(edge?.edge_type).toBe('code_reference');
|
|
134
|
+
expect(edge?.weight).toBe(1.0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('is idempotent — calling twice does not duplicate the edge', async () => {
|
|
138
|
+
const { linkMemoryToCode } = await import('../graph-memory-bridge.js');
|
|
139
|
+
const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
|
|
140
|
+
|
|
141
|
+
await seedBrainNode('observation:O-dup', 'Dup test');
|
|
142
|
+
await seedNexusNode('src/file.ts::myFunc', 'myFunc', 'myFunc', 'src/file.ts');
|
|
143
|
+
|
|
144
|
+
await linkMemoryToCode(tempDir, 'observation:O-dup', 'src/file.ts::myFunc');
|
|
145
|
+
await linkMemoryToCode(tempDir, 'observation:O-dup', 'src/file.ts::myFunc');
|
|
146
|
+
|
|
147
|
+
const brainNative = getBrainNativeDb();
|
|
148
|
+
const rows = brainNative
|
|
149
|
+
?.prepare(
|
|
150
|
+
`SELECT COUNT(*) as cnt FROM brain_page_edges
|
|
151
|
+
WHERE from_id = 'observation:O-dup'
|
|
152
|
+
AND to_id = 'src/file.ts::myFunc'
|
|
153
|
+
AND edge_type = 'code_reference'`,
|
|
154
|
+
)
|
|
155
|
+
.get() as { cnt: number } | undefined;
|
|
156
|
+
|
|
157
|
+
expect(rows?.cnt).toBe(1);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// autoLinkMemories
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
describe('autoLinkMemories', () => {
|
|
166
|
+
it('returns zero counts when brain and nexus are empty', async () => {
|
|
167
|
+
const { autoLinkMemories } = await import('../graph-memory-bridge.js');
|
|
168
|
+
const result = await autoLinkMemories(tempDir);
|
|
169
|
+
expect(result.scanned).toBe(0);
|
|
170
|
+
expect(result.linked).toBe(0);
|
|
171
|
+
expect(result.links).toHaveLength(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('matches brain nodes to nexus by exact symbol name', async () => {
|
|
175
|
+
const { autoLinkMemories } = await import('../graph-memory-bridge.js');
|
|
176
|
+
|
|
177
|
+
// Brain node label contains the symbol name
|
|
178
|
+
await seedBrainNode(
|
|
179
|
+
'observation:O-sym1',
|
|
180
|
+
'Used getBrainDb to initialize the database connection',
|
|
181
|
+
'observation',
|
|
182
|
+
0.8,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
await seedNexusNode(
|
|
186
|
+
'src/store/brain-sqlite.ts::getBrainDb',
|
|
187
|
+
'getBrainDb',
|
|
188
|
+
'getBrainDb',
|
|
189
|
+
'src/store/brain-sqlite.ts',
|
|
190
|
+
'function',
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const result = await autoLinkMemories(tempDir);
|
|
194
|
+
|
|
195
|
+
expect(result.scanned).toBeGreaterThan(0);
|
|
196
|
+
expect(result.linked).toBeGreaterThanOrEqual(1);
|
|
197
|
+
|
|
198
|
+
const link = result.links.find(
|
|
199
|
+
(l) => l.brainNodeId === 'observation:O-sym1' && l.nexusNodeId.includes('getBrainDb'),
|
|
200
|
+
);
|
|
201
|
+
expect(link).toBeDefined();
|
|
202
|
+
expect(['exact-symbol', 'fuzzy-symbol']).toContain(link?.matchStrategy);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('matches brain nodes to nexus by file path in label', async () => {
|
|
206
|
+
const { autoLinkMemories } = await import('../graph-memory-bridge.js');
|
|
207
|
+
|
|
208
|
+
await seedBrainNode(
|
|
209
|
+
'decision:D-fp1',
|
|
210
|
+
'Modified src/store/brain-sqlite.ts to add WAL mode support',
|
|
211
|
+
'decision',
|
|
212
|
+
0.9,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
await seedNexusNode(
|
|
216
|
+
'src/store/brain-sqlite.ts',
|
|
217
|
+
'brain-sqlite.ts',
|
|
218
|
+
null,
|
|
219
|
+
'src/store/brain-sqlite.ts',
|
|
220
|
+
'file',
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const result = await autoLinkMemories(tempDir);
|
|
224
|
+
|
|
225
|
+
expect(result.linked).toBeGreaterThanOrEqual(1);
|
|
226
|
+
const link = result.links.find(
|
|
227
|
+
(l) => l.brainNodeId === 'decision:D-fp1' && l.nexusNodeId === 'src/store/brain-sqlite.ts',
|
|
228
|
+
);
|
|
229
|
+
expect(link).toBeDefined();
|
|
230
|
+
expect(link?.matchStrategy).toBe('exact-file');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('marks already-linked edges as alreadyLinked', async () => {
|
|
234
|
+
const { autoLinkMemories, linkMemoryToCode } = await import('../graph-memory-bridge.js');
|
|
235
|
+
|
|
236
|
+
await seedBrainNode('observation:O-pre', 'calls getBrainDb directly', 'observation', 0.8);
|
|
237
|
+
await seedNexusNode(
|
|
238
|
+
'src/store/brain-sqlite.ts::getBrainDb',
|
|
239
|
+
'getBrainDb',
|
|
240
|
+
'getBrainDb',
|
|
241
|
+
'src/store/brain-sqlite.ts',
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Pre-link manually
|
|
245
|
+
await linkMemoryToCode(tempDir, 'observation:O-pre', 'src/store/brain-sqlite.ts::getBrainDb');
|
|
246
|
+
|
|
247
|
+
// Auto-link should count it as already linked
|
|
248
|
+
const result = await autoLinkMemories(tempDir);
|
|
249
|
+
expect(result.alreadyLinked).toBeGreaterThanOrEqual(1);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// queryMemoriesForCode
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
describe('queryMemoriesForCode', () => {
|
|
258
|
+
it('returns empty memories for unknown symbol', async () => {
|
|
259
|
+
const { queryMemoriesForCode } = await import('../graph-memory-bridge.js');
|
|
260
|
+
const result = await queryMemoriesForCode(tempDir, 'unknown::symbol');
|
|
261
|
+
expect(result.nexusNodeId).toBe('unknown::symbol');
|
|
262
|
+
expect(result.memories).toHaveLength(0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('returns memories linked to a given nexus node', async () => {
|
|
266
|
+
const { linkMemoryToCode, queryMemoriesForCode } = await import('../graph-memory-bridge.js');
|
|
267
|
+
|
|
268
|
+
await seedBrainNode('observation:O-q1', 'Query test observation');
|
|
269
|
+
await seedNexusNode('src/file.ts::myFunc', 'myFunc', 'myFunc', 'src/file.ts');
|
|
270
|
+
|
|
271
|
+
await linkMemoryToCode(tempDir, 'observation:O-q1', 'src/file.ts::myFunc');
|
|
272
|
+
|
|
273
|
+
const result = await queryMemoriesForCode(tempDir, 'src/file.ts::myFunc');
|
|
274
|
+
expect(result.memories).toHaveLength(1);
|
|
275
|
+
expect(result.memories[0]?.nodeId).toBe('observation:O-q1');
|
|
276
|
+
expect(result.memories[0]?.edgeWeight).toBe(1.0);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// queryCodeForMemory
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
describe('queryCodeForMemory', () => {
|
|
285
|
+
it('returns empty codeNodes for unknown memory ID', async () => {
|
|
286
|
+
const { queryCodeForMemory } = await import('../graph-memory-bridge.js');
|
|
287
|
+
const result = await queryCodeForMemory(tempDir, 'observation:O-unknown');
|
|
288
|
+
expect(result.brainNodeId).toBe('observation:O-unknown');
|
|
289
|
+
expect(result.codeNodes).toHaveLength(0);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('returns code nodes linked from a given memory node', async () => {
|
|
293
|
+
const { linkMemoryToCode, queryCodeForMemory } = await import('../graph-memory-bridge.js');
|
|
294
|
+
|
|
295
|
+
await seedBrainNode('decision:D-c1', 'Code for memory test');
|
|
296
|
+
await seedNexusNode(
|
|
297
|
+
'src/store/schema.ts::brainDecisions',
|
|
298
|
+
'brainDecisions',
|
|
299
|
+
'brainDecisions',
|
|
300
|
+
'src/store/schema.ts',
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
await linkMemoryToCode(tempDir, 'decision:D-c1', 'src/store/schema.ts::brainDecisions');
|
|
304
|
+
|
|
305
|
+
const result = await queryCodeForMemory(tempDir, 'decision:D-c1');
|
|
306
|
+
expect(result.codeNodes).toHaveLength(1);
|
|
307
|
+
expect(result.codeNodes[0]?.nexusNodeId).toBe('src/store/schema.ts::brainDecisions');
|
|
308
|
+
expect(result.codeNodes[0]?.kind).toBe('function');
|
|
309
|
+
expect(result.codeNodes[0]?.edgeWeight).toBe(1.0);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// listCodeLinks
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
describe('listCodeLinks', () => {
|
|
318
|
+
it('returns empty array when no code_reference edges exist', async () => {
|
|
319
|
+
const { listCodeLinks } = await import('../graph-memory-bridge.js');
|
|
320
|
+
const result = await listCodeLinks(tempDir);
|
|
321
|
+
expect(result).toHaveLength(0);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns all code_reference edges with enriched metadata', async () => {
|
|
325
|
+
const { linkMemoryToCode, listCodeLinks } = await import('../graph-memory-bridge.js');
|
|
326
|
+
|
|
327
|
+
await seedBrainNode('observation:O-list1', 'List test obs 1');
|
|
328
|
+
await seedBrainNode('observation:O-list2', 'List test obs 2');
|
|
329
|
+
await seedNexusNode('src/a.ts::funcA', 'funcA', 'funcA', 'src/a.ts');
|
|
330
|
+
await seedNexusNode('src/b.ts::funcB', 'funcB', 'funcB', 'src/b.ts');
|
|
331
|
+
|
|
332
|
+
await linkMemoryToCode(tempDir, 'observation:O-list1', 'src/a.ts::funcA');
|
|
333
|
+
await linkMemoryToCode(tempDir, 'observation:O-list2', 'src/b.ts::funcB');
|
|
334
|
+
|
|
335
|
+
const result = await listCodeLinks(tempDir);
|
|
336
|
+
expect(result).toHaveLength(2);
|
|
337
|
+
|
|
338
|
+
const link1 = result.find((l) => l.brainNodeId === 'observation:O-list1');
|
|
339
|
+
expect(link1?.nexusNodeLabel).toBe('funcA');
|
|
340
|
+
expect(link1?.filePath).toBe('src/a.ts');
|
|
341
|
+
expect(link1?.weight).toBe(1.0);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('respects the limit parameter', async () => {
|
|
345
|
+
const { linkMemoryToCode, listCodeLinks } = await import('../graph-memory-bridge.js');
|
|
346
|
+
|
|
347
|
+
for (let i = 0; i < 5; i++) {
|
|
348
|
+
await seedBrainNode(`observation:O-lim${i}`, `Limit test ${i}`);
|
|
349
|
+
await seedNexusNode(`src/x${i}.ts::fn${i}`, `fn${i}`, `fn${i}`, `src/x${i}.ts`);
|
|
350
|
+
await linkMemoryToCode(tempDir, `observation:O-lim${i}`, `src/x${i}.ts::fn${i}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const result = await listCodeLinks(tempDir, 3);
|
|
354
|
+
expect(result).toHaveLength(3);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -43,6 +43,15 @@ vi.mock('@anthropic-ai/sdk/helpers/zod', () => ({
|
|
|
43
43
|
zodOutputFormat: vi.fn().mockReturnValue({ _mock: 'zodOutputFormat' }),
|
|
44
44
|
}));
|
|
45
45
|
|
|
46
|
+
// Mock the key resolver so tests don't depend on filesystem state
|
|
47
|
+
// (~/.claude/.credentials.json, ~/.local/share/cleo/anthropic-key).
|
|
48
|
+
// Default: no key. Tests that inject a client bypass this anyway.
|
|
49
|
+
const mockResolveKey = vi.fn().mockReturnValue(null);
|
|
50
|
+
vi.mock('../anthropic-key-resolver.js', () => ({
|
|
51
|
+
resolveAnthropicApiKey: (...args: unknown[]) => mockResolveKey(...args),
|
|
52
|
+
clearAnthropicKeyCache: vi.fn(),
|
|
53
|
+
}));
|
|
54
|
+
|
|
46
55
|
// Mock the SDK entry point so buildAnthropicClient doesn't touch the network.
|
|
47
56
|
// Tests that need this will inject a custom client via options.client instead.
|
|
48
57
|
vi.mock('@anthropic-ai/sdk', () => {
|
|
@@ -52,6 +61,14 @@ vi.mock('@anthropic-ai/sdk', () => {
|
|
|
52
61
|
return { default: MockAnthropic };
|
|
53
62
|
});
|
|
54
63
|
|
|
64
|
+
// Mock the key resolver so tests don't depend on filesystem state
|
|
65
|
+
// (~/.claude/.credentials.json, ~/.local/share/cleo/anthropic-key).
|
|
66
|
+
// Default: no key. Tests that inject a client via options.client bypass this.
|
|
67
|
+
vi.mock('../anthropic-key-resolver.js', () => ({
|
|
68
|
+
resolveAnthropicApiKey: vi.fn().mockReturnValue(null),
|
|
69
|
+
clearAnthropicKeyCache: vi.fn(),
|
|
70
|
+
}));
|
|
71
|
+
|
|
55
72
|
// ---- imports after mocks --------------------------------------------------
|
|
56
73
|
|
|
57
74
|
import { storeDecision } from '../decisions.js';
|