Files
obelisk/tests/query.test.mjs
T

311 lines
12 KiB
JavaScript

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.mjs';
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, 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;
}
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.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();
});