Files
clawhdf5/packages/clawhdf5-node/__tests__/memory.test.ts

291 lines
8.2 KiB
TypeScript

/**
* Integration tests for @redclaw/clawhdf5.
*
* These tests require the native addon to be built first:
* npm run build
*
* They write temporary .h5 files to the OS temp directory and clean up after
* themselves.
*/
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import { ClawhdfMemory, MemorySearchResult, BackendStats, ConsolidationStats } from '../src/index';
// Helper: create a unique temp path for each test.
function tmpBrain(name: string): string {
return path.join(os.tmpdir(), `clawhdf5-test-${name}-${Date.now()}.brain`);
}
// Helper: clean up after test.
function cleanup(...paths: string[]): void {
for (const p of paths) {
for (const ext of ['', '.wal']) {
try { fs.unlinkSync(p + ext); } catch { /* ignore */ }
}
}
}
describe('ClawhdfMemory — lifecycle', () => {
test('create() produces an empty store', () => {
const p = tmpBrain('create');
try {
const mem = ClawhdfMemory.create(p, 4);
const s = mem.stats();
expect(s.totalRecords).toBe(0);
expect(s.totalEmbeddings).toBe(0);
expect(s.modalities).toEqual([]);
expect(s.lastUpdated).toBeUndefined();
} finally {
cleanup(p);
}
});
test('openOrCreate() creates when absent', () => {
const p = tmpBrain('openOrCreate-new');
try {
expect(fs.existsSync(p)).toBe(false);
const mem = ClawhdfMemory.openOrCreate(p, 4);
expect(fs.existsSync(p)).toBe(true);
expect(mem.stats().totalRecords).toBe(0);
} finally {
cleanup(p);
}
});
test('open() reloads persisted data', () => {
const p = tmpBrain('open-persist');
try {
{
const mem = ClawhdfMemory.create(p, 4);
mem.write('note.md', 'persisted content');
}
const mem2 = ClawhdfMemory.open(p);
const result = mem2.get('note.md');
expect(result).not.toBeNull();
expect(result).toContain('persisted content');
} finally {
cleanup(p);
}
});
test('openOrCreate() opens when present', () => {
const p = tmpBrain('openOrCreate-exists');
try {
{
const mem = ClawhdfMemory.create(p, 4);
mem.write('x.md', 'some data');
}
const mem2 = ClawhdfMemory.openOrCreate(p, 4);
expect(mem2.stats().totalRecords).toBe(1);
} finally {
cleanup(p);
}
});
});
describe('ClawhdfMemory — write / get', () => {
test('write then get returns the content', () => {
const p = tmpBrain('write-get');
try {
const mem = ClawhdfMemory.create(p, 4);
mem.write('memory/notes.md', 'Hello, memory!');
const result = mem.get('memory/notes.md');
expect(result).toContain('Hello, memory!');
} finally {
cleanup(p);
}
});
test('get returns null for unknown path', () => {
const p = tmpBrain('get-missing');
try {
const mem = ClawhdfMemory.create(p, 4);
expect(mem.get('does/not/exist.md')).toBeNull();
} finally {
cleanup(p);
}
});
test('get with line range filters correctly', () => {
const p = tmpBrain('get-linerange');
try {
const mem = ClawhdfMemory.create(p, 4);
mem.write('lines.md', 'line0\nline1\nline2\nline3\nline4');
const slice = mem.get('lines.md', 1, 2);
expect(slice).not.toBeNull();
const lines = slice!.split('\n');
expect(lines).toHaveLength(2);
expect(lines[0]).toBe('line1');
expect(lines[1]).toBe('line2');
} finally {
cleanup(p);
}
});
});
describe('ClawhdfMemory — stats', () => {
test('stats update after writes', () => {
const p = tmpBrain('stats');
try {
const mem = ClawhdfMemory.create(p, 4);
mem.write('a.md', 'content A');
mem.write('b.md', 'content B');
const s = mem.stats();
expect(s.totalRecords).toBe(2);
expect(s.modalities).toContain('text');
expect(s.fileSizeBytes).toBeGreaterThan(0);
expect(s.lastUpdated).toBeDefined();
} finally {
cleanup(p);
}
});
});
describe('ClawhdfMemory — Markdown ingestion & export', () => {
test('ingestMarkdown returns section count', () => {
const p = tmpBrain('ingest');
try {
const mem = ClawhdfMemory.create(p, 4);
const md = '# Goals\n\nLearn Rust.\n\n# Projects\n\nBuild clawhdf5.';
const count = mem.ingestMarkdown('memory/user.md', md);
expect(count).toBe(2);
expect(mem.stats().totalRecords).toBe(2);
} finally {
cleanup(p);
}
});
test('ingestMarkdown empty string returns 0', () => {
const p = tmpBrain('ingest-empty');
try {
const mem = ClawhdfMemory.create(p, 4);
expect(mem.ingestMarkdown('empty.md', '')).toBe(0);
} finally {
cleanup(p);
}
});
test('exportMarkdown roundtrip preserves sections', () => {
const p = tmpBrain('export');
try {
const mem = ClawhdfMemory.create(p, 4);
const md = '## Section One\n\nContent one.\n\n## Section Two\n\nContent two.';
mem.ingestMarkdown('docs/test.md', md);
const exported = mem.exportMarkdown('docs/test.md');
expect(exported).toContain('Section One');
expect(exported).toContain('Content one.');
expect(exported).toContain('Section Two');
expect(exported).toContain('Content two.');
} finally {
cleanup(p);
}
});
test('exportMarkdown throws for unknown path', () => {
const p = tmpBrain('export-missing');
try {
const mem = ClawhdfMemory.create(p, 4);
expect(() => mem.exportMarkdown('missing.md')).toThrow();
} finally {
cleanup(p);
}
});
});
describe('ClawhdfMemory — search', () => {
test('search on empty store returns empty array', () => {
const p = tmpBrain('search-empty');
try {
const mem = ClawhdfMemory.create(p, 4);
const results = mem.search('anything', new Float32Array(4), 5);
expect(results).toEqual([]);
} finally {
cleanup(p);
}
});
test('search result fields are populated', () => {
const p = tmpBrain('search-fields');
try {
const mem = ClawhdfMemory.create(p, 4);
mem.write('mem/x.md', 'hello world greeting');
const results = mem.search('hello', new Float32Array(4), 3);
if (results.length > 0) {
const r = results[0];
expect(typeof r.text).toBe('string');
expect(r.text.length).toBeGreaterThan(0);
expect(typeof r.score).toBe('number');
expect(typeof r.path).toBe('string');
expect(typeof r.source).toBe('string');
}
} finally {
cleanup(p);
}
});
});
describe('ClawhdfMemory — consolidation hooks', () => {
test('compact() returns a number', () => {
const p = tmpBrain('compact');
try {
const mem = ClawhdfMemory.create(p, 4);
mem.write('x.md', 'some content');
const removed = mem.compact();
expect(typeof removed).toBe('number');
expect(removed).toBeGreaterThanOrEqual(0);
} finally {
cleanup(p);
}
});
test('tickSession() does not throw', () => {
const p = tmpBrain('tick');
try {
const mem = ClawhdfMemory.create(p, 4);
mem.write('y.md', 'tick test');
expect(() => mem.tickSession()).not.toThrow();
} finally {
cleanup(p);
}
});
test('flushWal() does not throw', () => {
const p = tmpBrain('flushwal');
try {
const mem = ClawhdfMemory.create(p, 4);
expect(() => mem.flushWal()).not.toThrow();
} finally {
cleanup(p);
}
});
test('runConsolidation() returns correct shape', () => {
const p = tmpBrain('consolidate');
try {
const mem = ClawhdfMemory.create(p, 4);
mem.write('a.md', 'alpha content');
mem.write('b.md', 'beta content');
const stats: ConsolidationStats = mem.runConsolidation(Date.now() / 1000);
expect(typeof stats.workingCount).toBe('number');
expect(typeof stats.episodicCount).toBe('number');
expect(typeof stats.semanticCount).toBe('number');
expect(typeof stats.totalEvictions).toBe('number');
expect(typeof stats.totalPromotions).toBe('number');
} finally {
cleanup(p);
}
});
test('walPendingCount() returns a non-negative number', () => {
const p = tmpBrain('walpending');
try {
const mem = ClawhdfMemory.create(p, 4);
expect(mem.walPendingCount()).toBeGreaterThanOrEqual(0);
} finally {
cleanup(p);
}
});
});