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:37:32 +08:00
import { createQueryApi , createAttuneApi } from '../packages/core/src/query.ts' ;
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,
2026-08-04 11:33:01 -04:00
is_meta INTEGER DEFAULT 0, visibility TEXT DEFAULT 'visible',
source TEXT DEFAULT 'claude'
2026-07-08 16:11:12 +08:00
);
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 ( `
2026-08-04 11:33:01 -04:00
INSERT INTO messages (uuid, session_id, text, role, timestamp, model, cwd, content_type, is_meta, visibility)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2026-07-08 16:11:12 +08:00
` );
2026-08-04 11:33:01 -04:00
insert . run ( 'msg-meta' , 'sid-search' , 'needle injected caveat' , 'user' , '2026-06-10T10:00:30Z' , null , '/tmp/quiet-zero' , 'text' , 1 , 'visible' );
insert . run ( 'msg-text' , 'sid-search' , 'needle visible reply' , 'assistant' , '2026-06-10T10:01:00Z' , 'claude-opus' , '/tmp/quiet-zero' , 'text' , 0 , 'visible' );
insert . run ( 'msg-meta-near' , 'sid-search' , '<command-name>/exit</command-name>' , 'user' , '2026-06-10T10:01:30Z' , null , '/tmp/quiet-zero' , 'text' , 1 , 'visible' );
insert . run ( 'msg-thinking' , 'sid-search' , 'nearby reasoning trace' , 'assistant' , '2026-06-10T10:02:00Z' , 'claude-opus' , '/tmp/quiet-zero' , 'thinking' , 0 , 'visible' );
insert . run ( 'msg-inactive' , 'sid-search' , 'needle superseded experiment' , 'assistant' , '2026-06-10T10:02:30Z' , 'claude-opus' , '/tmp/quiet-zero' , 'text' , 0 , 'inactive' );
insert . run ( 'msg-inactive-meta' , 'sid-search' , 'needle superseded injected' , 'user' , '2026-06-10T10:02:40Z' , null , '/tmp/quiet-zero' , 'text' , 1 , 'inactive' );
insert . run ( 'msg-hidden' , 'sid-search' , 'needle abandoned branch' , 'assistant' , '2026-06-10T10:03:00Z' , 'claude-opus' , '/tmp/quiet-zero' , 'text' , 0 , 'hidden' );
2026-07-08 16:11:12 +08:00
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 );
2026-08-04 11:33:01 -04:00
assert . deepEqual ( api . search ( 'abandoned' , { includeMeta : true , limit : 5 }), []);
2026-07-08 16:11:12 +08:00
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' ],
);
2026-08-04 11:33:01 -04:00
assert . deepEqual (
api . thread ( 'sid-search' , { includeInactive : true }). map ( m => [ m . uuid , m . visibility ]),
[
[ 'msg-text' , 'visible' ],
[ 'msg-thinking' , 'visible' ],
[ 'msg-inactive' , 'inactive' ],
],
);
2026-07-08 16:11:12 +08:00
db . close ();
});
2026-08-04 11:33:01 -04:00
test ( 'inactive search is opt-in, orthogonal to meta filtering, and always labeled' , () => {
const db = searchDb ();
const api = createQueryApi ( db );
assert . deepEqual ( api . search ( 'superseded' , { limit : 5 }), []);
const inactive = api . search ( 'superseded' , { includeInactive : true , limit : 5 });
assert . deepEqual ( inactive . map ( row => [ row . message . uuid , row . message . visibility ]), [
[ 'msg-inactive' , 'inactive' ],
]);
assert . equal (
inactive [ 0 ]. context . every ( row => row . visibility === 'visible' || row . visibility === 'inactive' ),
true ,
);
const withMeta = api . search ( 'superseded' , {
includeInactive : true ,
includeMeta : true ,
limit : 5 ,
});
assert . deepEqual (
withMeta . map ( row => [ row . message . uuid , row . message . visibility ]). sort (),
[
[ 'msg-inactive' , 'inactive' ],
[ 'msg-inactive-meta' , 'inactive' ],
],
);
assert . deepEqual ( api . search ( 'abandoned' , { includeInactive : true , includeMeta : true }), []);
db . close ();
});
test ( 'context and trace reject hidden targets and omit hidden ancestors' , () => {
const db = new DatabaseSync ( ':memory:' );
db . exec ( SCHEMA );
db . prepare ( 'INSERT INTO sessions (id,title,source) VALUES (?,?,?)' )
. run ( 'sid-chain' , 'Visibility chain' , 'pi' );
const insert = db . prepare ( `
INSERT INTO messages (
uuid,session_id,type,parent_uuid,role,text,timestamp,visibility,source
) VALUES (?,?,?,?,?,?,?,?,?)
` );
insert . run ( 'visible-root' , 'sid-chain' , 'user' , null , 'user' , 'root' , '2026-08-02T10:00:00Z' , 'visible' , 'pi' );
insert . run ( 'hidden-parent' , 'sid-chain' , 'assistant' , 'visible-root' , 'assistant' , 'secret' , '2026-08-02T10:00:01Z' , 'hidden' , 'pi' );
insert . run ( 'visible-child' , 'sid-chain' , 'user' , 'hidden-parent' , 'user' , 'continue' , '2026-08-02T10:00:02Z' , 'visible' , 'pi' );
insert . run ( 'inactive-child' , 'sid-chain' , 'assistant' , 'visible-root' , 'assistant' , 'superseded' , '2026-08-02T10:00:03Z' , 'inactive' , 'pi' );
const api = createQueryApi ( db );
assert . equal ( api . context ( 'hidden-parent' ), null );
assert . equal ( api . context ( 'hidden-parent' , { includeInactive : true }), null );
assert . deepEqual ( api . trace ( 'hidden-parent' ), []);
assert . deepEqual ( api . trace ( 'hidden-parent' , { includeInactive : true }), []);
assert . equal ( api . context ( 'inactive-child' ), null );
assert . deepEqual ( api . trace ( 'inactive-child' ), []);
assert . deepEqual (
api . context ( 'inactive-child' , { includeInactive : true }). parentChain
. map ( message => [ message . uuid , message . visibility ]),
[[ 'visible-root' , 'visible' ]],
);
assert . deepEqual (
api . trace ( 'inactive-child' , { includeInactive : true })
. map ( message => [ message . uuid , message . visibility ]),
[
[ 'visible-root' , 'visible' ],
[ 'inactive-child' , 'inactive' ],
],
);
assert . deepEqual (
api . context ( 'visible-child' ). parentChain . map ( message => message . uuid ),
[ 'visible-root' ],
);
assert . deepEqual (
api . trace ( 'visible-child' ). map ( message => message . uuid ),
[ 'visible-root' , 'visible-child' ],
);
db . close ();
});
test ( 'raw rejects hidden targets and labels explicitly included inactive evidence' , () => {
const db = searchDb ();
const providerRegistry = {
raw : ({ messageUuid }) => ({
text : `raw: ${ messageUuid } ` ,
totalLength : `raw: ${ messageUuid } ` . length ,
}),
};
const api = createQueryApi ( db , { providerRegistry });
assert . equal ( api . raw ( 'msg-hidden' ), null );
assert . equal ( api . raw ( 'msg-hidden' , { includeInactive : true }), null );
assert . equal ( api . raw ( 'msg-inactive' ), null );
assert . deepEqual (
api . raw ( 'msg-inactive' , { includeInactive : true }),
{
text : 'raw:msg-inactive' ,
totalLength : 16 ,
offset : 0 ,
limit : 10000 ,
hasMore : false ,
visibility : 'inactive' ,
},
);
assert . equal ( api . raw ( 'msg-text' ). text , 'raw:msg-text' );
assert . equal ( api . raw ( 'msg-text' ). visibility , 'visible' );
db . close ();
});
test ( 'failures nextMessages does not leak hidden branch messages' , () => {
const db = new DatabaseSync ( ':memory:' );
db . exec ( SCHEMA );
db . prepare ( 'INSERT INTO sessions (id,title,source) VALUES (?,?,?)' )
. run ( 'sid-failure' , 'Failure branch' , 'pi' );
const insertMessage = db . prepare ( `
INSERT INTO messages (uuid,session_id,type,role,text,timestamp,visibility,source)
VALUES (?,?,?,?,?,?,?,?)
` );
insertMessage . run ( 'failure-result' , 'sid-failure' , 'user' , 'toolResult' , 'failed' , '2026-08-02T10:00:00Z' , 'visible' , 'pi' );
insertMessage . run ( 'hidden-next' , 'sid-failure' , 'assistant' , 'assistant' , 'abandoned' , '2026-08-02T10:00:01Z' , 'hidden' , 'pi' );
insertMessage . run ( 'inactive-next' , 'sid-failure' , 'assistant' , 'assistant' , 'superseded' , '2026-08-02T10:00:02Z' , 'inactive' , 'pi' );
insertMessage . run ( 'visible-next' , 'sid-failure' , 'assistant' , 'assistant' , 'recovered' , '2026-08-02T10:00:03Z' , 'visible' , 'pi' );
db . prepare ( `
INSERT INTO tool_calls (id,message_uuid,session_id,name,input_json)
VALUES (?,?,?,?,?)
` ). run ( 'call-failure' , 'failure-result' , 'sid-failure' , 'read' , '{}' );
db . prepare ( `
INSERT INTO tool_results (tool_use_id,message_uuid,session_id,content,is_error)
VALUES (?,?,?,?,?)
` ). run ( 'call-failure' , 'failure-result' , 'sid-failure' , 'failed' , 1 );
const row = createQueryApi ( db ). failures ( 'sid-failure' )[ 0 ];
assert . deepEqual ( row . nextMessages . map ( message => message . uuid ), [ 'visible-next' ]);
assert . equal ( row . visibility , 'visible' );
assert . deepEqual (
createQueryApi ( db ). failures ({ sessionId : 'sid-failure' , includeInactive : true })[ 0 ]
. nextMessages . map ( message => [ message . uuid , message . visibility ]),
[
[ 'inactive-next' , 'inactive' ],
[ 'visible-next' , 'visible' ],
],
);
db . close ();
});
test ( 'failures gates both result and linked call message visibility' , () => {
const db = new DatabaseSync ( ':memory:' );
db . exec ( SCHEMA );
db . prepare ( 'INSERT INTO sessions (id,title,source) VALUES (?,?,?)' )
. run ( 'sid-edge-visibility' , 'Tool edge visibility' , 'pi' );
const insertMessage = db . prepare ( `
INSERT INTO messages (uuid,session_id,type,role,text,timestamp,visibility,source)
VALUES (?,?,?,?,?,?,?,?)
` );
const insertCall = db . prepare ( `
INSERT INTO tool_calls (id,message_uuid,session_id,name,input_json,file_path)
VALUES (?,?,?,?,?,?)
` );
const insertResult = db . prepare ( `
INSERT INTO tool_results (tool_use_id,message_uuid,session_id,content,is_error)
VALUES (?,?,?,?,?)
` );
for ( const [ index , callVisibility ] of [ 'visible' , 'inactive' , 'hidden' ]. entries ()) {
const callId = `call- ${ callVisibility } ` ;
insertMessage . run (
`message- ${ callVisibility } ` ,
'sid-edge-visibility' ,
'assistant' ,
'assistant' ,
null ,
`2026-08-02T10:00:0 ${ index * 2 } Z` ,
callVisibility ,
'pi' ,
);
insertMessage . run (
`result- ${ callVisibility } ` ,
'sid-edge-visibility' ,
'user' ,
'toolResult' ,
`failed- ${ callVisibility } ` ,
`2026-08-02T10:00:0 ${ index * 2 + 1 } Z` ,
'visible' ,
'pi' ,
);
insertCall . run (
callId ,
`message- ${ callVisibility } ` ,
'sid-edge-visibility' ,
'read' ,
JSON . stringify ({ path : `/ ${ callVisibility } ` }),
`/ ${ callVisibility } ` ,
);
insertResult . run (
callId ,
`result- ${ callVisibility } ` ,
'sid-edge-visibility' ,
`failed- ${ callVisibility } ` ,
1 ,
);
}
const api = createQueryApi ( db );
assert . deepEqual (
api . failures ( 'sid-edge-visibility' ). map ( record => record . toolCall . id ),
[ 'call-visible' ],
);
assert . deepEqual (
api . failures ({ sessionId : 'sid-edge-visibility' , includeInactive : true })
. map ( record => record . toolCall . id )
. sort (),
[ 'call-inactive' , 'call-visible' ],
);
db . close ();
});
test ( 'summaries, file history, and failures expose inactive rows only on request' , () => {
const db = new DatabaseSync ( ':memory:' );
db . exec ( SCHEMA );
db . prepare ( 'INSERT INTO sessions (id,title,source) VALUES (?,?,?)' )
. run ( 'sid-structured' , 'Structured visibility' , 'pi' );
const insertMessage = db . prepare ( `
INSERT INTO messages (uuid,session_id,type,role,text,timestamp,visibility,source)
VALUES (?,?,?,?,?,?,?,?)
` );
const insertCall = db . prepare ( `
INSERT INTO tool_calls (id,message_uuid,session_id,name,input_json,file_path)
VALUES (?,?,?,?,?,?)
` );
const insertResult = db . prepare ( `
INSERT INTO tool_results (tool_use_id,message_uuid,session_id,content,is_error)
VALUES (?,?,?,?,?)
` );
const insertSummary = db . prepare ( `
INSERT INTO summaries (id,session_id,timestamp,source,content,visibility)
VALUES (?,?,?,?,?,?)
` );
for ( const [ index , visibility ] of [ 'visible' , 'inactive' , 'hidden' ]. entries ()) {
const suffix = visibility ;
const uuid = `message- ${ suffix } ` ;
const callId = `call- ${ suffix } ` ;
const timestamp = `2026-08-02T10:00:0 ${ index } Z` ;
insertMessage . run ( uuid , 'sid-structured' , 'user' , 'toolResult' , suffix , timestamp , visibility , 'pi' );
insertCall . run ( callId , uuid , 'sid-structured' , 'read' , '{}' , '/tmp/visibility.ts' );
insertResult . run ( callId , uuid , 'sid-structured' , `failed- ${ suffix } ` , 1 );
insertSummary . run ( `summary- ${ suffix } ` , 'sid-structured' , timestamp , 'pi:branch_summary' , suffix , visibility );
}
const api = createQueryApi ( db );
assert . deepEqual ( api . fileHistory ( '/tmp/visibility.ts' ). map ( row => row . visibility ), [ 'visible' ]);
assert . deepEqual (
api . fileHistory ( '/tmp/visibility.ts' , { includeInactive : true }). map ( row => row . visibility ),
[ 'visible' , 'inactive' ],
);
assert . deepEqual ( api . failures ( 'sid-structured' ). map ( row => row . visibility ), [ 'visible' ]);
assert . deepEqual (
api . failures ({ sessionId : 'sid-structured' , includeInactive : true })
. map ( row => [ row . visibility , row . result . visibility ]),
[
[ 'inactive' , 'inactive' ],
[ 'visible' , 'visible' ],
],
);
assert . deepEqual ( api . summaries ( 'sid-structured' ). map ( row => row . visibility ), [ 'visible' ]);
assert . deepEqual (
api . summaries ({ sessionId : 'sid-structured' , includeInactive : true })
. map ( row => row . visibility ),
[ 'inactive' , 'visible' ],
);
db . close ();
});
2026-07-08 16:11:12 +08:00
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 ();
});