2026-07-08 16:11:12 +08:00
import { test } from 'node:test' ;
import assert from 'node:assert/strict' ;
import { createRequire } from 'node:module' ;
import { mkdtempSync , mkdirSync , readFileSync , writeFileSync } from 'node:fs' ;
import { tmpdir } from 'node:os' ;
import { join } from 'node:path' ;
2026-07-12 00:28:17 +08:00
import { createQueryApi , createAttuneApi } from '../packages/core/src/query.mjs' ;
2026-07-08 16:11:12 +08:00
const require = createRequire ( import . meta . url );
const { DatabaseSync } = require ( 'node:sqlite' );
2026-07-12 00:28:17 +08:00
const SCHEMA = readFileSync ( new URL ( '../packages/core/src/schema.sql' , import . meta . url ), 'utf8' );
2026-07-08 16:11:12 +08:00
function memoryDb ({ projectPath = '/tmp/quiet-zero-test' } = {}) {
const db = new DatabaseSync ( ':memory:' );
db . exec ( SCHEMA );
const insertSession = db . prepare ( `
INSERT INTO sessions (id, title, project, project_path, started_at, ended_at, git_branch, message_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
` );
insertSession . run ( 'sid-1' , 'Older quiet-zero session' , 'quiet-zero' , projectPath , '2026-06-09T10:00:00Z' , '2026-06-09T11:00:00Z' , 'main' , 12 );
insertSession . run ( 'sid-2' , 'Memory layer session' , 'quiet-zero' , projectPath , '2026-06-10T10:00:00Z' , '2026-06-10T11:00:00Z' , 'codex/memory-layer' , 23 );
insertSession . run ( 'sid-3' , 'Other project session' , 'other-project' , '/tmp/other-project' , '2026-06-11T10:00:00Z' , '2026-06-11T11:00:00Z' , 'main' , 5 );
const insert = db . prepare ( `
INSERT INTO memories (id, session_id, project, path, summary, created_at)
VALUES (?, ?, ?, ?, ?, ?)
` );
insert . run ( 'mem-1' , 'sid-1' , 'quiet-zero' , '.obelisk/memories/parallel-agents.md' , 'Decision: use parallel agents for independent review facets.' , '2026-06-09T12:00:00Z' );
insert . run ( 'mem-2' , 'sid-2' , 'quiet-zero' , '.obelisk/memories/sqlite-memory.md' , 'Decision: store markdown memory records in SQLite.' , '2026-06-10T12:00:00Z' );
insert . run ( 'mem-3' , 'sid-3' , 'other-project' , '.obelisk/memories/parallel-agents.md' , 'Other project note about parallel agents.' , '2026-06-11T12:00:00Z' );
db . exec ( "INSERT INTO memories_fts(memories_fts) VALUES('rebuild')" );
return db ;
}
function searchDb () {
const db = new DatabaseSync ( ':memory:' );
db . exec ( `
CREATE TABLE sessions (
id TEXT PRIMARY KEY, title TEXT, project TEXT, started_at TEXT,
source TEXT DEFAULT 'claude'
);
CREATE TABLE messages (
uuid TEXT PRIMARY KEY, session_id TEXT, text TEXT, role TEXT,
timestamp TEXT, model TEXT, cwd TEXT, content_type TEXT,
is_meta INTEGER DEFAULT 0, source TEXT DEFAULT 'claude'
);
CREATE VIRTUAL TABLE messages_fts USING fts5(
uuid UNINDEXED, session_id UNINDEXED, text,
content=messages, content_rowid=rowid
);
` );
db . prepare ( `
INSERT INTO sessions (id, title, project, started_at)
VALUES (?, ?, ?, ?)
` ). run ( 'sid-search' , 'Search session' , 'quiet-zero' , '2026-06-10T10:00:00Z' );
const insert = db . prepare ( `
INSERT INTO messages (uuid, session_id, text, role, timestamp, model, cwd, content_type, is_meta)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
` );
insert . run ( 'msg-meta' , 'sid-search' , 'needle injected caveat' , 'user' , '2026-06-10T10:00:30Z' , null , '/tmp/quiet-zero' , 'text' , 1 );
insert . run ( 'msg-text' , 'sid-search' , 'needle visible reply' , 'assistant' , '2026-06-10T10:01:00Z' , 'claude-opus' , '/tmp/quiet-zero' , 'text' , 0 );
insert . run ( 'msg-meta-near' , 'sid-search' , '<command-name>/exit</command-name>' , 'user' , '2026-06-10T10:01:30Z' , null , '/tmp/quiet-zero' , 'text' , 1 );
insert . run ( 'msg-thinking' , 'sid-search' , 'nearby reasoning trace' , 'assistant' , '2026-06-10T10:02:00Z' , 'claude-opus' , '/tmp/quiet-zero' , 'thinking' , 0 );
db . exec ( "INSERT INTO messages_fts(messages_fts) VALUES('rebuild')" );
return db ;
}
2026-07-08 16:35:10 +08:00
test ( 'search falls back to safe tokenization for FTS-special input instead of throwing' , () => {
const db = searchDb ();
const api = createQueryApi ( db );
// 'needle-reply' is FTS5 operator syntax (a hyphen). Raw MATCH would throw;
// search() must fall back to safe per-token quoting ("needle" "reply") and
// still find the message that contains both tokens.
const rows = api . search ( 'needle-reply' , { limit : 5 });
assert . deepEqual ( rows . map ( r => r . message . uuid ), [ 'msg-text' ]);
db . close ();
});
2026-07-08 16:11:12 +08:00
test ( 'search exposes content_type on hits and temporal context' , () => {
const db = searchDb ();
const api = createQueryApi ( db );
const rows = api . search ( 'needle' , { limit : 1 });
assert . equal ( rows [ 0 ]. message . uuid , 'msg-text' );
assert . equal ( rows [ 0 ]. message . content_type , 'text' );
assert . equal ( rows [ 0 ]. message . is_meta , 0 );
assert . equal ( rows [ 0 ]. context [ 0 ]. uuid , 'msg-thinking' );
assert . equal ( rows [ 0 ]. context [ 0 ]. content_type , 'thinking' );
assert . equal ( rows [ 0 ]. context [ 0 ]. is_meta , 0 );
db . close ();
});
test ( 'search and thread omit meta messages by default and expose them on request' , () => {
const db = searchDb ();
const api = createQueryApi ( db );
assert . deepEqual ( api . search ( 'injected' , { limit : 5 }), []);
const withMeta = api . search ( 'injected' , { includeMeta : true , limit : 5 });
assert . equal ( withMeta [ 0 ]. message . uuid , 'msg-meta' );
assert . equal ( withMeta [ 0 ]. message . is_meta , 1 );
assert . deepEqual ( api . thread ( 'sid-search' ). map ( m => m . uuid ), [ 'msg-text' , 'msg-thinking' ]);
assert . deepEqual (
api . thread ( 'sid-search' , { includeMeta : true }). map ( m => m . uuid ),
[ 'msg-meta' , 'msg-text' , 'msg-meta-near' , 'msg-thinking' ],
);
db . close ();
});
test ( 'memories follows list-helper scalar opts and filters by query within scope' , () => {
const db = memoryDb ();
const api = createQueryApi ( db );
assert . deepEqual ( api . memories ( 'sid-1' ). map ( m => m . id ), [ 'mem-1' ]);
assert . deepEqual ( api . memories ( 1 ). map ( m => m . id ), [ 'mem-3' ]);
assert . deepEqual (
api . memories ({ project : '%quiet-zero%' , query : 'parallel agents' , limit : 5 }). map ( m => m . id ),
[ 'mem-1' ],
);
db . close ();
});
test ( 'memories requires English query terms' , () => {
const db = memoryDb ();
const api = createQueryApi ( db );
assert . throws (
() => api . memories ({ query : '记忆层' , limit : 5 }),
/memories\(\) query must use English terms/ ,
);
db . close ();
});
test ( 'memories uses FTS recall with safe English tokenization and rank' , () => {
const db = memoryDb ();
const api = createQueryApi ( db );
const rows = api . memories ({ project : '%quiet-zero%' , query : 'sqlite-memory' , limit : 5 });
assert . deepEqual ( rows . map ( m => m . id ), [ 'mem-2' ]);
assert . equal ( typeof rows [ 0 ]. rank , 'number' );
db . close ();
});
test ( 'memories does not broaden punctuation-only FTS queries into full recall' , () => {
const db = memoryDb ();
const api = createQueryApi ( db );
assert . deepEqual ( api . memories ({ project : '%quiet-zero%' , query : '---' , limit : 5 }), []);
db . close ();
});
test ( 'overview returns a compact current-project map with bounded sessions' , () => {
const db = memoryDb ({ projectPath : process . cwd () });
const api = createQueryApi ( db );
const view = api . overview ({ limit : 1 , projectLimit : 5 });
assert . equal ( view . current . cwd , process . cwd ());
assert . equal ( view . current . project . project , 'quiet-zero' );
assert . equal ( view . current . project . source , 'cwd_project_path' );
assert . equal ( view . current . project . confidence , 'exact' );
assert . equal ( 'session' in view . current , false );
assert . equal ( view . current_project . session_total , 2 );
assert . deepEqual ( view . current_project . sessions . map ( s => s . id ), [ 'sid-2' ]);
assert . equal ( view . current_project . memory_total , 2 );
assert . deepEqual ( view . current_project . memories . map ( m => m . id ), [ 'mem-2' , 'mem-1' ]);
assert . equal ( view . totals . projects , 2 );
assert . equal ( view . totals . sessions , 3 );
assert . equal ( view . totals . memories , 3 );
assert . ok ( view . projects . some ( p => p . project === 'quiet-zero' && p . session_count === 2 && p . memory_count === 2 ));
db . close ();
});
test ( 'query api is read-only and does not expose attune helpers' , () => {
const db = memoryDb ();
const api = createQueryApi ( db );
assert . equal ( api . remember , undefined );
assert . equal ( api . forget , undefined );
assert . equal ( typeof api . overview , 'function' );
assert . deepEqual ( api . sql ( 'SELECT id FROM memories ORDER BY id' ). map ( r => r . id ), [ 'mem-1' , 'mem-2' , 'mem-3' ]);
assert . throws (
() => api . sql ( "INSERT INTO memories (id, path, summary) VALUES ('mem-x', '/tmp/x.md', 'x')" ),
/sql\(\) only supports read-only SELECT\/WITH queries/ ,
);
db . close ();
});
test ( 'attune api exposes only memory mutation helpers' , () => {
const db = memoryDb ();
const api = createAttuneApi ( db );
assert . deepEqual ( Object . keys ( api ). sort (), [ 'forget' , 'remember' ]);
assert . equal ( api . search , undefined );
assert . equal ( api . sql , undefined );
assert . equal ( typeof api . remember , 'function' );
assert . equal ( typeof api . forget , 'function' );
db . close ();
});
test ( 'remember stores absolute project-relative memory path' , () => {
const projectDir = mkdtempSync ( join ( tmpdir (), 'obelisk-memory-project-' ));
const memoryDir = join ( projectDir , '.obelisk' , 'memories' );
mkdirSync ( memoryDir , { recursive : true });
const memoryPath = join ( memoryDir , 'decision.md' );
writeFileSync ( memoryPath , '# Decision\n' );
const db = memoryDb ({ projectPath : projectDir });
const api = createAttuneApi ( db );
const result = api . remember ({
path : '.obelisk/memories/decision.md' ,
session_id : 'sid-1' ,
summary : 'Decision: store normalized memory paths.' ,
});
assert . equal ( result . path , memoryPath );
assert . equal ( db . prepare ( 'SELECT path FROM memories WHERE id=?' ). get ( result . id ). path , memoryPath );
db . close ();
});
test ( 'remember updates FTS recall for the registered memory immediately' , () => {
const projectDir = mkdtempSync ( join ( tmpdir (), 'obelisk-memory-project-' ));
const memoryDir = join ( projectDir , '.obelisk' , 'memories' );
mkdirSync ( memoryDir , { recursive : true });
const memoryPath = join ( memoryDir , 'query-plan.md' );
writeFileSync ( memoryPath , '# Query Plan\n' );
const db = memoryDb ({ projectPath : projectDir });
const api = createAttuneApi ( db );
const registered = api . remember ({
path : '.obelisk/memories/query-plan.md' ,
session_id : 'sid-2' ,
summary : 'Decision: use faceted query plans for synthesis recall.' ,
});
const rows = createQueryApi ( db ). memories ({
project : '%quiet-zero%' ,
query : 'faceted query plans' ,
limit : 5 ,
});
assert . deepEqual ( rows . map ( m => m . id ), [ registered . id ]);
assert . equal ( typeof rows [ 0 ]. rank , 'number' );
db . close ();
});
test ( 'forget soft-deletes memory records from active recall' , () => {
const db = memoryDb ();
const api = createAttuneApi ( db );
const result = api . forget ({ id : 'mem-1' , reason : 'Outdated project guidance.' });
assert . equal ( result . id , 'mem-1' );
assert . equal ( result . deleted_reason , 'Outdated project guidance.' );
assert . match ( result . deleted_at , /^\d{4}-\d{2}-\d{2}T/ );
const row = db . prepare ( 'SELECT deleted_at, deleted_reason FROM memories WHERE id=?' ). get ( 'mem-1' );
assert . equal ( row . deleted_at , result . deleted_at );
assert . equal ( row . deleted_reason , 'Outdated project guidance.' );
assert . deepEqual ( createQueryApi ( db ). memories ({ project : '%quiet-zero%' , limit : 10 }). map ( m => m . id ), [ 'mem-2' ]);
db . close ();
});
test ( 'remember requires English summaries' , () => {
const projectDir = mkdtempSync ( join ( tmpdir (), 'obelisk-memory-project-' ));
const memoryDir = join ( projectDir , '.obelisk' , 'memories' );
mkdirSync ( memoryDir , { recursive : true });
const memoryPath = join ( memoryDir , 'decision.md' );
writeFileSync ( memoryPath , '# Decision\n' );
const db = memoryDb ({ projectPath : projectDir });
const api = createAttuneApi ( db );
assert . throws (
() => api . remember ({
path : '.obelisk/memories/decision.md' ,
session_id : 'sid-1' ,
summary : '决策:记忆摘要必须使用英文。' ,
}),
/remember\(\) summary must be written in English/ ,
);
db . close ();
});
test ( 'remember rejects missing memory files' , () => {
const projectDir = mkdtempSync ( join ( tmpdir (), 'obelisk-memory-project-' ));
const db = memoryDb ({ projectPath : projectDir });
const api = createAttuneApi ( db );
assert . throws (
() => api . remember ({
path : '.obelisk/memories/missing.md' ,
session_id : 'sid-1' ,
summary : 'Decision: this should not be registered.' ,
}),
/remember\(\) memory file does not exist/ ,
);
db . close ();
});