@claude-flow/memory 3.0.0-alpha.10 → 3.0.0-alpha.12
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/agent-memory-scope.d.ts.map +1 -1
- package/dist/agent-memory-scope.js +10 -2
- package/dist/agent-memory-scope.js.map +1 -1
- package/dist/agentdb-backend.d.ts.map +1 -1
- package/dist/agentdb-backend.js +18 -1
- package/dist/agentdb-backend.js.map +1 -1
- package/dist/controller-registry.d.ts +216 -0
- package/dist/controller-registry.d.ts.map +1 -0
- package/dist/controller-registry.js +893 -0
- package/dist/controller-registry.js.map +1 -0
- package/dist/controller-registry.test.d.ts +14 -0
- package/dist/controller-registry.test.d.ts.map +1 -0
- package/dist/controller-registry.test.js +636 -0
- package/dist/controller-registry.test.js.map +1 -0
- package/dist/database-provider.d.ts +2 -1
- package/dist/database-provider.d.ts.map +1 -1
- package/dist/database-provider.js +27 -2
- package/dist/database-provider.js.map +1 -1
- package/dist/hnsw-lite.d.ts +23 -0
- package/dist/hnsw-lite.d.ts.map +1 -0
- package/dist/hnsw-lite.js +168 -0
- package/dist/hnsw-lite.js.map +1 -0
- package/dist/hybrid-backend.d.ts +28 -0
- package/dist/hybrid-backend.d.ts.map +1 -1
- package/dist/hybrid-backend.js +53 -0
- package/dist/hybrid-backend.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/persistent-sona.d.ts +144 -0
- package/dist/persistent-sona.d.ts.map +1 -0
- package/dist/persistent-sona.js +332 -0
- package/dist/persistent-sona.js.map +1 -0
- package/dist/rvf-backend.d.ts +51 -0
- package/dist/rvf-backend.d.ts.map +1 -0
- package/dist/rvf-backend.js +481 -0
- package/dist/rvf-backend.js.map +1 -0
- package/dist/rvf-learning-store.d.ts +139 -0
- package/dist/rvf-learning-store.d.ts.map +1 -0
- package/dist/rvf-learning-store.js +295 -0
- package/dist/rvf-learning-store.js.map +1 -0
- package/dist/rvf-migration.d.ts +45 -0
- package/dist/rvf-migration.d.ts.map +1 -0
- package/dist/rvf-migration.js +254 -0
- package/dist/rvf-migration.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive tests for ControllerRegistry (ADR-053)
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Initialization lifecycle and level-based ordering
|
|
6
|
+
* - Graceful degradation (isolated controller failures)
|
|
7
|
+
* - Config-driven activation
|
|
8
|
+
* - Health check aggregation
|
|
9
|
+
* - Shutdown ordering
|
|
10
|
+
* - Cross-platform path handling (Linux/Mac/Windows)
|
|
11
|
+
* - AgentDB unavailable scenarios
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
14
|
+
import { ControllerRegistry, INIT_LEVELS, } from './controller-registry.js';
|
|
15
|
+
import { LearningBridge } from './learning-bridge.js';
|
|
16
|
+
import { MemoryGraph } from './memory-graph.js';
|
|
17
|
+
import { TieredCacheManager } from './cache-manager.js';
|
|
18
|
+
// ===== Mock Backend =====
|
|
19
|
+
function createMockBackend() {
|
|
20
|
+
const entries = new Map();
|
|
21
|
+
return {
|
|
22
|
+
async initialize() { },
|
|
23
|
+
async shutdown() { },
|
|
24
|
+
async store(entry) {
|
|
25
|
+
entries.set(entry.id, entry);
|
|
26
|
+
},
|
|
27
|
+
async get(id) {
|
|
28
|
+
return entries.get(id) ?? null;
|
|
29
|
+
},
|
|
30
|
+
async getByKey(namespace, key) {
|
|
31
|
+
for (const e of entries.values()) {
|
|
32
|
+
if (e.namespace === namespace && e.key === key)
|
|
33
|
+
return e;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
async update(id, update) {
|
|
38
|
+
const entry = entries.get(id);
|
|
39
|
+
if (!entry)
|
|
40
|
+
return null;
|
|
41
|
+
Object.assign(entry, update, { updatedAt: Date.now() });
|
|
42
|
+
return entry;
|
|
43
|
+
},
|
|
44
|
+
async delete(id) {
|
|
45
|
+
return entries.delete(id);
|
|
46
|
+
},
|
|
47
|
+
async query(query) {
|
|
48
|
+
const results = Array.from(entries.values());
|
|
49
|
+
if (query.namespace) {
|
|
50
|
+
return results.filter((e) => e.namespace === query.namespace).slice(0, query.limit);
|
|
51
|
+
}
|
|
52
|
+
return results.slice(0, query.limit);
|
|
53
|
+
},
|
|
54
|
+
async search(_embedding, _options) {
|
|
55
|
+
return [];
|
|
56
|
+
},
|
|
57
|
+
async bulkInsert(newEntries) {
|
|
58
|
+
for (const entry of newEntries)
|
|
59
|
+
entries.set(entry.id, entry);
|
|
60
|
+
},
|
|
61
|
+
async bulkDelete(ids) {
|
|
62
|
+
let count = 0;
|
|
63
|
+
for (const id of ids) {
|
|
64
|
+
if (entries.delete(id))
|
|
65
|
+
count++;
|
|
66
|
+
}
|
|
67
|
+
return count;
|
|
68
|
+
},
|
|
69
|
+
async count(namespace) {
|
|
70
|
+
if (namespace) {
|
|
71
|
+
return Array.from(entries.values()).filter((e) => e.namespace === namespace).length;
|
|
72
|
+
}
|
|
73
|
+
return entries.size;
|
|
74
|
+
},
|
|
75
|
+
async listNamespaces() {
|
|
76
|
+
return [...new Set(Array.from(entries.values()).map((e) => e.namespace))];
|
|
77
|
+
},
|
|
78
|
+
async clearNamespace(namespace) {
|
|
79
|
+
let count = 0;
|
|
80
|
+
for (const [id, entry] of entries) {
|
|
81
|
+
if (entry.namespace === namespace) {
|
|
82
|
+
entries.delete(id);
|
|
83
|
+
count++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return count;
|
|
87
|
+
},
|
|
88
|
+
async getStats() {
|
|
89
|
+
return {
|
|
90
|
+
totalEntries: entries.size,
|
|
91
|
+
entriesByNamespace: {},
|
|
92
|
+
entriesByType: { episodic: 0, semantic: 0, procedural: 0, working: 0, cache: 0 },
|
|
93
|
+
memoryUsage: 0,
|
|
94
|
+
avgQueryTime: 0,
|
|
95
|
+
avgSearchTime: 0,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
async healthCheck() {
|
|
99
|
+
return {
|
|
100
|
+
status: 'healthy',
|
|
101
|
+
components: {
|
|
102
|
+
storage: { status: 'healthy', latency: 0 },
|
|
103
|
+
index: { status: 'healthy', latency: 0 },
|
|
104
|
+
cache: { status: 'healthy', latency: 0 },
|
|
105
|
+
},
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
issues: [],
|
|
108
|
+
recommendations: [],
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ===== Test Suite =====
|
|
114
|
+
describe('ControllerRegistry', () => {
|
|
115
|
+
let registry;
|
|
116
|
+
let mockBackend;
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
registry = new ControllerRegistry();
|
|
119
|
+
mockBackend = createMockBackend();
|
|
120
|
+
});
|
|
121
|
+
afterEach(async () => {
|
|
122
|
+
if (registry.isInitialized()) {
|
|
123
|
+
await registry.shutdown();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// ----- Lifecycle Tests -----
|
|
127
|
+
describe('initialization lifecycle', () => {
|
|
128
|
+
it('should initialize with default config', async () => {
|
|
129
|
+
await registry.initialize({ backend: mockBackend });
|
|
130
|
+
expect(registry.isInitialized()).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
it('should not initialize twice', async () => {
|
|
133
|
+
await registry.initialize({ backend: mockBackend });
|
|
134
|
+
const count1 = registry.getActiveCount();
|
|
135
|
+
await registry.initialize({ backend: mockBackend });
|
|
136
|
+
expect(registry.getActiveCount()).toBe(count1);
|
|
137
|
+
});
|
|
138
|
+
it('should initialize with empty config', async () => {
|
|
139
|
+
await registry.initialize();
|
|
140
|
+
expect(registry.isInitialized()).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it('should emit initialized event', async () => {
|
|
143
|
+
const handler = vi.fn();
|
|
144
|
+
registry.on('initialized', handler);
|
|
145
|
+
await registry.initialize({ backend: mockBackend });
|
|
146
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
147
|
+
expect(handler).toHaveBeenCalledWith(expect.objectContaining({
|
|
148
|
+
initTimeMs: expect.any(Number),
|
|
149
|
+
activeControllers: expect.any(Number),
|
|
150
|
+
totalControllers: expect.any(Number),
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
it('should emit controller:initialized events', async () => {
|
|
154
|
+
const handler = vi.fn();
|
|
155
|
+
registry.on('controller:initialized', handler);
|
|
156
|
+
await registry.initialize({ backend: mockBackend });
|
|
157
|
+
// At minimum learningBridge and tieredCache should init
|
|
158
|
+
expect(handler.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
159
|
+
});
|
|
160
|
+
it('should track init time', async () => {
|
|
161
|
+
await registry.initialize({ backend: mockBackend });
|
|
162
|
+
const report = await registry.healthCheck();
|
|
163
|
+
expect(report.initTimeMs).toBeGreaterThan(0);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
// ----- Level-Based Ordering -----
|
|
167
|
+
describe('level-based initialization ordering', () => {
|
|
168
|
+
it('should define 7 initialization levels (0-6)', () => {
|
|
169
|
+
expect(INIT_LEVELS).toHaveLength(7);
|
|
170
|
+
expect(INIT_LEVELS[0].level).toBe(0);
|
|
171
|
+
expect(INIT_LEVELS[6].level).toBe(6);
|
|
172
|
+
});
|
|
173
|
+
it('should have monotonically increasing levels', () => {
|
|
174
|
+
for (let i = 1; i < INIT_LEVELS.length; i++) {
|
|
175
|
+
expect(INIT_LEVELS[i].level).toBeGreaterThan(INIT_LEVELS[i - 1].level);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
it('should include core controllers in level 1', () => {
|
|
179
|
+
const level1 = INIT_LEVELS.find((l) => l.level === 1);
|
|
180
|
+
expect(level1?.controllers).toContain('reasoningBank');
|
|
181
|
+
expect(level1?.controllers).toContain('learningBridge');
|
|
182
|
+
expect(level1?.controllers).toContain('tieredCache');
|
|
183
|
+
});
|
|
184
|
+
it('should include graph controllers in level 2', () => {
|
|
185
|
+
const level2 = INIT_LEVELS.find((l) => l.level === 2);
|
|
186
|
+
expect(level2?.controllers).toContain('memoryGraph');
|
|
187
|
+
expect(level2?.controllers).toContain('agentMemoryScope');
|
|
188
|
+
});
|
|
189
|
+
it('should include specialization controllers in level 3', () => {
|
|
190
|
+
const level3 = INIT_LEVELS.find((l) => l.level === 3);
|
|
191
|
+
expect(level3?.controllers).toContain('skills');
|
|
192
|
+
expect(level3?.controllers).toContain('explainableRecall');
|
|
193
|
+
expect(level3?.controllers).toContain('reflexion');
|
|
194
|
+
});
|
|
195
|
+
it('should include causal controllers in level 4', () => {
|
|
196
|
+
const level4 = INIT_LEVELS.find((l) => l.level === 4);
|
|
197
|
+
expect(level4?.controllers).toContain('causalGraph');
|
|
198
|
+
expect(level4?.controllers).toContain('nightlyLearner');
|
|
199
|
+
});
|
|
200
|
+
it('should include advanced services in level 5', () => {
|
|
201
|
+
const level5 = INIT_LEVELS.find((l) => l.level === 5);
|
|
202
|
+
expect(level5?.controllers).toContain('graphTransformer');
|
|
203
|
+
expect(level5?.controllers).toContain('sonaTrajectory');
|
|
204
|
+
});
|
|
205
|
+
it('should include session management in level 6', () => {
|
|
206
|
+
const level6 = INIT_LEVELS.find((l) => l.level === 6);
|
|
207
|
+
expect(level6?.controllers).toContain('federatedSession');
|
|
208
|
+
});
|
|
209
|
+
it('should not have duplicate controller names across levels', () => {
|
|
210
|
+
const allNames = [];
|
|
211
|
+
for (const level of INIT_LEVELS) {
|
|
212
|
+
for (const name of level.controllers) {
|
|
213
|
+
expect(allNames).not.toContain(name);
|
|
214
|
+
allNames.push(name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
// ----- Graceful Degradation -----
|
|
220
|
+
describe('graceful degradation', () => {
|
|
221
|
+
it('should continue when AgentDB is unavailable', async () => {
|
|
222
|
+
// No AgentDB module available — should still init CLI-layer controllers
|
|
223
|
+
await registry.initialize({ backend: mockBackend });
|
|
224
|
+
expect(registry.isInitialized()).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
it('should mark failed controllers as unavailable without crashing', async () => {
|
|
227
|
+
await registry.initialize({ backend: mockBackend });
|
|
228
|
+
const report = await registry.healthCheck();
|
|
229
|
+
// Some controllers should be unavailable (no AgentDB)
|
|
230
|
+
// but the registry itself should be functional
|
|
231
|
+
expect(report.status).not.toBe('unhealthy');
|
|
232
|
+
});
|
|
233
|
+
it('should emit controller:failed for failed controllers', async () => {
|
|
234
|
+
const handler = vi.fn();
|
|
235
|
+
registry.on('controller:failed', handler);
|
|
236
|
+
// Enable a controller that requires AgentDB (which is unavailable)
|
|
237
|
+
await registry.initialize({
|
|
238
|
+
backend: mockBackend,
|
|
239
|
+
controllers: { reasoningBank: true },
|
|
240
|
+
});
|
|
241
|
+
// ReasoningBank requires AgentDB, so it should fail or be unavailable
|
|
242
|
+
// The exact behavior depends on whether agentdb is importable
|
|
243
|
+
});
|
|
244
|
+
it('should handle null backend gracefully', async () => {
|
|
245
|
+
await registry.initialize({});
|
|
246
|
+
expect(registry.isInitialized()).toBe(true);
|
|
247
|
+
expect(registry.getBackend()).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
it('should isolate controller failures from each other', async () => {
|
|
250
|
+
// Initialize with backend - learningBridge and tieredCache should work
|
|
251
|
+
await registry.initialize({ backend: mockBackend });
|
|
252
|
+
// LearningBridge should be available (it only needs backend)
|
|
253
|
+
const bridge = registry.get('learningBridge');
|
|
254
|
+
expect(bridge).toBeInstanceOf(LearningBridge);
|
|
255
|
+
// TieredCache should be available
|
|
256
|
+
const cache = registry.get('tieredCache');
|
|
257
|
+
expect(cache).toBeInstanceOf(TieredCacheManager);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
// ----- Config-Driven Activation -----
|
|
261
|
+
describe('config-driven activation', () => {
|
|
262
|
+
it('should respect explicit controller enable/disable', async () => {
|
|
263
|
+
await registry.initialize({
|
|
264
|
+
backend: mockBackend,
|
|
265
|
+
controllers: {
|
|
266
|
+
learningBridge: false,
|
|
267
|
+
tieredCache: true,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
expect(registry.isEnabled('learningBridge')).toBe(false);
|
|
271
|
+
expect(registry.isEnabled('tieredCache')).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
it('should enable learningBridge by default when backend is available', async () => {
|
|
274
|
+
await registry.initialize({ backend: mockBackend });
|
|
275
|
+
expect(registry.isEnabled('learningBridge')).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
it('should enable tieredCache by default', async () => {
|
|
278
|
+
await registry.initialize({ backend: mockBackend });
|
|
279
|
+
expect(registry.isEnabled('tieredCache')).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
it('should pass SONA mode to LearningBridge', async () => {
|
|
282
|
+
await registry.initialize({
|
|
283
|
+
backend: mockBackend,
|
|
284
|
+
neural: { enabled: true, sonaMode: 'research' },
|
|
285
|
+
});
|
|
286
|
+
const bridge = registry.get('learningBridge');
|
|
287
|
+
expect(bridge).toBeInstanceOf(LearningBridge);
|
|
288
|
+
});
|
|
289
|
+
it('should pass memoryGraph config', async () => {
|
|
290
|
+
await registry.initialize({
|
|
291
|
+
backend: mockBackend,
|
|
292
|
+
memory: {
|
|
293
|
+
memoryGraph: { pageRankDamping: 0.9, maxNodes: 1000 },
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
const graph = registry.get('memoryGraph');
|
|
297
|
+
expect(graph).toBeInstanceOf(MemoryGraph);
|
|
298
|
+
});
|
|
299
|
+
it('should pass tieredCache config', async () => {
|
|
300
|
+
await registry.initialize({
|
|
301
|
+
backend: mockBackend,
|
|
302
|
+
memory: {
|
|
303
|
+
tieredCache: { maxSize: 5000, ttl: 60000 },
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
const cache = registry.get('tieredCache');
|
|
307
|
+
expect(cache).toBeInstanceOf(TieredCacheManager);
|
|
308
|
+
});
|
|
309
|
+
it('should not enable optional controllers by default', async () => {
|
|
310
|
+
await registry.initialize({ backend: mockBackend });
|
|
311
|
+
expect(registry.isEnabled('hybridSearch')).toBe(false);
|
|
312
|
+
expect(registry.isEnabled('federatedSession')).toBe(false);
|
|
313
|
+
expect(registry.isEnabled('semanticRouter')).toBe(false);
|
|
314
|
+
expect(registry.isEnabled('sonaTrajectory')).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
// ----- Controller Access -----
|
|
318
|
+
describe('controller access (get/isEnabled)', () => {
|
|
319
|
+
it('should return null for unregistered controllers', async () => {
|
|
320
|
+
await registry.initialize({ backend: mockBackend });
|
|
321
|
+
expect(registry.get('hybridSearch')).toBeNull();
|
|
322
|
+
});
|
|
323
|
+
it('should return typed controller instances', async () => {
|
|
324
|
+
await registry.initialize({ backend: mockBackend });
|
|
325
|
+
const bridge = registry.get('learningBridge');
|
|
326
|
+
if (bridge) {
|
|
327
|
+
expect(typeof bridge.consolidate).toBe('function');
|
|
328
|
+
expect(typeof bridge.getStats).toBe('function');
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
it('should return false for disabled controllers', async () => {
|
|
332
|
+
await registry.initialize({
|
|
333
|
+
backend: mockBackend,
|
|
334
|
+
controllers: { learningBridge: false },
|
|
335
|
+
});
|
|
336
|
+
expect(registry.isEnabled('learningBridge')).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
// ----- Health Check -----
|
|
340
|
+
describe('health check', () => {
|
|
341
|
+
it('should return healthy when controllers are active', async () => {
|
|
342
|
+
await registry.initialize({ backend: mockBackend });
|
|
343
|
+
const report = await registry.healthCheck();
|
|
344
|
+
expect(report.timestamp).toBeGreaterThan(0);
|
|
345
|
+
expect(report.initTimeMs).toBeGreaterThanOrEqual(0);
|
|
346
|
+
expect(report.controllers).toBeInstanceOf(Array);
|
|
347
|
+
});
|
|
348
|
+
it('should report active and total controller counts', async () => {
|
|
349
|
+
await registry.initialize({ backend: mockBackend });
|
|
350
|
+
const report = await registry.healthCheck();
|
|
351
|
+
expect(report.activeControllers).toBeGreaterThanOrEqual(0);
|
|
352
|
+
expect(report.totalControllers).toBeGreaterThanOrEqual(report.activeControllers);
|
|
353
|
+
});
|
|
354
|
+
it('should report agentdb availability', async () => {
|
|
355
|
+
await registry.initialize({ backend: mockBackend });
|
|
356
|
+
const report = await registry.healthCheck();
|
|
357
|
+
expect(typeof report.agentdbAvailable).toBe('boolean');
|
|
358
|
+
});
|
|
359
|
+
it('should classify status correctly', async () => {
|
|
360
|
+
await registry.initialize({ backend: mockBackend });
|
|
361
|
+
const report = await registry.healthCheck();
|
|
362
|
+
expect(['healthy', 'degraded', 'unhealthy']).toContain(report.status);
|
|
363
|
+
});
|
|
364
|
+
it('should include individual controller health', async () => {
|
|
365
|
+
await registry.initialize({ backend: mockBackend });
|
|
366
|
+
const report = await registry.healthCheck();
|
|
367
|
+
for (const controller of report.controllers) {
|
|
368
|
+
expect(controller).toHaveProperty('name');
|
|
369
|
+
expect(controller).toHaveProperty('status');
|
|
370
|
+
expect(controller).toHaveProperty('initTimeMs');
|
|
371
|
+
expect(['healthy', 'degraded', 'unavailable']).toContain(controller.status);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
// ----- Shutdown -----
|
|
376
|
+
describe('shutdown', () => {
|
|
377
|
+
it('should shutdown cleanly', async () => {
|
|
378
|
+
await registry.initialize({ backend: mockBackend });
|
|
379
|
+
await registry.shutdown();
|
|
380
|
+
expect(registry.isInitialized()).toBe(false);
|
|
381
|
+
});
|
|
382
|
+
it('should emit shutdown event', async () => {
|
|
383
|
+
const handler = vi.fn();
|
|
384
|
+
registry.on('shutdown', handler);
|
|
385
|
+
await registry.initialize({ backend: mockBackend });
|
|
386
|
+
await registry.shutdown();
|
|
387
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
388
|
+
});
|
|
389
|
+
it('should handle double shutdown', async () => {
|
|
390
|
+
await registry.initialize({ backend: mockBackend });
|
|
391
|
+
await registry.shutdown();
|
|
392
|
+
await registry.shutdown(); // Should be a no-op
|
|
393
|
+
expect(registry.isInitialized()).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
it('should handle shutdown without initialization', async () => {
|
|
396
|
+
await registry.shutdown(); // Should be a no-op
|
|
397
|
+
expect(registry.isInitialized()).toBe(false);
|
|
398
|
+
});
|
|
399
|
+
it('should clean up controllers', async () => {
|
|
400
|
+
await registry.initialize({ backend: mockBackend });
|
|
401
|
+
const countBefore = registry.getActiveCount();
|
|
402
|
+
await registry.shutdown();
|
|
403
|
+
expect(registry.getActiveCount()).toBe(0);
|
|
404
|
+
});
|
|
405
|
+
it('should allow re-initialization after shutdown', async () => {
|
|
406
|
+
await registry.initialize({ backend: mockBackend });
|
|
407
|
+
await registry.shutdown();
|
|
408
|
+
await registry.initialize({ backend: mockBackend });
|
|
409
|
+
expect(registry.isInitialized()).toBe(true);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
// ----- Controller Listing -----
|
|
413
|
+
describe('listControllers', () => {
|
|
414
|
+
it('should return list of all registered controllers', async () => {
|
|
415
|
+
await registry.initialize({ backend: mockBackend });
|
|
416
|
+
const list = registry.listControllers();
|
|
417
|
+
expect(list).toBeInstanceOf(Array);
|
|
418
|
+
for (const item of list) {
|
|
419
|
+
expect(item).toHaveProperty('name');
|
|
420
|
+
expect(item).toHaveProperty('enabled');
|
|
421
|
+
expect(item).toHaveProperty('level');
|
|
422
|
+
expect(typeof item.name).toBe('string');
|
|
423
|
+
expect(typeof item.enabled).toBe('boolean');
|
|
424
|
+
expect(typeof item.level).toBe('number');
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
// ----- AgentDB Integration -----
|
|
429
|
+
describe('AgentDB integration', () => {
|
|
430
|
+
it('should handle missing agentdb module', async () => {
|
|
431
|
+
// With no agentdb installed, should still work
|
|
432
|
+
await registry.initialize({ backend: mockBackend });
|
|
433
|
+
expect(registry.isInitialized()).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
it('should return null AgentDB when unavailable', async () => {
|
|
436
|
+
await registry.initialize({ backend: mockBackend });
|
|
437
|
+
// May or may not be available depending on test environment
|
|
438
|
+
const agentdb = registry.getAgentDB();
|
|
439
|
+
// Just ensure it doesn't throw
|
|
440
|
+
expect(agentdb === null || agentdb !== null).toBe(true);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
// ----- Cross-Platform Path Handling -----
|
|
444
|
+
describe('cross-platform compatibility', () => {
|
|
445
|
+
it('should handle forward slash paths', async () => {
|
|
446
|
+
await registry.initialize({
|
|
447
|
+
backend: mockBackend,
|
|
448
|
+
dbPath: '/tmp/test/memory.db',
|
|
449
|
+
});
|
|
450
|
+
expect(registry.isInitialized()).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
it('should handle relative paths', async () => {
|
|
453
|
+
await registry.initialize({
|
|
454
|
+
backend: mockBackend,
|
|
455
|
+
dbPath: './data/memory.db',
|
|
456
|
+
});
|
|
457
|
+
expect(registry.isInitialized()).toBe(true);
|
|
458
|
+
});
|
|
459
|
+
it('should handle :memory: path', async () => {
|
|
460
|
+
await registry.initialize({
|
|
461
|
+
backend: mockBackend,
|
|
462
|
+
dbPath: ':memory:',
|
|
463
|
+
});
|
|
464
|
+
expect(registry.isInitialized()).toBe(true);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
// ----- LearningBridge Integration -----
|
|
468
|
+
describe('LearningBridge via registry', () => {
|
|
469
|
+
it('should create LearningBridge with backend', async () => {
|
|
470
|
+
await registry.initialize({ backend: mockBackend });
|
|
471
|
+
const bridge = registry.get('learningBridge');
|
|
472
|
+
expect(bridge).toBeInstanceOf(LearningBridge);
|
|
473
|
+
});
|
|
474
|
+
it('should pass config to LearningBridge', async () => {
|
|
475
|
+
await registry.initialize({
|
|
476
|
+
backend: mockBackend,
|
|
477
|
+
memory: {
|
|
478
|
+
learningBridge: {
|
|
479
|
+
sonaMode: 'edge',
|
|
480
|
+
confidenceDecayRate: 0.01,
|
|
481
|
+
accessBoostAmount: 0.05,
|
|
482
|
+
consolidationThreshold: 5,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
const bridge = registry.get('learningBridge');
|
|
487
|
+
expect(bridge).toBeInstanceOf(LearningBridge);
|
|
488
|
+
const stats = bridge.getStats();
|
|
489
|
+
expect(stats.totalTrajectories).toBe(0);
|
|
490
|
+
expect(stats.neuralAvailable).toBe(false); // No neural module in tests
|
|
491
|
+
});
|
|
492
|
+
it('should not create LearningBridge without backend', async () => {
|
|
493
|
+
await registry.initialize({});
|
|
494
|
+
const bridge = registry.get('learningBridge');
|
|
495
|
+
// Without backend, LearningBridge returns null
|
|
496
|
+
expect(bridge).toBeNull();
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
// ----- MemoryGraph Integration -----
|
|
500
|
+
describe('MemoryGraph via registry', () => {
|
|
501
|
+
it('should create MemoryGraph when configured', async () => {
|
|
502
|
+
await registry.initialize({
|
|
503
|
+
backend: mockBackend,
|
|
504
|
+
memory: {
|
|
505
|
+
memoryGraph: { pageRankDamping: 0.85, maxNodes: 5000 },
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
const graph = registry.get('memoryGraph');
|
|
509
|
+
expect(graph).toBeInstanceOf(MemoryGraph);
|
|
510
|
+
});
|
|
511
|
+
it('should report graph stats', async () => {
|
|
512
|
+
await registry.initialize({
|
|
513
|
+
backend: mockBackend,
|
|
514
|
+
memory: {
|
|
515
|
+
memoryGraph: {},
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
const graph = registry.get('memoryGraph');
|
|
519
|
+
if (graph) {
|
|
520
|
+
const stats = graph.getStats();
|
|
521
|
+
expect(stats.nodeCount).toBeGreaterThanOrEqual(0);
|
|
522
|
+
expect(stats.edgeCount).toBeGreaterThanOrEqual(0);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
// ----- TieredCache Integration -----
|
|
527
|
+
describe('TieredCacheManager via registry', () => {
|
|
528
|
+
it('should create TieredCacheManager', async () => {
|
|
529
|
+
await registry.initialize({ backend: mockBackend });
|
|
530
|
+
const cache = registry.get('tieredCache');
|
|
531
|
+
expect(cache).toBeInstanceOf(TieredCacheManager);
|
|
532
|
+
});
|
|
533
|
+
it('should respect cache config', async () => {
|
|
534
|
+
await registry.initialize({
|
|
535
|
+
backend: mockBackend,
|
|
536
|
+
memory: {
|
|
537
|
+
tieredCache: { maxSize: 500, ttl: 10000 },
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
const cache = registry.get('tieredCache');
|
|
541
|
+
expect(cache).toBeInstanceOf(TieredCacheManager);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
// ----- Event Emission -----
|
|
545
|
+
describe('events', () => {
|
|
546
|
+
it('should emit agentdb:unavailable when module missing', async () => {
|
|
547
|
+
const handler = vi.fn();
|
|
548
|
+
registry.on('agentdb:unavailable', handler);
|
|
549
|
+
await registry.initialize({ backend: mockBackend });
|
|
550
|
+
// AgentDB may or may not be available in test environment
|
|
551
|
+
// Just verify the listener doesn't break anything
|
|
552
|
+
});
|
|
553
|
+
it('should emit all lifecycle events', async () => {
|
|
554
|
+
const events = [];
|
|
555
|
+
registry.on('initialized', () => events.push('initialized'));
|
|
556
|
+
registry.on('shutdown', () => events.push('shutdown'));
|
|
557
|
+
await registry.initialize({ backend: mockBackend });
|
|
558
|
+
expect(events).toContain('initialized');
|
|
559
|
+
await registry.shutdown();
|
|
560
|
+
expect(events).toContain('shutdown');
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
// ----- Performance -----
|
|
564
|
+
describe('performance', () => {
|
|
565
|
+
it('should initialize within 500ms', async () => {
|
|
566
|
+
const start = performance.now();
|
|
567
|
+
await registry.initialize({ backend: mockBackend });
|
|
568
|
+
const duration = performance.now() - start;
|
|
569
|
+
// Per ADR-053: "No regression beyond 10% in CLI startup time"
|
|
570
|
+
expect(duration).toBeLessThan(500);
|
|
571
|
+
});
|
|
572
|
+
it('should shutdown within 100ms', async () => {
|
|
573
|
+
await registry.initialize({ backend: mockBackend });
|
|
574
|
+
const start = performance.now();
|
|
575
|
+
await registry.shutdown();
|
|
576
|
+
const duration = performance.now() - start;
|
|
577
|
+
expect(duration).toBeLessThan(100);
|
|
578
|
+
});
|
|
579
|
+
it('should have low overhead for controller access', async () => {
|
|
580
|
+
await registry.initialize({ backend: mockBackend });
|
|
581
|
+
const start = performance.now();
|
|
582
|
+
for (let i = 0; i < 1000; i++) {
|
|
583
|
+
registry.get('learningBridge');
|
|
584
|
+
registry.get('tieredCache');
|
|
585
|
+
registry.isEnabled('reasoningBank');
|
|
586
|
+
}
|
|
587
|
+
const duration = performance.now() - start;
|
|
588
|
+
// 3000 lookups should complete in under 10ms
|
|
589
|
+
expect(duration).toBeLessThan(10);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
// ===== HybridBackend Proxy Methods Tests =====
|
|
594
|
+
describe('HybridBackend proxy methods', () => {
|
|
595
|
+
it('should export recordFeedback method', async () => {
|
|
596
|
+
const { HybridBackend } = await import('./hybrid-backend.js');
|
|
597
|
+
const backend = new HybridBackend();
|
|
598
|
+
expect(typeof backend.recordFeedback).toBe('function');
|
|
599
|
+
});
|
|
600
|
+
it('should export verifyWitnessChain method', async () => {
|
|
601
|
+
const { HybridBackend } = await import('./hybrid-backend.js');
|
|
602
|
+
const backend = new HybridBackend();
|
|
603
|
+
expect(typeof backend.verifyWitnessChain).toBe('function');
|
|
604
|
+
});
|
|
605
|
+
it('should export getWitnessChain method', async () => {
|
|
606
|
+
const { HybridBackend } = await import('./hybrid-backend.js');
|
|
607
|
+
const backend = new HybridBackend();
|
|
608
|
+
expect(typeof backend.getWitnessChain).toBe('function');
|
|
609
|
+
});
|
|
610
|
+
it('should return false for recordFeedback when AgentDB unavailable', async () => {
|
|
611
|
+
const { HybridBackend } = await import('./hybrid-backend.js');
|
|
612
|
+
const backend = new HybridBackend();
|
|
613
|
+
await backend.initialize();
|
|
614
|
+
const result = await backend.recordFeedback('entry-1', { score: 0.9 });
|
|
615
|
+
expect(result).toBe(false);
|
|
616
|
+
await backend.shutdown();
|
|
617
|
+
});
|
|
618
|
+
it('should return invalid for verifyWitnessChain when AgentDB unavailable', async () => {
|
|
619
|
+
const { HybridBackend } = await import('./hybrid-backend.js');
|
|
620
|
+
const backend = new HybridBackend();
|
|
621
|
+
await backend.initialize();
|
|
622
|
+
const result = await backend.verifyWitnessChain('entry-1');
|
|
623
|
+
expect(result.valid).toBe(false);
|
|
624
|
+
expect(result.errors).toContain('AgentDB not available');
|
|
625
|
+
await backend.shutdown();
|
|
626
|
+
});
|
|
627
|
+
it('should return empty array for getWitnessChain when AgentDB unavailable', async () => {
|
|
628
|
+
const { HybridBackend } = await import('./hybrid-backend.js');
|
|
629
|
+
const backend = new HybridBackend();
|
|
630
|
+
await backend.initialize();
|
|
631
|
+
const result = await backend.getWitnessChain('entry-1');
|
|
632
|
+
expect(result).toEqual([]);
|
|
633
|
+
await backend.shutdown();
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
//# sourceMappingURL=controller-registry.test.js.map
|