204 lines
8.3 KiB
JavaScript
204 lines
8.3 KiB
JavaScript
import { test } from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||
|
|
import { tmpdir } from 'node:os';
|
||
|
|
import { dirname, join } from 'node:path';
|
||
|
|
import { spawnSync } from 'node:child_process';
|
||
|
|
import { pathToFileURL } from 'node:url';
|
||
|
|
import { DatabaseSync } from 'node:sqlite';
|
||
|
|
|
||
|
|
import { piSessionId } from '../packages/core/src/providers/pi.ts';
|
||
|
|
import { runCli } from './cli-test-helpers.mjs';
|
||
|
|
|
||
|
|
test('passive-pull runtime indexes Pi sessions from the default home', () => {
|
||
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-pi-runtime-'));
|
||
|
|
const sessionPath = join(
|
||
|
|
home,
|
||
|
|
'.pi',
|
||
|
|
'agent',
|
||
|
|
'sessions',
|
||
|
|
'--tmp-pi-project--',
|
||
|
|
'runtime.jsonl',
|
||
|
|
);
|
||
|
|
mkdirSync(dirname(sessionPath), { recursive: true });
|
||
|
|
writeFileSync(
|
||
|
|
sessionPath,
|
||
|
|
readFileSync(new URL('./fixtures/pi/tool-session.jsonl', import.meta.url)),
|
||
|
|
);
|
||
|
|
|
||
|
|
const coreUrl = pathToFileURL(join(process.cwd(), 'packages/core/src/core.ts')).href;
|
||
|
|
const script = `
|
||
|
|
import { executeQuery } from ${JSON.stringify(coreUrl)};
|
||
|
|
const result = await executeQuery("return sessions({ source: 'pi', limit: 5 });");
|
||
|
|
process.stdout.write(JSON.stringify(result));
|
||
|
|
`;
|
||
|
|
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
||
|
|
delete env.PI_CODING_AGENT_DIR;
|
||
|
|
delete env.PI_CODING_AGENT_SESSION_DIR;
|
||
|
|
const run = spawnSync(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', script], {
|
||
|
|
cwd: process.cwd(),
|
||
|
|
env,
|
||
|
|
encoding: 'utf8',
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(run.status, 0, run.stderr);
|
||
|
|
const sessions = JSON.parse(run.stdout);
|
||
|
|
assert.equal(sessions.length, 1);
|
||
|
|
const fixtureHeader = JSON.parse(
|
||
|
|
readFileSync(new URL('./fixtures/pi/tool-session.jsonl', import.meta.url), 'utf8').split('\n')[0],
|
||
|
|
);
|
||
|
|
assert.equal(sessions[0].id, piSessionId(fixtureHeader));
|
||
|
|
assert.deepEqual(
|
||
|
|
(({ title, source, message_count }) => ({ title, source, message_count }))(sessions[0]),
|
||
|
|
{ title: 'Tool probe', source: 'pi', message_count: 4 },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('passive-pull runtime honors the same persisted Pi root as the app', () => {
|
||
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-pi-custom-runtime-'));
|
||
|
|
const defaultRoot = join(home, '.pi', 'agent', 'sessions');
|
||
|
|
const customRoot = join(home, 'custom-pi-sessions');
|
||
|
|
const sessionPath = join(customRoot, '--tmp-pi-real-tool--', 'session.jsonl');
|
||
|
|
mkdirSync(defaultRoot, { recursive: true });
|
||
|
|
mkdirSync(dirname(sessionPath), { recursive: true });
|
||
|
|
writeFileSync(
|
||
|
|
sessionPath,
|
||
|
|
readFileSync(new URL('./fixtures/pi/tool-session.jsonl', import.meta.url)),
|
||
|
|
);
|
||
|
|
const settingsPath = join(home, '.obelisk', 'settings.json');
|
||
|
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
||
|
|
writeFileSync(settingsPath, JSON.stringify({
|
||
|
|
providerRoots: { pi: customRoot },
|
||
|
|
}));
|
||
|
|
|
||
|
|
const coreUrl = pathToFileURL(join(process.cwd(), 'packages/core/src/core.ts')).href;
|
||
|
|
const script = `
|
||
|
|
import { executeQuery } from ${JSON.stringify(coreUrl)};
|
||
|
|
const result = await executeQuery("return sessions({ source: 'pi', limit: 5 });");
|
||
|
|
process.stdout.write(JSON.stringify(result));
|
||
|
|
`;
|
||
|
|
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
||
|
|
delete env.PI_CODING_AGENT_DIR;
|
||
|
|
delete env.PI_CODING_AGENT_SESSION_DIR;
|
||
|
|
const run = spawnSync(process.execPath, ['--experimental-strip-types', '--input-type=module', '-e', script], {
|
||
|
|
cwd: process.cwd(),
|
||
|
|
env,
|
||
|
|
encoding: 'utf8',
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(run.status, 0, run.stderr);
|
||
|
|
const sessions = JSON.parse(run.stdout);
|
||
|
|
assert.equal(sessions.length, 1);
|
||
|
|
assert.equal(sessions[0].source, 'pi');
|
||
|
|
assert.equal(sessions[0].title, 'Tool probe');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('passive CLI operations report an incomplete Pi inventory without hiding partial results', () => {
|
||
|
|
for (const command of ['search', 'query', 'attune']) {
|
||
|
|
const home = mkdtempSync(join(tmpdir(), `obelisk-pi-partial-${command}-`));
|
||
|
|
const piRoot = join(home, '.pi', 'agent', 'sessions');
|
||
|
|
mkdirSync(dirname(piRoot), { recursive: true });
|
||
|
|
writeFileSync(piRoot, 'not a directory');
|
||
|
|
const queryPath = join(home, 'query.mjs');
|
||
|
|
writeFileSync(
|
||
|
|
queryPath,
|
||
|
|
command === 'attune'
|
||
|
|
? 'return null;'
|
||
|
|
: "return sessions({ source: 'pi', limit: 5 });",
|
||
|
|
);
|
||
|
|
const args = command === 'search'
|
||
|
|
? ['--search', 'partial-probe']
|
||
|
|
: [`--${command}`, queryPath];
|
||
|
|
|
||
|
|
const result = runCli(args, {
|
||
|
|
home,
|
||
|
|
env: { PI_CODING_AGENT_DIR: '', PI_CODING_AGENT_SESSION_DIR: '' },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||
|
|
assert.deepEqual(JSON.parse(result.stdout), command === 'attune' ? null : []);
|
||
|
|
assert.match(result.stderr, /Warning: incomplete pi source inventory/);
|
||
|
|
assert.match(
|
||
|
|
result.stderr,
|
||
|
|
new RegExp(piRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||
|
|
);
|
||
|
|
assert.match(result.stderr, /ENOTDIR|not a directory/i);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('CLI force rebuild rejects a structurally invalid Pi snapshot and preserves the last good index', () => {
|
||
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-pi-cli-force-'));
|
||
|
|
const sessionPath = join(home, '.pi', 'agent', 'sessions', 'project', 'session.jsonl');
|
||
|
|
mkdirSync(dirname(sessionPath), { recursive: true });
|
||
|
|
const fixture = readFileSync(new URL('./fixtures/pi/tool-session.jsonl', import.meta.url), 'utf8');
|
||
|
|
writeFileSync(sessionPath, fixture);
|
||
|
|
const env = { PI_CODING_AGENT_DIR: '', PI_CODING_AGENT_SESSION_DIR: '' };
|
||
|
|
|
||
|
|
const first = runCli(['--build'], { home, env });
|
||
|
|
assert.equal(first.status, 0, first.stderr || first.stdout);
|
||
|
|
const dbPath = join(home, '.obelisk', 'obelisk.sqlite');
|
||
|
|
let db = new DatabaseSync(dbPath, { readOnly: true });
|
||
|
|
const before = {
|
||
|
|
sessions: db.prepare("SELECT id,title,message_count,jsonl_path FROM sessions WHERE source='pi'").all()
|
||
|
|
.map(row => ({ ...row })),
|
||
|
|
messages: db.prepare("SELECT uuid,text FROM messages WHERE source='pi' ORDER BY uuid").all()
|
||
|
|
.map(row => ({ ...row })),
|
||
|
|
};
|
||
|
|
db.close();
|
||
|
|
|
||
|
|
writeFileSync(sessionPath, [
|
||
|
|
fixture.split('\n')[0],
|
||
|
|
JSON.stringify({
|
||
|
|
type: 'message',
|
||
|
|
id: 'invalid-message',
|
||
|
|
parentId: null,
|
||
|
|
timestamp: '2026-08-02T10:00:01.000Z',
|
||
|
|
message: null,
|
||
|
|
}),
|
||
|
|
'',
|
||
|
|
].join('\n'));
|
||
|
|
const failed = runCli(['--build'], { home, env });
|
||
|
|
assert.equal(failed.status, 1, failed.stderr || failed.stdout);
|
||
|
|
assert.match(JSON.parse(failed.stdout).error, /provider_failure/);
|
||
|
|
|
||
|
|
db = new DatabaseSync(dbPath, { readOnly: true });
|
||
|
|
const after = {
|
||
|
|
sessions: db.prepare("SELECT id,title,message_count,jsonl_path FROM sessions WHERE source='pi'").all()
|
||
|
|
.map(row => ({ ...row })),
|
||
|
|
messages: db.prepare("SELECT uuid,text FROM messages WHERE source='pi' ORDER BY uuid").all()
|
||
|
|
.map(row => ({ ...row })),
|
||
|
|
};
|
||
|
|
db.close();
|
||
|
|
assert.deepEqual(after, before);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('malformed official Pi settings use Pi 0.83 default-root fallback', () => {
|
||
|
|
const home = mkdtempSync(join(tmpdir(), 'obelisk-pi-cli-settings-'));
|
||
|
|
const agentDir = join(home, '.pi', 'agent');
|
||
|
|
const customRoot = join(home, 'custom-pi-sessions');
|
||
|
|
const customPath = join(customRoot, 'project', 'custom.jsonl');
|
||
|
|
const defaultPath = join(agentDir, 'sessions', 'project', 'default.jsonl');
|
||
|
|
const settingsPath = join(agentDir, 'settings.json');
|
||
|
|
const fixture = readFileSync(new URL('./fixtures/pi/tool-session.jsonl', import.meta.url), 'utf8');
|
||
|
|
mkdirSync(dirname(customPath), { recursive: true });
|
||
|
|
mkdirSync(dirname(defaultPath), { recursive: true });
|
||
|
|
writeFileSync(customPath, fixture);
|
||
|
|
writeFileSync(settingsPath, JSON.stringify({ sessionDir: customRoot }));
|
||
|
|
const env = { PI_CODING_AGENT_DIR: '', PI_CODING_AGENT_SESSION_DIR: '' };
|
||
|
|
assert.equal(runCli(['--build'], { home, env }).status, 0);
|
||
|
|
|
||
|
|
const lure = fixture.trim().split('\n').map(line => JSON.parse(line));
|
||
|
|
lure[0] = { ...lure[0], id: 'default-lure', cwd: '/tmp/default-lure' };
|
||
|
|
writeFileSync(defaultPath, `${lure.map(record => JSON.stringify(record)).join('\n')}\n`);
|
||
|
|
writeFileSync(settingsPath, '{broken');
|
||
|
|
const rebuilt = runCli(['--build'], { home, env });
|
||
|
|
assert.equal(rebuilt.status, 0, rebuilt.stderr || rebuilt.stdout);
|
||
|
|
|
||
|
|
const db = new DatabaseSync(join(home, '.obelisk', 'obelisk.sqlite'), { readOnly: true });
|
||
|
|
assert.deepEqual(
|
||
|
|
db.prepare("SELECT jsonl_path FROM sessions WHERE source='pi'").all().map(row => row.jsonl_path),
|
||
|
|
[defaultPath],
|
||
|
|
);
|
||
|
|
db.close();
|
||
|
|
});
|