Pi cannot be read as another linear JSONL stream. Its history is a tree with a durable leaf, orphan roots, branch summaries, and two compaction forms, so the active context is something the format states rather than something line order implies. The adapter keeps those semantics inside itself and projects the result into the existing canonical tables. Sessions are keyed by (normalized header cwd, header id) rather than by path, because Pi's --session-id lookup is project-local: two projects may reuse an id, while a move or an identical copy is still one session. Discovery covers both layouts Pi writes and fingerprints each file by mtime, ctime, size and inode, so a rewrite that preserves mtime is not read as unchanged. Abandoned branches are preserved rather than dropped. Visibility becomes three-state -- visible, inactive, hidden -- and helpers return only visible rows until includeInactive asks for the superseded path, labeling every row so a caller knows which it holds. Usage counts all three, because an abandoned call still spent tokens; message_count reports only the visible transcript. A committed MIT-licensed oracle transcribed from Pi 0.83.0 pins the context algorithms, and a fixed-seed differential runs 512 generated sessions against it on every test run. Schema changes are additive.
253 lines
8.5 KiB
JavaScript
253 lines
8.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
|
|
import { assembleSessionDetail } from '../packages/core/src/session-detail.ts';
|
|
import { persist } from '../packages/core/src/persist.ts';
|
|
import { parse as parseCodex } from '../packages/core/src/providers/codex.ts';
|
|
|
|
const SCHEMA = readFileSync(new URL('../packages/core/src/schema.sql', import.meta.url), 'utf8');
|
|
|
|
function writeCodexFixture(lines) {
|
|
const dir = mkdtempSync(join(tmpdir(), 'obelisk-provider-detail-'));
|
|
const path = join(dir, 'rollout.jsonl');
|
|
writeFileSync(path, `${lines.map(line => JSON.stringify(line)).join('\n')}\n`);
|
|
return path;
|
|
}
|
|
|
|
test('a provider record stream assembles directly into session detail', () => {
|
|
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a317d';
|
|
const path = writeCodexFixture([
|
|
{
|
|
type: 'session_meta',
|
|
timestamp: '2026-06-10T10:00:00Z',
|
|
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
|
|
},
|
|
{
|
|
type: 'event_msg',
|
|
timestamp: '2026-06-10T10:00:01Z',
|
|
payload: { type: 'user_message', message: 'inspect the repository' },
|
|
},
|
|
{
|
|
type: 'event_msg',
|
|
timestamp: '2026-06-10T10:00:02Z',
|
|
payload: { type: 'agent_message', message: 'I will inspect it.' },
|
|
},
|
|
{
|
|
type: 'response_item',
|
|
timestamp: '2026-06-10T10:00:03Z',
|
|
payload: { type: 'function_call', call_id: 'call_1', name: 'shell', arguments: '{"cmd":"ls"}' },
|
|
},
|
|
{
|
|
type: 'response_item',
|
|
timestamp: '2026-06-10T10:00:04Z',
|
|
payload: { type: 'function_call_output', call_id: 'call_1', output: 'package.json' },
|
|
},
|
|
]);
|
|
|
|
const records = [...parseCodex({ key: path, sessionId: '' }, null)];
|
|
const detail = assembleSessionDetail(records);
|
|
|
|
assert.deepEqual(detail.messages.map(message => message.text), [
|
|
'inspect the repository',
|
|
'I will inspect it.',
|
|
]);
|
|
assert.equal(detail.messages[1].tool_calls?.[0].name, 'shell');
|
|
assert.equal(detail.messages[1].tool_calls?.[0].result?.content, 'package.json');
|
|
|
|
const db = new DatabaseSync(':memory:');
|
|
db.exec(SCHEMA);
|
|
persist(db, { key: path, sessionId: '' }, parseCodex({ key: path, sessionId: '' }, null));
|
|
const persistedDetail = assembleSessionDetail({
|
|
session: db.prepare('SELECT * FROM sessions').get(),
|
|
messages: db.prepare('SELECT * FROM messages ORDER BY timestamp, uuid').all(),
|
|
toolCalls: db.prepare('SELECT * FROM tool_calls').all(),
|
|
toolResults: db.prepare('SELECT * FROM tool_results').all(),
|
|
});
|
|
assert.deepEqual(persistedDetail, detail);
|
|
db.close();
|
|
});
|
|
|
|
test('Codex token_count without usage remains persistable', () => {
|
|
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a3180';
|
|
const path = writeCodexFixture([
|
|
{
|
|
type: 'session_meta',
|
|
timestamp: '2026-07-14T12:21:21.000Z',
|
|
payload: { id: threadId, cwd: '/tmp/demo', timestamp: '2026-07-14T12:21:21.000Z' },
|
|
},
|
|
{
|
|
type: 'event_msg',
|
|
timestamp: '2026-07-14T12:21:30.000Z',
|
|
payload: { type: 'agent_message', message: 'hello' },
|
|
},
|
|
{
|
|
type: 'event_msg',
|
|
timestamp: '2026-07-14T12:21:31.000Z',
|
|
payload: { type: 'token_count', info: null },
|
|
},
|
|
]);
|
|
const db = new DatabaseSync(':memory:');
|
|
db.exec(SCHEMA);
|
|
|
|
assert.doesNotThrow(() => {
|
|
persist(db, { key: path, sessionId: `codex:${threadId}` }, parseCodex({
|
|
key: path,
|
|
sessionId: `codex:${threadId}`,
|
|
}, null));
|
|
});
|
|
const usage = db.prepare(
|
|
'SELECT input_tokens, output_tokens FROM messages WHERE text = ?',
|
|
).get('hello');
|
|
assert.equal(usage.input_tokens, null);
|
|
assert.equal(usage.output_tokens, null);
|
|
db.close();
|
|
});
|
|
|
|
test('provider-classified hidden context never reaches session detail', () => {
|
|
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a317e';
|
|
const path = writeCodexFixture([
|
|
{
|
|
type: 'session_meta',
|
|
timestamp: '2026-06-10T10:00:00Z',
|
|
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
|
|
},
|
|
{
|
|
type: 'response_item',
|
|
timestamp: '2026-06-10T10:00:01Z',
|
|
payload: {
|
|
type: 'message',
|
|
role: 'user',
|
|
content: [{ type: 'input_text', text: '<environment_context>\n <cwd>/proj</cwd>\n</environment_context>' }],
|
|
},
|
|
},
|
|
{
|
|
type: 'response_item',
|
|
timestamp: '2026-06-10T10:00:02Z',
|
|
payload: {
|
|
type: 'message',
|
|
role: 'user',
|
|
content: [{ type: 'input_text', text: '<codex_internal_context source="goal">\nsecret state\n</codex_internal_context>' }],
|
|
},
|
|
},
|
|
{
|
|
type: 'event_msg',
|
|
timestamp: '2026-06-10T10:00:03Z',
|
|
payload: { type: 'user_message', message: 'show the actual request' },
|
|
},
|
|
]);
|
|
|
|
const records = [...parseCodex({ key: path, sessionId: '' }, null)];
|
|
const detail = assembleSessionDetail(records);
|
|
|
|
assert.deepEqual(detail.messages.map(message => message.text), ['show the actual request']);
|
|
assert.equal(
|
|
records.filter(record => record.kind === 'message' && record.visibility === 'hidden').length,
|
|
2,
|
|
);
|
|
assert.equal(
|
|
records.find(record => record.kind === 'session').message_count,
|
|
1,
|
|
);
|
|
});
|
|
|
|
test('provider normalization removes only structural image wrappers before deduplication', () => {
|
|
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a317f';
|
|
const path = writeCodexFixture([
|
|
{
|
|
type: 'session_meta',
|
|
timestamp: '2026-06-10T10:00:00Z',
|
|
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
|
|
},
|
|
{
|
|
type: 'event_msg',
|
|
timestamp: '2026-06-10T10:00:01Z',
|
|
payload: { type: 'user_message', message: 'look at this screenshot' },
|
|
},
|
|
{
|
|
type: 'response_item',
|
|
timestamp: '2026-06-10T10:00:01Z',
|
|
payload: {
|
|
type: 'message',
|
|
role: 'user',
|
|
content: [
|
|
{ type: 'input_text', text: 'look at this screenshot' },
|
|
{ type: 'input_text', text: '<image>' },
|
|
{ type: 'input_image', image_url: 'data:image/png;base64,AAAA' },
|
|
{ type: 'input_text', text: '</image>' },
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
|
|
const records = [...parseCodex({ key: path, sessionId: '' }, null)];
|
|
const detail = assembleSessionDetail(records);
|
|
|
|
assert.deepEqual(detail.messages.map(message => message.text), ['look at this screenshot']);
|
|
});
|
|
|
|
test('canonical visibility survives persistence before row-based assembly', () => {
|
|
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a3180';
|
|
const path = writeCodexFixture([
|
|
{
|
|
type: 'session_meta',
|
|
timestamp: '2026-06-10T10:00:00Z',
|
|
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
|
|
},
|
|
{
|
|
type: 'response_item',
|
|
timestamp: '2026-06-10T10:00:01Z',
|
|
payload: {
|
|
type: 'message',
|
|
role: 'user',
|
|
content: [{ type: 'input_text', text: '<environment_context>hidden</environment_context>' }],
|
|
},
|
|
},
|
|
{
|
|
type: 'event_msg',
|
|
timestamp: '2026-06-10T10:00:02Z',
|
|
payload: { type: 'user_message', message: 'visible request' },
|
|
},
|
|
]);
|
|
const db = new DatabaseSync(':memory:');
|
|
db.exec(SCHEMA);
|
|
|
|
persist(db, { key: path, sessionId: '' }, parseCodex({ key: path, sessionId: '' }, null));
|
|
const messages = db.prepare('SELECT * FROM messages ORDER BY timestamp, uuid').all();
|
|
const assembled = assembleSessionDetail({ messages }).messages;
|
|
|
|
assert.equal(messages[0].visibility, 'hidden');
|
|
assert.deepEqual(assembled.map(message => message.text), ['visible request']);
|
|
db.close();
|
|
});
|
|
|
|
test('provider normalization classifies Skill instructions before assembly', () => {
|
|
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a3181';
|
|
const path = writeCodexFixture([
|
|
{
|
|
type: 'session_meta',
|
|
timestamp: '2026-06-10T10:00:00Z',
|
|
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
|
|
},
|
|
{
|
|
type: 'response_item',
|
|
timestamp: '2026-06-10T10:00:01Z',
|
|
payload: {
|
|
type: 'message',
|
|
role: 'user',
|
|
content: [{ type: 'input_text', text: 'Base directory for this skill: /tmp/skill\n# Instructions' }],
|
|
},
|
|
},
|
|
]);
|
|
|
|
const records = [...parseCodex({ key: path, sessionId: '' }, null)];
|
|
const message = records.find(record => record.kind === 'message');
|
|
|
|
assert.equal(message.content_type, 'skill_instructions');
|
|
assert.equal(message.is_meta, 1);
|
|
assert.equal(message.visibility, 'visible');
|
|
});
|