291 lines
8.2 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|