chore: sanitize tests, add CONTEXT.md + ADRs, track tests/docs
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
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 '../scripts/query.mjs';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const SCHEMA = readFileSync(new URL('../scripts/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 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();
|
||||
});
|
||||
Reference in New Issue
Block a user