/** * Tests for AutoMemoryBridge * * TDD London School (mock-first) tests for the bidirectional bridge * between Claude Code auto memory and AgentDB. */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fsSync from 'node:fs'; import * as path from 'node:path'; import { AutoMemoryBridge, resolveAutoMemoryDir, findGitRoot, parseMarkdownEntries, extractSummaries, formatInsightLine, hashContent, pruneTopicFile, hasSummaryLine, } from './auto-memory-bridge.js'; // ===== 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().mockResolvedValue(null), 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, }; } // ===== Utility Function Tests ===== describe('resolveAutoMemoryDir', () => { it('should derive path from working directory', () => { const result = resolveAutoMemoryDir('/workspaces/my-project'); expect(result).toContain('.claude/projects/'); expect(result).toContain('memory'); expect(result).not.toContain('//'); }); it('should replace slashes with dashes', () => { const result = resolveAutoMemoryDir('/workspaces/my-project'); expect(result).toContain('workspaces-my-project'); }); it('should produce consistent paths for same input', () => { const a = resolveAutoMemoryDir('/workspaces/my-project'); const b = resolveAutoMemoryDir('/workspaces/my-project'); expect(a).toBe(b); }); }); describe('findGitRoot', () => { it('should find git root for a directory inside a repo', () => { // We know /workspaces/claude-flow is a git repo const root = findGitRoot('/workspaces/claude-flow/v3/@claude-flow/memory'); expect(root).toBe('/workspaces/claude-flow'); }); it('should return the directory itself if it is the git root', () => { const root = findGitRoot('/workspaces/claude-flow'); expect(root).toBe('/workspaces/claude-flow'); }); it('should return null for root filesystem', () => { // /proc is almost certainly not in a git repo const result = findGitRoot('/proc'); expect(result).toBeNull(); }); }); describe('parseMarkdownEntries', () => { it('should parse markdown with ## headings into entries', () => { const content = `# Main Title ## Section One Content of section one. More content here. ## Section Two Content of section two. `; const entries = parseMarkdownEntries(content); expect(entries).toHaveLength(2); expect(entries[0].heading).toBe('Section One'); expect(entries[0].content).toContain('Content of section one'); expect(entries[1].heading).toBe('Section Two'); }); it('should return empty array for content without ## headings', () => { const content = '# Only h1 heading\nSome text\n'; const entries = parseMarkdownEntries(content); expect(entries).toHaveLength(0); }); it('should handle multiple lines under a heading', () => { const content = `## Heading Line 1 Line 2 Line 3 `; const entries = parseMarkdownEntries(content); expect(entries).toHaveLength(1); expect(entries[0].content).toContain('Line 1'); expect(entries[0].content).toContain('Line 3'); }); it('should trim whitespace from section content', () => { const content = '## Padded\n\n Text here \n\n'; const entries = parseMarkdownEntries(content); expect(entries[0].content).toBe('Text here'); }); it('should handle empty content', () => { expect(parseMarkdownEntries('')).toHaveLength(0); }); }); describe('extractSummaries', () => { it('should extract bullet points from content', () => { const content = `# Topic - First summary - Second summary - See \`details.md\` for more Some other text `; const summaries = extractSummaries(content); expect(summaries).toHaveLength(2); expect(summaries[0]).toBe('First summary'); expect(summaries[1]).toBe('Second summary'); }); it('should skip "See" references', () => { const content = '- Good item\n- See `file.md` for details\n'; const summaries = extractSummaries(content); expect(summaries).toHaveLength(1); expect(summaries[0]).toBe('Good item'); }); it('should return empty array for content without bullets', () => { const content = 'No bullets here\n'; const summaries = extractSummaries(content); expect(summaries).toHaveLength(0); }); it('should strip metadata annotations from summaries', () => { const content = '- Use Int8 quantization _(agent:tester, 2026-02-08, conf: 0.95)_\n'; const summaries = extractSummaries(content); expect(summaries).toHaveLength(1); expect(summaries[0]).toBe('Use Int8 quantization'); }); it('should handle summaries without annotations', () => { const content = '- Clean summary without annotation\n'; const summaries = extractSummaries(content); expect(summaries[0]).toBe('Clean summary without annotation'); }); }); describe('formatInsightLine', () => { it('should format insight as a markdown bullet', () => { const insight = createTestInsight(); const line = formatInsightLine(insight); expect(line.startsWith('- HNSW index requires initialization before search')).toBe(true); expect(line).toContain('agent:tester'); expect(line).toContain('0.95'); }); it('should include detail as indented content for multi-line details', () => { const insight = createTestInsight({ detail: 'Line 1\nLine 2\nLine 3', }); const line = formatInsightLine(insight); expect(line).toContain(' Line 1'); expect(line).toContain(' Line 2'); }); it('should not add indented detail for single-line details', () => { const insight = createTestInsight({ detail: 'Short detail' }); const line = formatInsightLine(insight); // Single-line detail should not produce indented lines expect(line.split('\n')).toHaveLength(1); }); }); describe('hashContent', () => { it('should produce consistent hashes', () => { const hash1 = hashContent('test content'); const hash2 = hashContent('test content'); expect(hash1).toBe(hash2); }); it('should produce different hashes for different content', () => { const hash1 = hashContent('content A'); const hash2 = hashContent('content B'); expect(hash1).not.toBe(hash2); }); it('should return a 16-character hex string', () => { const hash = hashContent('test'); expect(hash).toMatch(/^[0-9a-f]{16}$/); }); }); describe('pruneTopicFile', () => { it('should not prune if under limit', () => { const content = '# Header\n\nSubheader\n- Item 1\n- Item 2\n'; const result = pruneTopicFile(content, 100); expect(result).toBe(content); }); it('should keep header and newest entries when pruning', () => { const lines = ['# Header', '', 'Description']; for (let i = 0; i < 20; i++) { lines.push(`- Entry ${i}`); } const content = lines.join('\n'); const result = pruneTopicFile(content, 13); const resultLines = result.split('\n'); expect(resultLines[0]).toBe('# Header'); expect(resultLines).toHaveLength(13); expect(resultLines[resultLines.length - 1]).toBe('- Entry 19'); }); }); describe('hasSummaryLine', () => { it('should find exact summary at start of bullet line', () => { const content = '- Use Int8 quantization _(source, date)_\n- Other item\n'; expect(hasSummaryLine(content, 'Use Int8 quantization')).toBe(true); }); it('should not match substrings inside other bullets', () => { const content = '- Do not use Int8 for this case\n'; // "Use Int8" should NOT match because the line starts with "- Do not use..." expect(hasSummaryLine(content, 'Use Int8')).toBe(false); }); it('should return false when summary is absent', () => { const content = '- Something else\n'; expect(hasSummaryLine(content, 'Missing summary')).toBe(false); }); it('should handle empty content', () => { expect(hasSummaryLine('', 'anything')).toBe(false); }); }); // ===== AutoMemoryBridge Tests ===== describe('AutoMemoryBridge', () => { let bridge; let backend; let testDir; beforeEach(() => { testDir = path.join('/tmp', `auto-memory-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`); fsSync.mkdirSync(testDir, { recursive: true }); backend = createMockBackend(); bridge = new AutoMemoryBridge(backend, { memoryDir: testDir, syncMode: 'on-session-end', }); }); afterEach(() => { bridge.destroy(); if (fsSync.existsSync(testDir)) { fsSync.rmSync(testDir, { recursive: true, force: true }); } }); describe('constructor', () => { it('should initialize with default config', () => { const b = new AutoMemoryBridge(backend); expect(b.getMemoryDir()).toBeTruthy(); b.destroy(); }); it('should use provided memory directory', () => { expect(bridge.getMemoryDir()).toBe(testDir); }); }); describe('getIndexPath', () => { it('should return path to MEMORY.md', () => { expect(bridge.getIndexPath()).toBe(path.join(testDir, 'MEMORY.md')); }); }); describe('getTopicPath', () => { it('should return path to topic file based on category', () => { const p = bridge.getTopicPath('debugging'); expect(p).toBe(path.join(testDir, 'debugging.md')); }); it('should use custom topic mapping', () => { const custom = new AutoMemoryBridge(backend, { memoryDir: testDir, topicMapping: { debugging: 'bugs.md' }, }); const p = custom.getTopicPath('debugging'); expect(p).toBe(path.join(testDir, 'bugs.md')); custom.destroy(); }); }); describe('recordInsight', () => { it('should store insight in AgentDB', async () => { const insight = createTestInsight(); await bridge.recordInsight(insight); expect(backend.store).toHaveBeenCalledTimes(1); const storedEntry = backend.storedEntries[0]; expect(storedEntry.namespace).toBe('learnings'); expect(storedEntry.tags).toContain('insight'); expect(storedEntry.tags).toContain('debugging'); }); it('should emit insight:recorded event', async () => { const handler = vi.fn(); bridge.on('insight:recorded', handler); const insight = createTestInsight(); await bridge.recordInsight(insight); expect(handler).toHaveBeenCalledWith(insight); }); it('should write to files immediately in on-write mode', async () => { bridge.destroy(); bridge = new AutoMemoryBridge(backend, { memoryDir: testDir, syncMode: 'on-write', }); const insight = createTestInsight(); await bridge.recordInsight(insight); const topicPath = bridge.getTopicPath('debugging'); expect(fsSync.existsSync(topicPath)).toBe(true); const content = fsSync.readFileSync(topicPath, 'utf-8'); expect(content).toContain(insight.summary); }); it('should not write to files immediately in on-session-end mode', async () => { const insight = createTestInsight(); await bridge.recordInsight(insight); const topicPath = bridge.getTopicPath('debugging'); expect(fsSync.existsSync(topicPath)).toBe(false); }); it('should store confidence in metadata', async () => { await bridge.recordInsight(createTestInsight({ confidence: 0.42 })); const stored = backend.storedEntries[0]; expect(stored.metadata.confidence).toBe(0.42); }); }); describe('syncToAutoMemory', () => { it('should flush buffered insights to files', async () => { await bridge.recordInsight(createTestInsight()); await bridge.recordInsight(createTestInsight({ category: 'performance', summary: 'Use Int8 quantization for 3.92x memory reduction', })); const result = await bridge.syncToAutoMemory(); expect(result.synced).toBeGreaterThan(0); expect(result.categories).toContain('debugging'); expect(result.categories).toContain('performance'); expect(result.errors).toHaveLength(0); expect(fsSync.existsSync(bridge.getTopicPath('debugging'))).toBe(true); expect(fsSync.existsSync(bridge.getTopicPath('performance'))).toBe(true); }); it('should create MEMORY.md index', async () => { await bridge.recordInsight(createTestInsight()); await bridge.syncToAutoMemory(); expect(fsSync.existsSync(bridge.getIndexPath())).toBe(true); }); it('should emit sync:completed event', async () => { const handler = vi.fn(); bridge.on('sync:completed', handler); await bridge.syncToAutoMemory(); expect(handler).toHaveBeenCalledWith(expect.objectContaining({ durationMs: expect.any(Number), })); }); it('should not duplicate insights on repeated sync', async () => { await bridge.recordInsight(createTestInsight()); await bridge.syncToAutoMemory(); await bridge.syncToAutoMemory(); const topicPath = bridge.getTopicPath('debugging'); const content = fsSync.readFileSync(topicPath, 'utf-8'); const matches = content.match(/HNSW index requires/g); expect(matches).toHaveLength(1); }); it('should clear the insight buffer after sync', async () => { await bridge.recordInsight(createTestInsight()); expect(bridge.getStatus().bufferedInsights).toBe(1); await bridge.syncToAutoMemory(); expect(bridge.getStatus().bufferedInsights).toBe(0); }); it('should skip AgentDB entries already synced from the buffer', async () => { // Record an insight (stored in buffer AND AgentDB) await bridge.recordInsight(createTestInsight()); // Mock backend.query to return the same insight from AgentDB const mockEntry = { id: 'test-1', key: 'insight:debugging:12345', content: 'HNSW index requires initialization before search', tags: ['insight', 'debugging'], metadata: { category: 'debugging', summary: 'HNSW index requires initialization before search', confidence: 0.95, }, }; backend.query.mockResolvedValueOnce([mockEntry]); await bridge.syncToAutoMemory(); // Should only appear once const content = fsSync.readFileSync(bridge.getTopicPath('debugging'), 'utf-8'); const matches = content.match(/HNSW index requires/g); expect(matches).toHaveLength(1); }); }); describe('importFromAutoMemory', () => { it('should import entries from existing markdown files', async () => { const topicContent = `# Debugging Insights ## Known Issues - Always init HNSW before search - SQLite WASM needs sql.js `; fsSync.writeFileSync(path.join(testDir, 'debugging.md'), topicContent, 'utf-8'); const result = await bridge.importFromAutoMemory(); expect(result.imported).toBeGreaterThan(0); expect(result.files).toContain('debugging.md'); // Should use bulkInsert, not individual store calls expect(backend.bulkInsert).toHaveBeenCalled(); }); it('should skip entries already in AgentDB', async () => { const topicContent = `# Test ## Existing Already in DB `; fsSync.writeFileSync(path.join(testDir, 'test.md'), topicContent, 'utf-8'); // Mock backend to return existing entry with matching content hash backend.query.mockResolvedValue([{ id: 'existing-1', metadata: { contentHash: hashContent('Already in DB') }, }]); const result = await bridge.importFromAutoMemory(); expect(result.skipped).toBeGreaterThan(0); }); it('should return zero imported for non-existent directory', async () => { bridge.destroy(); bridge = new AutoMemoryBridge(backend, { memoryDir: '/tmp/nonexistent-auto-memory-dir-xyz', }); const result = await bridge.importFromAutoMemory(); expect(result.imported).toBe(0); expect(result.files).toHaveLength(0); }); it('should batch imports with bulkInsert', async () => { // Create multiple files with multiple sections fsSync.writeFileSync(path.join(testDir, 'file1.md'), '## A\nContent A\n## B\nContent B\n', 'utf-8'); fsSync.writeFileSync(path.join(testDir, 'file2.md'), '## C\nContent C\n', 'utf-8'); await bridge.importFromAutoMemory(); // bulkInsert should be called once with all entries expect(backend.bulkInsert).toHaveBeenCalledTimes(1); const batchArg = backend.bulkInsert.mock.calls[0][0]; expect(batchArg).toHaveLength(3); }); }); describe('curateIndex', () => { it('should generate MEMORY.md from topic files', async () => { fsSync.writeFileSync(path.join(testDir, 'debugging.md'), '# Debugging\n\n- Init HNSW before search\n- Check embeddings dimension\n', 'utf-8'); fsSync.writeFileSync(path.join(testDir, 'performance.md'), '# Performance\n\n- Use Int8 quantization\n', 'utf-8'); await bridge.curateIndex(); const indexContent = fsSync.readFileSync(bridge.getIndexPath(), 'utf-8'); expect(indexContent).toContain('# Claude Flow V3 Project Memory'); expect(indexContent).toContain('Init HNSW before search'); expect(indexContent).toContain('Use Int8 quantization'); }); it('should stay under maxIndexLines', async () => { const lines = ['# Debugging', '']; for (let i = 0; i < 200; i++) { lines.push(`- Item ${i} is a debugging insight`); } fsSync.writeFileSync(path.join(testDir, 'debugging.md'), lines.join('\n'), 'utf-8'); bridge.destroy(); bridge = new AutoMemoryBridge(backend, { memoryDir: testDir, maxIndexLines: 20, }); await bridge.curateIndex(); const indexContent = fsSync.readFileSync(bridge.getIndexPath(), 'utf-8'); const indexLines = indexContent.split('\n'); expect(indexLines.length).toBeLessThanOrEqual(20); }); it('should strip metadata from summaries in the index', async () => { fsSync.writeFileSync(path.join(testDir, 'debugging.md'), '# Debugging\n\n- Fixed a bug _(agent:tester, 2026-02-08, conf: 0.90)_\n', 'utf-8'); await bridge.curateIndex(); const indexContent = fsSync.readFileSync(bridge.getIndexPath(), 'utf-8'); expect(indexContent).toContain('- Fixed a bug'); expect(indexContent).not.toContain('_(agent:tester'); }); it('should handle pruneStrategy=fifo by removing oldest entries', async () => { const lines = ['# Debugging', '']; for (let i = 0; i < 50; i++) { lines.push(`- Item ${i}`); } fsSync.writeFileSync(path.join(testDir, 'debugging.md'), lines.join('\n'), 'utf-8'); bridge.destroy(); bridge = new AutoMemoryBridge(backend, { memoryDir: testDir, maxIndexLines: 10, pruneStrategy: 'fifo', }); await bridge.curateIndex(); const indexContent = fsSync.readFileSync(bridge.getIndexPath(), 'utf-8'); // FIFO removes oldest (first) items, keeps newest expect(indexContent).toContain('Item 49'); expect(indexContent).not.toContain('Item 0'); }); }); describe('getStatus', () => { it('should report status for existing directory', async () => { fsSync.writeFileSync(path.join(testDir, 'MEMORY.md'), '# Memory\n- Item 1\n- Item 2\n', 'utf-8'); const status = bridge.getStatus(); expect(status.exists).toBe(true); expect(status.memoryDir).toBe(testDir); expect(status.files.length).toBeGreaterThan(0); expect(status.indexLines).toBeGreaterThanOrEqual(3); }); it('should report status for non-existent directory', () => { bridge.destroy(); bridge = new AutoMemoryBridge(backend, { memoryDir: '/tmp/nonexistent-dir-xyz', }); const status = bridge.getStatus(); expect(status.exists).toBe(false); expect(status.files).toHaveLength(0); }); it('should count buffered insights', async () => { await bridge.recordInsight(createTestInsight()); await bridge.recordInsight(createTestInsight({ summary: 'Another insight' })); const status = bridge.getStatus(); expect(status.bufferedInsights).toBe(2); }); it('should report lastSyncTime after sync', async () => { expect(bridge.getStatus().lastSyncTime).toBe(0); await bridge.syncToAutoMemory(); expect(bridge.getStatus().lastSyncTime).toBeGreaterThan(0); }); }); describe('recordInsight - key uniqueness', () => { it('should generate unique keys for rapid sequential inserts', async () => { // Record multiple insights as fast as possible (same ms possible) await bridge.recordInsight(createTestInsight({ summary: 'Insight A' })); await bridge.recordInsight(createTestInsight({ summary: 'Insight B' })); await bridge.recordInsight(createTestInsight({ summary: 'Insight C' })); // All three should have unique keys const keys = backend.storedEntries.map(e => e.key); const uniqueKeys = new Set(keys); expect(uniqueKeys.size).toBe(3); }); }); describe('syncToAutoMemory - error handling', () => { it('should emit sync:failed on backend query error', async () => { const handler = vi.fn(); bridge.on('sync:failed', handler); // Make ensureMemoryDir throw backend.query.mockRejectedValueOnce(new Error('DB connection lost')); // Record an insight so there's something to sync await bridge.recordInsight(createTestInsight()); // The sync should still succeed for the buffered part // because queryRecentInsights has its own try/catch const result = await bridge.syncToAutoMemory(); expect(result.errors).toHaveLength(0); }); it('should report errors for individual insight write failures', async () => { // Create a read-only file to force a write error const topicPath = bridge.getTopicPath('debugging'); fsSync.writeFileSync(topicPath, '# Debugging\n\n- Existing\n', 'utf-8'); fsSync.chmodSync(topicPath, 0o444); // read-only await bridge.recordInsight(createTestInsight()); const result = await bridge.syncToAutoMemory(); // Should have error from trying to write to read-only file expect(result.errors.length).toBeGreaterThan(0); // Restore permissions for cleanup fsSync.chmodSync(topicPath, 0o644); }); }); describe('syncToAutoMemory - append to existing topic file', () => { it('should append new insight to existing topic file', async () => { // Create initial topic file const topicPath = bridge.getTopicPath('debugging'); fsSync.writeFileSync(topicPath, '# Debugging\n\n- Existing item\n', 'utf-8'); bridge.destroy(); bridge = new AutoMemoryBridge(backend, { memoryDir: testDir, syncMode: 'on-write', }); await bridge.recordInsight(createTestInsight({ summary: 'New insight' })); const content = fsSync.readFileSync(topicPath, 'utf-8'); expect(content).toContain('Existing item'); expect(content).toContain('New insight'); }); it('should prune topic file when it exceeds maxTopicFileLines', async () => { // Create a topic file near the limit const topicPath = bridge.getTopicPath('debugging'); const lines = ['# Debugging', '', 'Description']; for (let i = 0; i < 500; i++) { lines.push(`- Entry ${i}`); } fsSync.writeFileSync(topicPath, lines.join('\n'), 'utf-8'); bridge.destroy(); bridge = new AutoMemoryBridge(backend, { memoryDir: testDir, syncMode: 'on-write', maxTopicFileLines: 500, }); await bridge.recordInsight(createTestInsight({ summary: 'Overflow insight' })); const content = fsSync.readFileSync(topicPath, 'utf-8'); expect(content).toContain('Overflow insight'); // Old entries near the top should have been pruned expect(content).not.toContain('Entry 0'); // Header should be preserved expect(content).toContain('# Debugging'); }); }); describe('syncToAutoMemory - classifyEntry coverage', () => { it('should classify by metadata category when present', async () => { const entry = { id: 'e1', key: 'insight:security:999:0', content: 'SQL injection found', tags: ['insight'], metadata: { category: 'security', summary: 'SQL injection found', confidence: 0.9 }, }; backend.query.mockResolvedValueOnce([entry]); await bridge.syncToAutoMemory(); expect(fsSync.existsSync(bridge.getTopicPath('security'))).toBe(true); const content = fsSync.readFileSync(bridge.getTopicPath('security'), 'utf-8'); expect(content).toContain('SQL injection found'); }); it('should classify by tags when metadata category is absent', async () => { const entry = { id: 'e2', key: 'insight:unknown:999:0', content: 'Performance is slow', tags: ['insight', 'performance', 'benchmark'], metadata: { summary: 'Performance is slow', confidence: 0.85 }, }; backend.query.mockResolvedValueOnce([entry]); await bridge.syncToAutoMemory(); expect(fsSync.existsSync(bridge.getTopicPath('performance'))).toBe(true); }); it('should default to project-patterns for unclassifiable entries', async () => { const entry = { id: 'e3', key: 'insight:misc:999:0', content: 'Miscellaneous note', tags: ['insight'], metadata: { summary: 'Miscellaneous note', confidence: 0.8 }, }; backend.query.mockResolvedValueOnce([entry]); await bridge.syncToAutoMemory(); expect(fsSync.existsSync(bridge.getTopicPath('project-patterns'))).toBe(true); }); it('should classify debugging tags correctly', async () => { const bugEntry = { id: 'e4', key: 'insight:bug:999:0', content: 'Found a bug', tags: ['insight', 'bug'], metadata: { summary: 'Found a bug', confidence: 0.9 }, }; backend.query.mockResolvedValueOnce([bugEntry]); await bridge.syncToAutoMemory(); expect(fsSync.existsSync(bridge.getTopicPath('debugging'))).toBe(true); }); it('should classify swarm/agent tags correctly', async () => { const swarmEntry = { id: 'e5', key: 'insight:swarm:999:0', content: 'Swarm completed successfully', tags: ['insight', 'swarm'], metadata: { summary: 'Swarm completed successfully', confidence: 0.9 }, }; backend.query.mockResolvedValueOnce([swarmEntry]); await bridge.syncToAutoMemory(); expect(fsSync.existsSync(bridge.getTopicPath('swarm-results'))).toBe(true); }); }); describe('importFromAutoMemory - edge cases', () => { it('should emit import:completed event', async () => { const handler = vi.fn(); bridge.on('import:completed', handler); fsSync.writeFileSync(path.join(testDir, 'test.md'), '## Section\nContent here\n', 'utf-8'); await bridge.importFromAutoMemory(); expect(handler).toHaveBeenCalledWith(expect.objectContaining({ imported: expect.any(Number), durationMs: expect.any(Number), })); }); it('should handle files with no ## headings', async () => { fsSync.writeFileSync(path.join(testDir, 'empty.md'), '# Just a title\nSome text without sections\n', 'utf-8'); const result = await bridge.importFromAutoMemory(); expect(result.imported).toBe(0); expect(result.files).toContain('empty.md'); }); }); describe('curateIndex - edge cases', () => { it('should handle empty topic files', async () => { fsSync.writeFileSync(path.join(testDir, 'debugging.md'), '# Debugging\n\n', 'utf-8'); await bridge.curateIndex(); const content = fsSync.readFileSync(bridge.getIndexPath(), 'utf-8'); // Should not include empty section expect(content).not.toContain('Debugging'); }); it('should emit index:curated event', async () => { const handler = vi.fn(); bridge.on('index:curated', handler); fsSync.writeFileSync(path.join(testDir, 'debugging.md'), '# Debugging\n\n- An item\n', 'utf-8'); await bridge.curateIndex(); expect(handler).toHaveBeenCalledWith(expect.objectContaining({ lines: expect.any(Number), })); }); it('should handle pruneStrategy=lru same as fifo', async () => { const lines = ['# Debugging', '']; for (let i = 0; i < 50; i++) { lines.push(`- Item ${i}`); } fsSync.writeFileSync(path.join(testDir, 'debugging.md'), lines.join('\n'), 'utf-8'); bridge.destroy(); bridge = new AutoMemoryBridge(backend, { memoryDir: testDir, maxIndexLines: 10, pruneStrategy: 'lru', }); await bridge.curateIndex(); const indexContent = fsSync.readFileSync(bridge.getIndexPath(), 'utf-8'); // LRU removes oldest (first) items, same as FIFO expect(indexContent).toContain('Item 49'); expect(indexContent).not.toContain('Item 0'); }); }); describe('destroy', () => { it('should clean up periodic sync timer', () => { const periodicBridge = new AutoMemoryBridge(backend, { memoryDir: testDir, syncMode: 'periodic', syncIntervalMs: 1000, }); periodicBridge.destroy(); }); it('should remove all listeners', () => { bridge.on('insight:recorded', () => { }); bridge.on('sync:completed', () => { }); bridge.destroy(); expect(bridge.listenerCount('insight:recorded')).toBe(0); expect(bridge.listenerCount('sync:completed')).toBe(0); }); }); }); //# sourceMappingURL=auto-memory-bridge.test.js.map