This commit is contained in:
230
packages/hive-mind-core/tests/mind/schema.test.ts
Normal file
230
packages/hive-mind-core/tests/mind/schema.test.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { MindDB } from '../../src/mind/db.js';
|
||||
import Database from 'better-sqlite3';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
describe('.mind SQLite Schema', () => {
|
||||
let db: MindDB;
|
||||
let dbPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dbPath = path.join(os.tmpdir(), `waggle-test-${Date.now()}.mind`);
|
||||
db = new MindDB(dbPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
try {
|
||||
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
||||
} catch {
|
||||
// Windows: file may still be locked briefly after close
|
||||
}
|
||||
});
|
||||
|
||||
it('creates a single portable file on disk', () => {
|
||||
expect(fs.existsSync(dbPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('creates identity table (Layer 0)', () => {
|
||||
const cols = getColumns(db, 'identity');
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'name' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'role' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'department' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'personality' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'capabilities' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'system_prompt' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'created_at' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'updated_at' }));
|
||||
});
|
||||
|
||||
it('creates awareness table (Layer 1)', () => {
|
||||
const cols = getColumns(db, 'awareness');
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'category' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'content' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'priority' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'created_at' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'expires_at' }));
|
||||
});
|
||||
|
||||
it('creates sessions table', () => {
|
||||
const cols = getColumns(db, 'sessions');
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'gop_id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'project_id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'status' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'started_at' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'ended_at' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'summary' }));
|
||||
});
|
||||
|
||||
it('enforces session status CHECK constraint', () => {
|
||||
const raw = db.getDatabase();
|
||||
raw.prepare(`INSERT INTO sessions (gop_id, project_id, status, started_at)
|
||||
VALUES ('gop:test', NULL, 'active', datetime('now'))`).run();
|
||||
expect(() => {
|
||||
raw.prepare(`INSERT INTO sessions (gop_id, project_id, status, started_at)
|
||||
VALUES ('gop:bad', NULL, 'invalid_status', datetime('now'))`).run();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('creates memory_frames table (Layer 2)', () => {
|
||||
const cols = getColumns(db, 'memory_frames');
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'frame_type' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'gop_id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 't' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'base_frame_id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'content' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'importance' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'access_count' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'created_at' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'last_accessed' }));
|
||||
});
|
||||
|
||||
it('enforces frame_type CHECK constraint (I, P, B)', () => {
|
||||
const raw = db.getDatabase();
|
||||
// Valid types should work
|
||||
raw.prepare(`INSERT INTO sessions (gop_id, status, started_at) VALUES ('gop:t1', 'active', datetime('now'))`).run();
|
||||
raw.prepare(`INSERT INTO memory_frames (frame_type, gop_id, t, content, importance)
|
||||
VALUES ('I', 'gop:t1', 0, 'test', 'normal')`).run();
|
||||
raw.prepare(`INSERT INTO memory_frames (frame_type, gop_id, t, content, importance)
|
||||
VALUES ('P', 'gop:t1', 1, 'delta', 'normal')`).run();
|
||||
raw.prepare(`INSERT INTO memory_frames (frame_type, gop_id, t, content, importance)
|
||||
VALUES ('B', 'gop:t1', 2, 'xref', 'normal')`).run();
|
||||
// Invalid type should fail
|
||||
expect(() => {
|
||||
raw.prepare(`INSERT INTO memory_frames (frame_type, gop_id, t, content, importance)
|
||||
VALUES ('X', 'gop:t1', 3, 'bad', 'normal')`).run();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('enforces importance CHECK constraint', () => {
|
||||
const raw = db.getDatabase();
|
||||
raw.prepare(`INSERT INTO sessions (gop_id, status, started_at) VALUES ('gop:imp', 'active', datetime('now'))`).run();
|
||||
for (const level of ['critical', 'important', 'normal', 'temporary', 'deprecated']) {
|
||||
raw.prepare(`INSERT INTO memory_frames (frame_type, gop_id, t, content, importance)
|
||||
VALUES ('I', 'gop:imp', 0, 'test-${level}', ?)`).run(level);
|
||||
}
|
||||
expect(() => {
|
||||
raw.prepare(`INSERT INTO memory_frames (frame_type, gop_id, t, content, importance)
|
||||
VALUES ('I', 'gop:imp', 0, 'bad', 'invalid_importance')`).run();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('creates FTS5 virtual table for memory search', () => {
|
||||
const raw = db.getDatabase();
|
||||
const tables = raw.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='memory_frames_fts'"
|
||||
).get() as { name: string } | undefined;
|
||||
expect(tables).toBeDefined();
|
||||
expect(tables!.name).toBe('memory_frames_fts');
|
||||
});
|
||||
|
||||
it('creates sqlite-vec virtual table for embeddings', () => {
|
||||
const raw = db.getDatabase();
|
||||
const tables = raw.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='memory_frames_vec'"
|
||||
).get() as { name: string } | undefined;
|
||||
expect(tables).toBeDefined();
|
||||
expect(tables!.name).toBe('memory_frames_vec');
|
||||
});
|
||||
|
||||
it('creates knowledge_entities table (Layer 3)', () => {
|
||||
const cols = getColumns(db, 'knowledge_entities');
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'entity_type' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'name' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'properties' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'valid_from' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'valid_to' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'recorded_at' }));
|
||||
});
|
||||
|
||||
it('creates knowledge_relations table (Layer 3)', () => {
|
||||
const cols = getColumns(db, 'knowledge_relations');
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'source_id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'target_id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'relation_type' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'confidence' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'properties' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'valid_from' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'valid_to' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'recorded_at' }));
|
||||
});
|
||||
|
||||
it('creates procedures table (Layer 4)', () => {
|
||||
const cols = getColumns(db, 'procedures');
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'id' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'name' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'model' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'template' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'version' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'success_rate' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'avg_cost' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'created_at' }));
|
||||
expect(cols).toContainEqual(expect.objectContaining({ name: 'updated_at' }));
|
||||
});
|
||||
|
||||
it('creates meta table with schema version', () => {
|
||||
const raw = db.getDatabase();
|
||||
const row = raw.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get() as { value: string };
|
||||
expect(row).toBeDefined();
|
||||
expect(row.value).toBe('1');
|
||||
});
|
||||
|
||||
it('creates GOP indexes for fast window queries', () => {
|
||||
const raw = db.getDatabase();
|
||||
const indexes = raw.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_frames_%'"
|
||||
).all() as { name: string }[];
|
||||
const names = indexes.map(i => i.name);
|
||||
expect(names).toContain('idx_frames_gop_t');
|
||||
expect(names).toContain('idx_frames_type');
|
||||
expect(names).toContain('idx_frames_base');
|
||||
});
|
||||
|
||||
it('creates session index for project queries', () => {
|
||||
const raw = db.getDatabase();
|
||||
const idx = raw.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND name='idx_sessions_project'"
|
||||
).get() as { name: string } | undefined;
|
||||
expect(idx).toBeDefined();
|
||||
});
|
||||
|
||||
it('can open an existing .mind file without recreating schema', () => {
|
||||
const raw = db.getDatabase();
|
||||
raw.prepare("INSERT INTO meta (key, value) VALUES ('test_key', 'test_value')").run();
|
||||
db.close();
|
||||
|
||||
const db2 = new MindDB(dbPath);
|
||||
const row = db2.getDatabase().prepare("SELECT value FROM meta WHERE key = 'test_key'").get() as { value: string };
|
||||
expect(row.value).toBe('test_value');
|
||||
db2.close();
|
||||
|
||||
// Reopen for afterEach cleanup
|
||||
db = new MindDB(dbPath);
|
||||
});
|
||||
|
||||
it('supports in-memory database for testing', () => {
|
||||
const memDb = new MindDB(':memory:');
|
||||
const cols = getColumns(memDb, 'identity');
|
||||
expect(cols.length).toBeGreaterThan(0);
|
||||
memDb.close();
|
||||
});
|
||||
});
|
||||
|
||||
function getColumns(db: MindDB, table: string) {
|
||||
return db.getDatabase().prepare(`PRAGMA table_info('${table}')`).all() as Array<{
|
||||
cid: number;
|
||||
name: string;
|
||||
type: string;
|
||||
notnull: number;
|
||||
dflt_value: string | null;
|
||||
pk: number;
|
||||
}>;
|
||||
}
|
||||
Reference in New Issue
Block a user