231 lines
10 KiB
TypeScript
231 lines
10 KiB
TypeScript
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;
|
|
}>;
|
|
}
|