578 lines
26 KiB
JavaScript
578 lines
26 KiB
JavaScript
/**
|
|
* Tests for LearningBridge
|
|
*
|
|
* TDD London School (mock-first) tests for the bridge that connects
|
|
* AutoMemoryBridge insights to the NeuralLearningSystem.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { LearningBridge } from './learning-bridge.js';
|
|
// ===== Mock Neural System =====
|
|
function createMockNeuralSystem() {
|
|
return {
|
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
beginTask: vi.fn().mockReturnValue('traj-1'),
|
|
recordStep: vi.fn(),
|
|
completeTask: vi.fn().mockResolvedValue(undefined),
|
|
findPatterns: vi.fn().mockResolvedValue([]),
|
|
cleanup: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
}
|
|
function createNeuralLoader(neural) {
|
|
return async () => neural;
|
|
}
|
|
function createFailingNeuralLoader() {
|
|
return async () => { throw new Error('Module not found'); };
|
|
}
|
|
// ===== Mock Backend =====
|
|
function createMockBackend() {
|
|
const storedEntries = [];
|
|
return {
|
|
storedEntries,
|
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
store: vi.fn().mockImplementation(async (entry) => {
|
|
storedEntries.push(entry);
|
|
}),
|
|
get: vi.fn().mockResolvedValue(null),
|
|
getByKey: vi.fn().mockResolvedValue(null),
|
|
update: vi.fn().mockImplementation(async (id, upd) => {
|
|
const entry = storedEntries.find(e => e.id === id);
|
|
if (!entry)
|
|
return null;
|
|
if (upd.metadata)
|
|
entry.metadata = { ...entry.metadata, ...upd.metadata };
|
|
return entry;
|
|
}),
|
|
delete: vi.fn().mockResolvedValue(true),
|
|
query: vi.fn().mockResolvedValue([]),
|
|
search: vi.fn().mockResolvedValue([]),
|
|
bulkInsert: vi.fn().mockResolvedValue(undefined),
|
|
bulkDelete: vi.fn().mockResolvedValue(0),
|
|
count: vi.fn().mockResolvedValue(0),
|
|
listNamespaces: vi.fn().mockResolvedValue([]),
|
|
clearNamespace: vi.fn().mockResolvedValue(0),
|
|
getStats: vi.fn().mockResolvedValue({
|
|
totalEntries: 0, entriesByNamespace: {}, entriesByType: {},
|
|
memoryUsage: 0, avgQueryTime: 0, avgSearchTime: 0,
|
|
}),
|
|
healthCheck: vi.fn().mockResolvedValue({
|
|
status: 'healthy',
|
|
components: {
|
|
storage: { status: 'healthy', latency: 0 },
|
|
index: { status: 'healthy', latency: 0 },
|
|
cache: { status: 'healthy', latency: 0 },
|
|
},
|
|
timestamp: Date.now(), issues: [], recommendations: [],
|
|
}),
|
|
};
|
|
}
|
|
// ===== Test Fixtures =====
|
|
function createTestInsight(overrides = {}) {
|
|
return {
|
|
category: 'debugging',
|
|
summary: 'HNSW index requires initialization before search',
|
|
source: 'agent:tester',
|
|
confidence: 0.95,
|
|
...overrides,
|
|
};
|
|
}
|
|
function createTestEntry(overrides = {}) {
|
|
const now = Date.now();
|
|
return {
|
|
id: 'entry-1',
|
|
key: 'insight:debugging:12345:0',
|
|
content: 'HNSW index requires initialization',
|
|
type: 'semantic',
|
|
namespace: 'learnings',
|
|
tags: ['insight', 'debugging'],
|
|
metadata: { confidence: 0.8, category: 'debugging' },
|
|
accessLevel: 'private',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
version: 1,
|
|
references: [],
|
|
accessCount: 0,
|
|
lastAccessedAt: now,
|
|
...overrides,
|
|
};
|
|
}
|
|
// ===== Tests =====
|
|
describe('LearningBridge', () => {
|
|
let bridge;
|
|
let backend;
|
|
let neural;
|
|
beforeEach(() => {
|
|
backend = createMockBackend();
|
|
neural = createMockNeuralSystem();
|
|
bridge = new LearningBridge(backend, { neuralLoader: createNeuralLoader(neural) });
|
|
});
|
|
afterEach(() => {
|
|
bridge.destroy();
|
|
});
|
|
// ===== constructor =====
|
|
describe('constructor', () => {
|
|
it('should create with default config', () => {
|
|
const b = new LearningBridge(backend);
|
|
const stats = b.getStats();
|
|
expect(stats.totalTrajectories).toBe(0);
|
|
expect(stats.neuralAvailable).toBe(false);
|
|
b.destroy();
|
|
});
|
|
it('should create with custom config', () => {
|
|
const custom = new LearningBridge(backend, {
|
|
sonaMode: 'research',
|
|
confidenceDecayRate: 0.01,
|
|
accessBoostAmount: 0.05,
|
|
maxConfidence: 0.9,
|
|
minConfidence: 0.2,
|
|
ewcLambda: 5000,
|
|
consolidationThreshold: 20,
|
|
});
|
|
expect(custom.getStats().totalTrajectories).toBe(0);
|
|
custom.destroy();
|
|
});
|
|
it('should respect enabled=false', async () => {
|
|
const disabled = new LearningBridge(backend, {
|
|
enabled: false,
|
|
neuralLoader: createNeuralLoader(neural),
|
|
});
|
|
await disabled.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
expect(neural.beginTask).not.toHaveBeenCalled();
|
|
disabled.destroy();
|
|
});
|
|
});
|
|
// ===== onInsightRecorded =====
|
|
describe('onInsightRecorded', () => {
|
|
it('should store trajectory when neural available', async () => {
|
|
const insight = createTestInsight();
|
|
await bridge.onInsightRecorded(insight, 'entry-1');
|
|
expect(neural.beginTask).toHaveBeenCalledWith(insight.summary, 'general');
|
|
expect(neural.recordStep).toHaveBeenCalledWith('traj-1', expect.objectContaining({
|
|
action: 'record:debugging',
|
|
reward: 0.95,
|
|
}));
|
|
expect(bridge.getStats().totalTrajectories).toBe(1);
|
|
});
|
|
it('should emit insight:learning-started event', async () => {
|
|
const handler = vi.fn();
|
|
bridge.on('insight:learning-started', handler);
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
expect(handler).toHaveBeenCalledWith({ entryId: 'entry-1', category: 'debugging' });
|
|
});
|
|
it('should no-op when disabled', async () => {
|
|
const disabled = new LearningBridge(backend, {
|
|
enabled: false,
|
|
neuralLoader: createNeuralLoader(neural),
|
|
});
|
|
await disabled.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
expect(neural.beginTask).not.toHaveBeenCalled();
|
|
disabled.destroy();
|
|
});
|
|
it('should no-op when destroyed', async () => {
|
|
bridge.destroy();
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
expect(neural.beginTask).not.toHaveBeenCalled();
|
|
});
|
|
it('should handle neural unavailable gracefully', async () => {
|
|
const safeBridge = new LearningBridge(backend, {
|
|
neuralLoader: createFailingNeuralLoader(),
|
|
});
|
|
const handler = vi.fn();
|
|
safeBridge.on('insight:learning-started', handler);
|
|
await safeBridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
expect(handler).toHaveBeenCalled();
|
|
expect(safeBridge.getStats().neuralAvailable).toBe(false);
|
|
safeBridge.destroy();
|
|
});
|
|
it('should pass hash embedding as stateEmbedding', async () => {
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
const stepArg = neural.recordStep.mock.calls[0][1];
|
|
expect(stepArg.stateEmbedding).toBeInstanceOf(Float32Array);
|
|
expect(stepArg.stateEmbedding.length).toBe(768);
|
|
});
|
|
it('should create unique trajectory per entry', async () => {
|
|
neural.beginTask.mockReturnValueOnce('traj-1').mockReturnValueOnce('traj-2');
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
await bridge.onInsightRecorded(createTestInsight({ summary: 'Second insight' }), 'entry-2');
|
|
expect(bridge.getStats().totalTrajectories).toBe(2);
|
|
expect(bridge.getStats().activeTrajectories).toBe(2);
|
|
});
|
|
it('should survive beginTask throwing', async () => {
|
|
neural.beginTask.mockImplementationOnce(() => { throw new Error('fail'); });
|
|
const handler = vi.fn();
|
|
bridge.on('insight:learning-started', handler);
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
expect(handler).toHaveBeenCalled();
|
|
});
|
|
});
|
|
// ===== onInsightAccessed =====
|
|
describe('onInsightAccessed', () => {
|
|
it('should boost confidence by accessBoostAmount', async () => {
|
|
const entry = createTestEntry({ metadata: { confidence: 0.5 } });
|
|
backend.get.mockResolvedValueOnce(entry);
|
|
await bridge.onInsightAccessed('entry-1');
|
|
expect(backend.update).toHaveBeenCalledWith('entry-1', {
|
|
metadata: expect.objectContaining({ confidence: 0.53 }),
|
|
});
|
|
});
|
|
it('should cap confidence at maxConfidence', async () => {
|
|
const entry = createTestEntry({ metadata: { confidence: 0.99 } });
|
|
backend.get.mockResolvedValueOnce(entry);
|
|
await bridge.onInsightAccessed('entry-1');
|
|
const updateCall = backend.update.mock.calls[0];
|
|
expect(updateCall[1].metadata.confidence).toBeLessThanOrEqual(1.0);
|
|
});
|
|
it('should handle missing entry gracefully', async () => {
|
|
backend.get.mockResolvedValueOnce(null);
|
|
await bridge.onInsightAccessed('nonexistent');
|
|
expect(backend.update).not.toHaveBeenCalled();
|
|
});
|
|
it('should emit insight:accessed event', async () => {
|
|
const entry = createTestEntry({ metadata: { confidence: 0.7 } });
|
|
backend.get.mockResolvedValueOnce(entry);
|
|
const handler = vi.fn();
|
|
bridge.on('insight:accessed', handler);
|
|
await bridge.onInsightAccessed('entry-1');
|
|
expect(handler).toHaveBeenCalledWith({
|
|
entryId: 'entry-1',
|
|
newConfidence: expect.any(Number),
|
|
});
|
|
});
|
|
it('should update entry metadata in backend preserving existing fields', async () => {
|
|
const entry = createTestEntry({
|
|
metadata: { confidence: 0.6, category: 'debugging', extra: 'preserved' },
|
|
});
|
|
backend.get.mockResolvedValueOnce(entry);
|
|
await bridge.onInsightAccessed('entry-1');
|
|
const updateCall = backend.update.mock.calls[0][1];
|
|
expect(updateCall.metadata.category).toBe('debugging');
|
|
expect(updateCall.metadata.extra).toBe('preserved');
|
|
expect(updateCall.metadata.confidence).toBeCloseTo(0.63, 5);
|
|
});
|
|
it('should record neural step when trajectory exists', async () => {
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
vi.clearAllMocks();
|
|
const entry = createTestEntry();
|
|
backend.get.mockResolvedValueOnce(entry);
|
|
await bridge.onInsightAccessed('entry-1');
|
|
expect(neural.recordStep).toHaveBeenCalledWith('traj-1', {
|
|
action: 'access',
|
|
reward: 0.03,
|
|
});
|
|
});
|
|
it('should not record neural step without trajectory', async () => {
|
|
const entry = createTestEntry();
|
|
backend.get.mockResolvedValueOnce(entry);
|
|
await bridge.onInsightAccessed('entry-1');
|
|
expect(neural.recordStep).not.toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ action: 'access' }));
|
|
});
|
|
it('should use default 0.5 when metadata lacks confidence', async () => {
|
|
const entry = createTestEntry({ metadata: {} });
|
|
backend.get.mockResolvedValueOnce(entry);
|
|
await bridge.onInsightAccessed('entry-1');
|
|
const updateCall = backend.update.mock.calls[0][1];
|
|
expect(updateCall.metadata.confidence).toBeCloseTo(0.53, 5);
|
|
});
|
|
it('should no-op when disabled', async () => {
|
|
const disabled = new LearningBridge(backend, { enabled: false });
|
|
await disabled.onInsightAccessed('entry-1');
|
|
expect(backend.get).not.toHaveBeenCalled();
|
|
disabled.destroy();
|
|
});
|
|
it('should track boost stats', async () => {
|
|
const entry = createTestEntry({ metadata: { confidence: 0.5 } });
|
|
backend.get.mockResolvedValue(entry);
|
|
await bridge.onInsightAccessed('entry-1');
|
|
await bridge.onInsightAccessed('entry-1');
|
|
expect(bridge.getStats().avgConfidenceBoost).toBeCloseTo(0.03, 5);
|
|
});
|
|
});
|
|
// ===== consolidate =====
|
|
describe('consolidate', () => {
|
|
async function seedTrajectories(count) {
|
|
for (let i = 0; i < count; i++) {
|
|
neural.beginTask.mockReturnValueOnce(`traj-${i}`);
|
|
await bridge.onInsightRecorded(createTestInsight({ summary: `Insight ${i}` }), `entry-${i}`);
|
|
}
|
|
}
|
|
it('should complete active trajectories', async () => {
|
|
await seedTrajectories(10);
|
|
const result = await bridge.consolidate();
|
|
expect(result.trajectoriesCompleted).toBe(10);
|
|
expect(result.patternsLearned).toBe(10);
|
|
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
});
|
|
it('should return early when below threshold', async () => {
|
|
await seedTrajectories(2);
|
|
const result = await bridge.consolidate();
|
|
expect(result.trajectoriesCompleted).toBe(0);
|
|
expect(neural.completeTask).not.toHaveBeenCalled();
|
|
});
|
|
it('should return early when neural unavailable', async () => {
|
|
const safeBridge = new LearningBridge(backend, {
|
|
neuralLoader: createFailingNeuralLoader(),
|
|
});
|
|
const result = await safeBridge.consolidate();
|
|
expect(result.trajectoriesCompleted).toBe(0);
|
|
safeBridge.destroy();
|
|
});
|
|
it('should clear completed trajectories', async () => {
|
|
await seedTrajectories(10);
|
|
expect(bridge.getStats().activeTrajectories).toBe(10);
|
|
await bridge.consolidate();
|
|
expect(bridge.getStats().activeTrajectories).toBe(0);
|
|
});
|
|
it('should emit consolidation:completed event', async () => {
|
|
await seedTrajectories(10);
|
|
const handler = vi.fn();
|
|
bridge.on('consolidation:completed', handler);
|
|
await bridge.consolidate();
|
|
expect(handler).toHaveBeenCalledWith(expect.objectContaining({
|
|
trajectoriesCompleted: 10,
|
|
patternsLearned: 10,
|
|
}));
|
|
});
|
|
it('should track stats correctly', async () => {
|
|
await seedTrajectories(10);
|
|
await bridge.consolidate();
|
|
const stats = bridge.getStats();
|
|
expect(stats.completedTrajectories).toBe(10);
|
|
expect(stats.totalConsolidations).toBe(1);
|
|
});
|
|
it('should handle completeTask failure for individual trajectories', async () => {
|
|
await seedTrajectories(10);
|
|
let callCount = 0;
|
|
neural.completeTask.mockImplementation(async () => {
|
|
callCount++;
|
|
if (callCount === 3)
|
|
throw new Error('Neural failure');
|
|
});
|
|
const result = await bridge.consolidate();
|
|
expect(result.trajectoriesCompleted).toBe(9);
|
|
});
|
|
it('should respect custom consolidationThreshold', async () => {
|
|
const customBridge = new LearningBridge(backend, {
|
|
consolidationThreshold: 2,
|
|
neuralLoader: createNeuralLoader(neural),
|
|
});
|
|
neural.beginTask.mockReturnValueOnce('traj-1').mockReturnValueOnce('traj-2');
|
|
await customBridge.onInsightRecorded(createTestInsight(), 'e-1');
|
|
await customBridge.onInsightRecorded(createTestInsight({ summary: 'S2' }), 'e-2');
|
|
const result = await customBridge.consolidate();
|
|
expect(result.trajectoriesCompleted).toBe(2);
|
|
customBridge.destroy();
|
|
});
|
|
});
|
|
// ===== decayConfidences =====
|
|
describe('decayConfidences', () => {
|
|
it('should decay entries older than 1 hour', async () => {
|
|
const twoHoursAgo = Date.now() - 2 * 3_600_000;
|
|
const entry = createTestEntry({
|
|
id: 'old-entry', updatedAt: twoHoursAgo, metadata: { confidence: 0.9 },
|
|
});
|
|
backend.query.mockResolvedValueOnce([entry]);
|
|
const count = await bridge.decayConfidences('learnings');
|
|
expect(count).toBe(1);
|
|
const newConf = backend.update.mock.calls[0][1].metadata.confidence;
|
|
expect(newConf).toBeCloseTo(0.89, 2);
|
|
});
|
|
it('should respect minConfidence floor', async () => {
|
|
const longAgo = Date.now() - 200 * 3_600_000;
|
|
const entry = createTestEntry({
|
|
id: 'ancient', updatedAt: longAgo, metadata: { confidence: 0.5 },
|
|
});
|
|
backend.query.mockResolvedValueOnce([entry]);
|
|
await bridge.decayConfidences('learnings');
|
|
const newConf = backend.update.mock.calls[0][1].metadata.confidence;
|
|
expect(newConf).toBeGreaterThanOrEqual(0.1);
|
|
});
|
|
it('should skip recent entries', async () => {
|
|
const entry = createTestEntry({
|
|
id: 'recent', updatedAt: Date.now() - 30 * 60_000, metadata: { confidence: 0.8 },
|
|
});
|
|
backend.query.mockResolvedValueOnce([entry]);
|
|
const count = await bridge.decayConfidences('learnings');
|
|
expect(count).toBe(0);
|
|
expect(backend.update).not.toHaveBeenCalled();
|
|
});
|
|
it('should return count of decayed entries', async () => {
|
|
const old = Date.now() - 2 * 3_600_000;
|
|
const entries = [
|
|
createTestEntry({ id: 'e1', updatedAt: old, metadata: { confidence: 0.9 } }),
|
|
createTestEntry({ id: 'e2', updatedAt: old, metadata: { confidence: 0.7 } }),
|
|
createTestEntry({ id: 'e3', updatedAt: Date.now(), metadata: { confidence: 0.5 } }),
|
|
];
|
|
backend.query.mockResolvedValueOnce(entries);
|
|
const count = await bridge.decayConfidences('learnings');
|
|
expect(count).toBe(2);
|
|
});
|
|
it('should handle empty namespace', async () => {
|
|
backend.query.mockResolvedValueOnce([]);
|
|
const count = await bridge.decayConfidences('empty-ns');
|
|
expect(count).toBe(0);
|
|
});
|
|
it('should handle query failure gracefully', async () => {
|
|
backend.query.mockRejectedValueOnce(new Error('DB error'));
|
|
const count = await bridge.decayConfidences('broken');
|
|
expect(count).toBe(0);
|
|
});
|
|
it('should track total decays in stats', async () => {
|
|
const old = Date.now() - 5 * 3_600_000;
|
|
backend.query.mockResolvedValueOnce([
|
|
createTestEntry({ id: 'e1', updatedAt: old, metadata: { confidence: 0.9 } }),
|
|
]);
|
|
await bridge.decayConfidences('learnings');
|
|
expect(bridge.getStats().totalDecays).toBe(1);
|
|
});
|
|
});
|
|
// ===== findSimilarPatterns =====
|
|
describe('findSimilarPatterns', () => {
|
|
it('should return patterns when neural available', async () => {
|
|
neural.findPatterns.mockResolvedValueOnce([
|
|
{ content: 'Pattern A', similarity: 0.9, category: 'debugging', confidence: 0.8 },
|
|
{ content: 'Pattern B', similarity: 0.7, category: 'performance', confidence: 0.6 },
|
|
]);
|
|
const patterns = await bridge.findSimilarPatterns('test query');
|
|
expect(patterns).toHaveLength(2);
|
|
expect(patterns[0].content).toBe('Pattern A');
|
|
expect(patterns[0].similarity).toBe(0.9);
|
|
expect(patterns[1].category).toBe('performance');
|
|
});
|
|
it('should return empty when neural unavailable', async () => {
|
|
const safeBridge = new LearningBridge(backend, {
|
|
neuralLoader: createFailingNeuralLoader(),
|
|
});
|
|
const patterns = await safeBridge.findSimilarPatterns('test');
|
|
expect(patterns).toHaveLength(0);
|
|
safeBridge.destroy();
|
|
});
|
|
it('should map results to PatternMatch format', async () => {
|
|
neural.findPatterns.mockResolvedValueOnce([
|
|
{ data: 'Raw data', score: 0.85, reward: 0.7 },
|
|
]);
|
|
const patterns = await bridge.findSimilarPatterns('test');
|
|
expect(patterns).toHaveLength(1);
|
|
expect(patterns[0].content).toBe('Raw data');
|
|
expect(patterns[0].similarity).toBe(0.85);
|
|
expect(patterns[0].confidence).toBe(0.7);
|
|
expect(patterns[0].category).toBe('unknown');
|
|
});
|
|
it('should pass k parameter to neural', async () => {
|
|
await bridge.findSimilarPatterns('test', 3);
|
|
expect(neural.findPatterns).toHaveBeenCalledWith(expect.any(Float32Array), 3);
|
|
});
|
|
it('should handle findPatterns throwing', async () => {
|
|
neural.findPatterns.mockRejectedValueOnce(new Error('Neural error'));
|
|
const patterns = await bridge.findSimilarPatterns('test');
|
|
expect(patterns).toHaveLength(0);
|
|
});
|
|
it('should handle non-array result from findPatterns', async () => {
|
|
neural.findPatterns.mockResolvedValueOnce(null);
|
|
const patterns = await bridge.findSimilarPatterns('test');
|
|
expect(patterns).toHaveLength(0);
|
|
});
|
|
});
|
|
// ===== getStats =====
|
|
describe('getStats', () => {
|
|
it('should return correct initial stats', () => {
|
|
const stats = bridge.getStats();
|
|
expect(stats.totalTrajectories).toBe(0);
|
|
expect(stats.completedTrajectories).toBe(0);
|
|
expect(stats.activeTrajectories).toBe(0);
|
|
expect(stats.totalConsolidations).toBe(0);
|
|
expect(stats.totalDecays).toBe(0);
|
|
expect(stats.avgConfidenceBoost).toBe(0);
|
|
});
|
|
it('should reflect operations in stats', async () => {
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
let stats = bridge.getStats();
|
|
expect(stats.totalTrajectories).toBe(1);
|
|
expect(stats.activeTrajectories).toBe(1);
|
|
expect(stats.neuralAvailable).toBe(true);
|
|
const entry = createTestEntry({ metadata: { confidence: 0.5 } });
|
|
backend.get.mockResolvedValueOnce(entry);
|
|
await bridge.onInsightAccessed('entry-1');
|
|
stats = bridge.getStats();
|
|
expect(stats.avgConfidenceBoost).toBeCloseTo(0.03, 5);
|
|
});
|
|
it('should show neuralAvailable=false before init', () => {
|
|
const fresh = new LearningBridge(backend);
|
|
expect(fresh.getStats().neuralAvailable).toBe(false);
|
|
fresh.destroy();
|
|
});
|
|
});
|
|
// ===== destroy =====
|
|
describe('destroy', () => {
|
|
it('should set destroyed state', async () => {
|
|
bridge.destroy();
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
expect(neural.beginTask).not.toHaveBeenCalled();
|
|
});
|
|
it('should clear trajectories', async () => {
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
expect(bridge.getStats().activeTrajectories).toBe(1);
|
|
bridge.destroy();
|
|
expect(bridge.getStats().activeTrajectories).toBe(0);
|
|
});
|
|
it('should make subsequent onInsightRecorded no-op', async () => {
|
|
bridge.destroy();
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
expect(bridge.getStats().totalTrajectories).toBe(0);
|
|
});
|
|
it('should make subsequent onInsightAccessed no-op', async () => {
|
|
bridge.destroy();
|
|
await bridge.onInsightAccessed('entry-1');
|
|
expect(backend.get).not.toHaveBeenCalled();
|
|
});
|
|
it('should make subsequent consolidate no-op', async () => {
|
|
bridge.destroy();
|
|
const result = await bridge.consolidate();
|
|
expect(result.trajectoriesCompleted).toBe(0);
|
|
});
|
|
it('should make subsequent decayConfidences no-op', async () => {
|
|
bridge.destroy();
|
|
const count = await bridge.decayConfidences('learnings');
|
|
expect(count).toBe(0);
|
|
});
|
|
it('should make subsequent findSimilarPatterns no-op', async () => {
|
|
bridge.destroy();
|
|
const patterns = await bridge.findSimilarPatterns('test');
|
|
expect(patterns).toHaveLength(0);
|
|
});
|
|
it('should call neural cleanup if available', async () => {
|
|
// Trigger neural init
|
|
await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
vi.clearAllMocks();
|
|
bridge.destroy();
|
|
expect(neural.cleanup).toHaveBeenCalled();
|
|
});
|
|
it('should remove all event listeners', () => {
|
|
bridge.on('insight:learning-started', () => { });
|
|
bridge.on('consolidation:completed', () => { });
|
|
bridge.destroy();
|
|
expect(bridge.listenerCount('insight:learning-started')).toBe(0);
|
|
expect(bridge.listenerCount('consolidation:completed')).toBe(0);
|
|
});
|
|
});
|
|
// ===== Neural init caching =====
|
|
describe('neural initialization', () => {
|
|
it('should only attempt neural load once', async () => {
|
|
const loaderFn = vi.fn().mockResolvedValue(neural);
|
|
const b = new LearningBridge(backend, { neuralLoader: loaderFn });
|
|
await b.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
await b.onInsightRecorded(createTestInsight({ summary: 'Second' }), 'entry-2');
|
|
expect(loaderFn).toHaveBeenCalledTimes(1);
|
|
b.destroy();
|
|
});
|
|
it('should cache failed init and not retry', async () => {
|
|
const loaderFn = vi.fn().mockRejectedValue(new Error('fail'));
|
|
const b = new LearningBridge(backend, { neuralLoader: loaderFn });
|
|
await b.onInsightRecorded(createTestInsight(), 'entry-1');
|
|
await b.onInsightRecorded(createTestInsight(), 'entry-2');
|
|
expect(loaderFn).toHaveBeenCalledTimes(1);
|
|
expect(b.getStats().neuralAvailable).toBe(false);
|
|
b.destroy();
|
|
});
|
|
});
|
|
});
|
|
//# sourceMappingURL=learning-bridge.test.js.map
|