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'; import { createQueryApi, createAttuneApi } from '../packages/core/src/query.ts'; const require = createRequire(import.meta.url); const { DatabaseSync } = require('node:sqlite'); const SCHEMA = readFileSync(new URL('../packages/core/src/schema.sql', import.meta.url), 'utf8'); 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, visibility TEXT DEFAULT 'visible', 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, visibility) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); 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', '/exit', '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'); db.exec("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')"); return db; } 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(); }); 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.search('abandoned', { includeMeta: true, limit: 5 }), []); 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'], ); assert.deepEqual( api.thread('sid-search', { includeInactive: true }).map(m => [m.uuid, m.visibility]), [ ['msg-text', 'visible'], ['msg-thinking', 'visible'], ['msg-inactive', 'inactive'], ], ); db.close(); }); 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(); }); 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(); });