Files
obelisk/tests/runtime.test.mjs
T

453 lines
18 KiB
JavaScript
Raw Normal View History

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { mkdtempSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, normalize } from 'node:path';
import { runCli as runRuntime } from './cli-test-helpers.mjs';
const require = createRequire(import.meta.url);
const { DatabaseSync } = require('node:sqlite');
function tempHome() {
const home = mkdtempSync(join(tmpdir(), 'obelisk-runtime-home-'));
mkdirSync(join(home, '.claude'), { recursive: true });
return home;
}
test('runtime query scripts cannot call attune helpers', () => {
const home = tempHome();
const scriptPath = join(home, 'query.mjs');
writeFileSync(scriptPath, 'return { rememberType: typeof remember, forgetType: typeof forget, overviewType: typeof overview };');
const result = runRuntime(['--query', scriptPath], { home });
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.deepEqual(JSON.parse(result.stdout), {
rememberType: 'undefined',
forgetType: 'undefined',
overviewType: 'function',
});
});
test('runtime attune scripts expose only memory mutation helpers', () => {
const home = tempHome();
const memoryPath = join(home, 'memory.md');
const scriptPath = join(home, 'attune.mjs');
writeFileSync(memoryPath, '# Memory\n');
writeFileSync(scriptPath, `
return {
rememberType: typeof remember,
forgetType: typeof forget,
searchType: typeof search,
sqlType: typeof sql,
overviewType: typeof overview,
result: remember({
path: ${JSON.stringify(memoryPath)},
project: 'runtime-test',
summary: 'Decision: runtime remember exposes only memory registration.'
})
};
`);
const result = runRuntime(['--attune', scriptPath], { home });
assert.equal(result.status, 0, result.stderr || result.stdout);
const payload = JSON.parse(result.stdout);
assert.equal(payload.rememberType, 'function');
assert.equal(payload.forgetType, 'function');
assert.equal(payload.searchType, 'undefined');
assert.equal(payload.sqlType, 'undefined');
assert.equal(payload.overviewType, 'undefined');
assert.equal(payload.result.path, memoryPath);
});
test('runtime rejects removed remember mode', () => {
const home = tempHome();
const scriptPath = join(home, 'remember.mjs');
writeFileSync(scriptPath, 'return { ok: true };');
const result = runRuntime(['--remember', scriptPath], { home });
assert.notEqual(result.status, 0);
assert.equal(result.stdout, '');
assert.match(result.stderr, /--attune <file\.js>/);
});
test('runtime migrates old memories schema before recall', () => {
const home = tempHome();
const dbPath = join(home, '.claude', 'obelisk.sqlite');
const db = new DatabaseSync(dbPath);
db.exec(`
CREATE TABLE memories (
id TEXT PRIMARY KEY, session_id TEXT, project TEXT,
message_start TEXT, message_end TEXT,
path TEXT, summary TEXT, created_at TEXT
);
INSERT INTO memories (id, project, path, summary, created_at)
VALUES ('mem-old', 'legacy-project', '/tmp/old.md', 'Decision: keep legacy memory rows readable.', '2026-06-10T12:00:00Z');
`);
db.close();
const scriptPath = join(home, 'query.mjs');
writeFileSync(scriptPath, `
return memories({ project: "%legacy-project%", query: "legacy memory", limit: 5 })
.map(m => ({ id: m.id, rankType: typeof m.rank }));
`);
const result = runRuntime(['--query', scriptPath], { home });
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.deepEqual(JSON.parse(result.stdout), [{ id: 'mem-old', rankType: 'number' }]);
});
test('runtime indexes Codex root sessions into the shared query helpers', () => {
const home = tempHome();
const codexSessionDir = join(home, '.codex', 'sessions', '2026', '06', '15');
mkdirSync(codexSessionDir, { recursive: true });
const codexId = '019ec6ee-cebd-7431-9c93-ceec89a98a5f';
writeFileSync(join(codexSessionDir, `rollout-2026-06-15T00-19-59-${codexId}.jsonl`), [
JSON.stringify({
timestamp: '2026-06-14T16:19:59.842Z',
type: 'session_meta',
payload: {
id: codexId,
timestamp: '2026-06-14T16:19:59.842Z',
cwd: '/tmp/obelisk-runtime',
cli_version: '0.135.0-alpha.1',
source: 'vscode',
git: { branch: 'feat/codex' },
},
}),
JSON.stringify({
timestamp: '2026-06-14T16:20:00.000Z',
type: 'event_msg',
payload: { type: 'user_message', message: 'codex user asks for runtime indexing', images: [], local_images: [], text_elements: [] },
}),
JSON.stringify({
timestamp: '2026-06-14T16:20:01.000Z',
type: 'response_item',
payload: {
type: 'message',
role: 'developer',
content: [{ type: 'input_text', text: 'developer replay should stay out of visible search' }],
},
}),
JSON.stringify({
timestamp: '2026-06-14T16:20:02.000Z',
type: 'event_msg',
payload: { type: 'agent_message', phase: 'final_answer', message: 'codex assistant replies from runtime' },
}),
JSON.stringify({
timestamp: '2026-06-14T16:20:03.000Z',
type: 'response_item',
payload: { type: 'function_call', call_id: 'call_codex_1', name: 'exec_command', arguments: '{"cmd":"pwd"}' },
}),
JSON.stringify({
timestamp: '2026-06-14T16:20:04.000Z',
type: 'response_item',
payload: { type: 'function_call_output', call_id: 'call_codex_1', output: '/tmp/obelisk-runtime' },
}),
'',
].join('\n'));
const scriptPath = join(home, 'query.mjs');
writeFileSync(scriptPath, `
const sid = ${JSON.stringify(`codex:${codexId}`)};
return {
sessions: sessions({ source: 'codex', limit: 5 }).map(s => ({
id: s.id,
source: s.source,
project: s.project,
project_path: s.project_path,
git_branch: s.git_branch,
version: s.version,
message_count: s.message_count
})),
messages: thread(sid).map(m => ({ role: m.role, text: m.text, source: m.source, content_type: m.content_type })),
search: search('runtime indexing', { source: 'codex', limit: 5 }).map(h => ({
uuid: h.message.uuid,
message_source: h.message.source,
session_source: h.session.source
})),
developerReplay: search('developer replay', { source: 'codex', limit: 5 }).length,
rawHasEventLine: raw(${JSON.stringify(`codex:${codexId}:000002`)}, { limit: 1000 })?.text.includes('codex user asks for runtime indexing') || false,
tool: sql('SELECT id, message_uuid, session_id, name FROM tool_calls WHERE id=?', ${JSON.stringify(`codex:${codexId}:call_codex_1`)})[0],
toolResult: sql('SELECT tool_use_id, message_uuid, session_id, content FROM tool_results WHERE tool_use_id=?', ${JSON.stringify(`codex:${codexId}:call_codex_1`)})[0],
overviewSources: overview({ limit: 5 }).totals.sources
};
`);
const result = runRuntime(['--query', scriptPath], { home });
assert.equal(result.status, 0, result.stderr || result.stdout);
const payload = JSON.parse(result.stdout);
assert.deepEqual(payload.sessions, [{
id: `codex:${codexId}`,
source: 'codex',
project: '-tmp-obelisk-runtime',
project_path: normalize('/tmp/obelisk-runtime'),
git_branch: 'feat/codex',
version: '0.135.0-alpha.1',
message_count: 3,
}]);
assert.deepEqual(payload.messages.map(m => [m.role, m.text, m.source, m.content_type]), [
['user', 'codex user asks for runtime indexing', 'codex', 'text'],
['assistant', 'codex assistant replies from runtime', 'codex', 'text'],
['assistant', null, 'codex', 'tool_use'],
]);
assert.equal(payload.search[0].message_source, 'codex');
assert.equal(payload.search[0].session_source, 'codex');
assert.equal(payload.developerReplay, 0);
assert.equal(payload.rawHasEventLine, true);
assert.equal(payload.tool.session_id, `codex:${codexId}`);
assert.equal(payload.tool.message_uuid, `codex:${codexId}:000005`);
assert.equal(payload.toolResult.message_uuid, `codex:${codexId}:000005`);
assert.equal(payload.toolResult.content, '/tmp/obelisk-runtime');
assert.ok(payload.overviewSources.some(s => s.source === 'codex' && s.session_count === 1));
});
test('runtime skips Codex guardian review threads', () => {
const home = tempHome();
const codexSessionDir = join(home, '.codex', 'sessions', '2026', '06', '15');
mkdirSync(codexSessionDir, { recursive: true });
const guardianId = '019ed5c4-8d52-7bc0-91f3-447a15e987d1';
writeFileSync(join(codexSessionDir, `rollout-2026-06-15T02-12-00-${guardianId}.jsonl`), [
JSON.stringify({
timestamp: '2026-06-14T18:12:00.000Z',
type: 'session_meta',
payload: {
id: guardianId,
timestamp: '2026-06-14T18:12:00.000Z',
cwd: '/tmp/obelisk-runtime',
cli_version: '0.135.0-alpha.1',
thread_source: 'subagent',
source: { subagent: { other: 'guardian' } },
},
}),
JSON.stringify({
timestamp: '2026-06-14T18:12:01.000Z',
type: 'turn_context',
payload: { cwd: '/tmp/obelisk-runtime', model: 'codex-auto-review' },
}),
JSON.stringify({
timestamp: '2026-06-14T18:12:02.000Z',
type: 'event_msg',
payload: { type: 'user_message', message: 'approval guardian prompt', images: [], local_images: [], text_elements: [] },
}),
JSON.stringify({
timestamp: '2026-06-14T18:12:03.000Z',
type: 'event_msg',
payload: { type: 'agent_message', phase: 'final_answer', message: '{"outcome":"allow"}' },
}),
'',
].join('\n'));
const scriptPath = join(home, 'query.mjs');
writeFileSync(scriptPath, `
const sid = ${JSON.stringify(`codex:${guardianId}`)};
return {
sessions: sessions({ source: 'codex', limit: 5 }).map(s => s.id),
searchCount: search('approval', { source: 'codex', limit: 5 }).length,
sessionRows: sql('SELECT COUNT(*) AS c FROM sessions WHERE id=?', sid)[0].c,
messageRows: sql('SELECT COUNT(*) AS c FROM messages WHERE session_id=?', sid)[0].c,
subagentRows: sql('SELECT COUNT(*) AS c FROM subagents WHERE agent_id=?', sid)[0].c
};
`);
const result = runRuntime(['--query', scriptPath], { home });
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.deepEqual(JSON.parse(result.stdout), {
sessions: [],
searchCount: 0,
sessionRows: 0,
messageRows: 0,
subagentRows: 0,
});
});
test('runtime removes stale Codex guardian rows when the JSONL was already indexed', () => {
const home = tempHome();
const codexSessionDir = join(home, '.codex', 'sessions', '2026', '06', '15');
mkdirSync(codexSessionDir, { recursive: true });
const initScriptPath = join(home, 'init.mjs');
writeFileSync(initScriptPath, 'return sessions({ source: "codex", limit: 5 }).length;');
assert.equal(runRuntime(['--query', initScriptPath], { home }).status, 0);
const guardianId = '019ed5c4-8d52-7bc0-91f3-447a15e987d1';
const guardianSessionId = `codex:${guardianId}`;
const jsonlPath = join(codexSessionDir, `rollout-2026-06-15T02-12-00-${guardianId}.jsonl`);
writeFileSync(jsonlPath, [
JSON.stringify({
timestamp: '2026-06-14T18:12:00.000Z',
type: 'session_meta',
payload: {
id: guardianId,
timestamp: '2026-06-14T18:12:00.000Z',
cwd: '/tmp/obelisk-runtime',
cli_version: '0.135.0-alpha.1',
thread_source: 'subagent',
source: { subagent: { other: 'guardian' } },
},
}),
JSON.stringify({
timestamp: '2026-06-14T18:12:01.000Z',
type: 'event_msg',
payload: { type: 'user_message', message: 'stale approval guardian prompt', images: [], local_images: [], text_elements: [] },
}),
'',
].join('\n'));
const db = new DatabaseSync(join(home, '.obelisk', 'obelisk.sqlite'));
db.prepare('INSERT INTO sessions (id,jsonl_path,source,message_count) VALUES (?,?,?,?)').run(guardianSessionId, jsonlPath, 'codex', 1);
db.prepare('INSERT INTO messages (uuid,session_id,type,timestamp,role,text,content_type,source) VALUES (?,?,?,?,?,?,?,?)')
.run(`${guardianSessionId}:000002`, guardianSessionId, 'user', '2026-06-14T18:12:01.000Z', 'user', 'stale approval guardian prompt', 'text', 'codex');
db.prepare('INSERT INTO tool_calls (id,message_uuid,session_id,name,input_json,file_path) VALUES (?,?,?,?,?,?)')
.run('codex:call_guardian', `${guardianSessionId}:000002`, guardianSessionId, 'exec_command', '{}', null);
db.prepare('INSERT INTO tool_results (tool_use_id,message_uuid,session_id,content,file_path,is_error) VALUES (?,?,?,?,?,?)')
.run('codex:call_guardian', `${guardianSessionId}:000002`, guardianSessionId, 'ok', null, 0);
db.prepare('INSERT INTO subagents (agent_id,session_id) VALUES (?,?)').run(guardianSessionId, guardianSessionId);
db.prepare('INSERT OR REPLACE INTO index_state (jsonl_path,mtime,lines_processed) VALUES (?,?,?)')
.run(jsonlPath, statSync(jsonlPath).mtimeMs, 2);
db.prepare("UPDATE index_state SET mtime=? WHERE jsonl_path='__last_build__'").run(Date.now() - 31000);
db.close();
const scriptPath = join(home, 'query.mjs');
writeFileSync(scriptPath, `
const sid = ${JSON.stringify(guardianSessionId)};
return {
sessions: sessions({ source: 'codex', limit: 5 }).map(s => s.id),
searchCount: search('stale', { source: 'codex', limit: 5 }).length,
sessionRows: sql('SELECT COUNT(*) AS c FROM sessions WHERE id=?', sid)[0].c,
messageRows: sql('SELECT COUNT(*) AS c FROM messages WHERE session_id=?', sid)[0].c,
toolRows: sql('SELECT COUNT(*) AS c FROM tool_calls WHERE session_id=?', sid)[0].c,
resultRows: sql('SELECT COUNT(*) AS c FROM tool_results WHERE session_id=?', sid)[0].c,
subagentRows: sql('SELECT COUNT(*) AS c FROM subagents WHERE agent_id=?', sid)[0].c
};
`);
const result = runRuntime(['--query', scriptPath], { home });
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.deepEqual(JSON.parse(result.stdout), {
sessions: [],
searchCount: 0,
sessionRows: 0,
messageRows: 0,
toolRows: 0,
resultRows: 0,
subagentRows: 0,
});
});
test('runtime maps Codex child threads onto subagents', () => {
const home = tempHome();
const codexSessionDir = join(home, '.codex', 'sessions', '2026', '06', '15');
mkdirSync(codexSessionDir, { recursive: true });
const parentId = '019ec6ee-cebd-7431-9c93-ceec89a98a5f';
const childId = '019ec739-9f75-7a02-ba2a-371986e23823';
writeFileSync(join(codexSessionDir, `rollout-2026-06-15T00-19-59-${parentId}.jsonl`), [
JSON.stringify({
timestamp: '2026-06-14T16:19:59.842Z',
type: 'session_meta',
payload: {
id: parentId,
timestamp: '2026-06-14T16:19:59.842Z',
cwd: '/tmp/obelisk-runtime',
cli_version: '0.135.0-alpha.1',
source: 'vscode',
},
}),
JSON.stringify({
timestamp: '2026-06-14T16:20:00.000Z',
type: 'event_msg',
payload: {
type: 'collab_agent_spawn_end',
call_id: 'call_spawn_1',
sender_thread_id: parentId,
new_thread_id: childId,
new_agent_nickname: 'Plato',
new_agent_role: 'worker',
prompt: 'inspect skill-side codex indexing',
},
}),
'',
].join('\n'));
writeFileSync(join(codexSessionDir, `rollout-2026-06-15T01-41-42-${childId}.jsonl`), [
JSON.stringify({
timestamp: '2026-06-14T17:41:42.924Z',
type: 'session_meta',
payload: {
id: childId,
timestamp: '2026-06-14T17:41:42.924Z',
cwd: '/tmp/obelisk-runtime',
cli_version: '0.135.0-alpha.1',
source: {
subagent: {
thread_spawn: {
parent_thread_id: parentId,
agent_nickname: 'Plato',
agent_role: 'worker',
},
},
},
agent_nickname: 'Plato',
agent_role: 'worker',
},
}),
JSON.stringify({
timestamp: '2026-06-14T17:41:43.000Z',
type: 'event_msg',
payload: { type: 'user_message', message: 'subagent prompt', images: [], local_images: [], text_elements: [] },
}),
JSON.stringify({
timestamp: '2026-06-14T17:41:44.000Z',
type: 'event_msg',
payload: { type: 'agent_message', phase: 'final_answer', message: 'subagent answer' },
}),
'',
].join('\n'));
const scriptPath = join(home, 'query.mjs');
writeFileSync(scriptPath, `
return {
parentSessions: sessions({ source: 'codex', limit: 5 }).map(s => s.id),
subagents: subagents({ source: 'codex', limit: 5 }).map(sa => ({
agent_id: sa.agent_id,
session_id: sa.session_id,
parent_tool_use_id: sa.parent_tool_use_id,
agent_type: sa.agent_type,
description: sa.description,
messageCount: sa.messageCount
})),
childMessages: sql(
'SELECT session_id, agent_id, is_sidechain, source, text FROM messages WHERE agent_id=? ORDER BY timestamp, uuid',
${JSON.stringify(`codex:${childId}`)}
)
};
`);
const result = runRuntime(['--query', scriptPath], { home });
assert.equal(result.status, 0, result.stderr || result.stdout);
const payload = JSON.parse(result.stdout);
assert.deepEqual(payload.parentSessions, [`codex:${parentId}`]);
assert.deepEqual(payload.subagents, [{
agent_id: `codex:${childId}`,
session_id: `codex:${parentId}`,
parent_tool_use_id: `codex:${parentId}:call_spawn_1`,
agent_type: 'worker',
description: 'Plato',
messageCount: 2,
}]);
assert.deepEqual(payload.childMessages.map(m => [m.session_id, m.agent_id, m.is_sidechain, m.source, m.text]), [
[`codex:${parentId}`, `codex:${childId}`, 1, 'codex', 'subagent prompt'],
[`codex:${parentId}`, `codex:${childId}`, 1, 'codex', 'subagent answer'],
]);
});