236 lines
11 KiB
JavaScript
236 lines
11 KiB
JavaScript
// Execute the shipped bridge, including its bounded native-header reader.
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { stripTypeScriptTypes } from 'node:module';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { test, after } from 'node:test';
|
|
|
|
const root = new URL('../../', import.meta.url);
|
|
const bridgeSource = new URL('nebula_app/src/ai_hook/bridges.rs', root);
|
|
const bridgePath = readFileSync(bridgeSource, 'utf8').match(/PI_EXTENSION_TS: &str = include_str!\("([^"]+)"\)/)?.[1];
|
|
assert.ok(bridgePath, 'test must execute the production PI_EXTENSION_TS asset');
|
|
const embedded = readFileSync(new URL(bridgePath, bridgeSource), 'utf8');
|
|
const events = [];
|
|
globalThis.__pebrelBridgeSpawn = (_hook, args) => {
|
|
events.push(JSON.parse(args[1]));
|
|
return { unref() {} };
|
|
};
|
|
globalThis.__pebrelPiVersion = '0.85.1';
|
|
globalThis.__pebrelLoadPiModule = async () => ({ VERSION: globalThis.__pebrelPiVersion });
|
|
const executable = stripTypeScriptTypes(embedded.replace(
|
|
'import { spawn } from "node:child_process";',
|
|
'const spawn = globalThis.__pebrelBridgeSpawn;',
|
|
).replace(
|
|
'await import(name)',
|
|
'await globalThis.__pebrelLoadPiModule(name)',
|
|
) + '\nexport { sessionFor };');
|
|
const bridge = await import('data:text/javascript;base64,' + Buffer.from(executable).toString('base64'));
|
|
mkdirSync(new URL('tmp/', root), { recursive: true });
|
|
const directory = mkdtempSync(fileURLToPath(new URL('tmp/pi-bridge-', root)));
|
|
after(() => rmSync(directory, { recursive: true }));
|
|
const context = (id, file) => ({
|
|
sessionManager: { getSessionId: () => id, getSessionFile: () => file },
|
|
});
|
|
|
|
test('the unchanged runtime imports load with old, new, absent and malformed SDK versions', async () => {
|
|
const cases = [
|
|
['@mariozechner/pi-coding-agent', '0.60.0', false],
|
|
['@earendil-works/pi-coding-agent', '0.80.3', false],
|
|
['@earendil-works/pi-coding-agent', '0.80.4', true],
|
|
['@earendil-works/pi-coding-agent', '0.85.1', true],
|
|
['@earendil-works/pi-coding-agent', 'invalid', false],
|
|
[null, null, false],
|
|
];
|
|
for (const [name, version, settled] of cases) {
|
|
const sandbox = mkdtempSync(join(directory, 'loader-'));
|
|
const asset = join(sandbox, 'bridge.mjs');
|
|
// Only erase TypeScript; keep every production import and the factory intact.
|
|
writeFileSync(asset, stripTypeScriptTypes(embedded));
|
|
if (name) {
|
|
const sdk = join(sandbox, 'node_modules', name);
|
|
mkdirSync(sdk, { recursive: true });
|
|
writeFileSync(join(sdk, 'package.json'), JSON.stringify({ type: 'module', exports: './index.js' }));
|
|
writeFileSync(join(sdk, 'index.js'), `export const VERSION = ${JSON.stringify(version)};`);
|
|
}
|
|
const loaded = await import(pathToFileURL(asset));
|
|
const callbacks = new Map();
|
|
await loaded.default({ on: (name, callback) => callbacks.set(name, callback) });
|
|
assert.equal(callbacks.has('agent_settled'), settled, `${name} ${version}`);
|
|
for (const event of ['agent_start', 'agent_end', 'session_start', 'session_shutdown']) {
|
|
assert.equal(typeof callbacks.get(event), 'function');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('native header replaces filename guessing; process badges are never recovery IDs', () => {
|
|
const file = join(directory, 'timestamp_native-id.jsonl');
|
|
writeFileSync(file, JSON.stringify({ type: 'session', id: 'native-id' }) + '\n');
|
|
assert.deepEqual(bridge.sessionFor(context(undefined, file)), {
|
|
session_id: 'native-id', session_file: file,
|
|
});
|
|
assert.deepEqual(bridge.sessionFor(context(undefined, undefined)), {});
|
|
assert.deepEqual(bridge.sessionFor(context(undefined, file + '.missing')), {});
|
|
assert.deepEqual(bridge.sessionFor({ sessionManager: {
|
|
getSessionId() { throw new Error('closing'); }, getSessionFile: () => file,
|
|
} }), { session_id: 'native-id', session_file: file });
|
|
});
|
|
|
|
test('stale or corrupt file cannot replace direct native identity', () => {
|
|
const file = join(directory, 'mismatch.jsonl');
|
|
writeFileSync(file, JSON.stringify({ type: 'session', id: 'old' }) + '\n');
|
|
assert.deepEqual(bridge.sessionFor(context('current', file)), { session_id: 'current' });
|
|
for (const content of ['invalid\n', '{"type":"message","id":"wrong"}\n', 'x'.repeat(20000)]) {
|
|
writeFileSync(file, content);
|
|
assert.deepEqual(bridge.sessionFor(context(undefined, file)), {});
|
|
}
|
|
});
|
|
|
|
test('a retryable failed attempt must not report successful completion', async () => {
|
|
const previous = process.env.PEBREL_HOOK_EXE;
|
|
process.env.PEBREL_HOOK_EXE = 'test-hook';
|
|
const firstEvent = events.length;
|
|
try {
|
|
const callbacks = new Map();
|
|
await bridge.default({ on: (kind, callback) => callbacks.set(kind, callback) });
|
|
const ctx = { ...context('retrying'), isIdle: () => false };
|
|
await callbacks.get('agent_start')({}, ctx);
|
|
await callbacks.get('agent_end')({ messages: [
|
|
{ role: 'assistant', stopReason: 'error', errorMessage: 'synthetic retryable failure' },
|
|
] }, ctx);
|
|
assert.deepEqual(events.slice(firstEvent).map(event => event.kind), ['prompt']);
|
|
} finally {
|
|
events.splice(firstEvent);
|
|
if (previous === undefined) delete process.env.PEBREL_HOOK_EXE;
|
|
else process.env.PEBREL_HOOK_EXE = previous;
|
|
}
|
|
});
|
|
|
|
for (const reason of ['stop', 'error', 'aborted', 'future', 'length', 'toolUse']) {
|
|
test(`settled ${reason} reports its stop_reason once, without leaking error text`, async () => {
|
|
const previous = process.env.PEBREL_HOOK_EXE;
|
|
process.env.PEBREL_HOOK_EXE = 'test-hook';
|
|
const firstEvent = events.length;
|
|
try {
|
|
const callbacks = new Map();
|
|
await bridge.default({ on: (kind, callback) => callbacks.set(kind, callback) });
|
|
const ctx = context('settled');
|
|
await callbacks.get('agent_start')({}, ctx);
|
|
await callbacks.get('agent_end')({ messages: [
|
|
{ role: 'assistant', stopReason: reason, errorMessage: 'SECRET-provider-request' },
|
|
] }, ctx);
|
|
assert.deepEqual(events.slice(firstEvent).map(event => event.kind), ['prompt']);
|
|
assert.equal(typeof callbacks.get('agent_settled'), 'function');
|
|
await callbacks.get('agent_settled')({}, ctx);
|
|
await callbacks.get('agent_settled')({}, ctx);
|
|
assert.deepEqual(events.slice(firstEvent).map(event => event.kind), ['prompt', 'done']);
|
|
assert.equal(events.at(-1).stop_reason, reason);
|
|
assert.equal('outcome' in events.at(-1), false);
|
|
assert.ok(!JSON.stringify(events.slice(firstEvent)).includes('SECRET'));
|
|
} finally {
|
|
events.splice(firstEvent);
|
|
if (previous === undefined) delete process.env.PEBREL_HOOK_EXE;
|
|
else process.env.PEBREL_HOOK_EXE = previous;
|
|
}
|
|
});
|
|
}
|
|
|
|
test('retry success replaces the failed attempt; switching sessions drops unfinished results', async () => {
|
|
const previous = process.env.PEBREL_HOOK_EXE;
|
|
process.env.PEBREL_HOOK_EXE = 'test-hook';
|
|
const firstEvent = events.length;
|
|
try {
|
|
const callbacks = new Map();
|
|
await bridge.default({ on: (kind, callback) => callbacks.set(kind, callback) });
|
|
const ctx = context('retry-success');
|
|
await callbacks.get('agent_start')({}, ctx);
|
|
await callbacks.get('agent_end')({ messages: [{ role: 'assistant', stopReason: 'error' }] }, ctx);
|
|
await callbacks.get('agent_start')({}, ctx);
|
|
await callbacks.get('agent_end')({ messages: [{ role: 'assistant', stopReason: 'stop' }] }, ctx);
|
|
await callbacks.get('agent_settled')({}, ctx);
|
|
assert.equal(events.slice(firstEvent).filter(event => event.kind === 'done').length, 1);
|
|
assert.equal(events.at(-1).stop_reason, 'stop');
|
|
await callbacks.get('agent_start')({}, ctx);
|
|
await callbacks.get('agent_end')({ messages: [{ role: 'assistant', stopReason: 'error' }] }, ctx);
|
|
await callbacks.get('session_start')({}, context('new-session'));
|
|
await callbacks.get('agent_settled')({}, context('new-session'));
|
|
assert.equal(events.at(-1).kind, 'session-start');
|
|
} finally {
|
|
events.splice(firstEvent);
|
|
if (previous === undefined) delete process.env.PEBREL_HOOK_EXE;
|
|
else process.env.PEBREL_HOOK_EXE = previous;
|
|
}
|
|
});
|
|
|
|
test('pre-settled Pi versions retain agent_end fallback without registering unsupported events', async () => {
|
|
const previousVersion = globalThis.__pebrelPiVersion;
|
|
globalThis.__pebrelPiVersion = '0.80.3';
|
|
const legacy = bridge;
|
|
const previous = process.env.PEBREL_HOOK_EXE;
|
|
process.env.PEBREL_HOOK_EXE = 'test-hook';
|
|
const firstEvent = events.length;
|
|
try {
|
|
const callbacks = new Map();
|
|
await legacy.default({ on: (kind, callback) => callbacks.set(kind, callback) });
|
|
assert.equal(callbacks.has('agent_settled'), false);
|
|
const ctx = context('legacy');
|
|
await callbacks.get('agent_start')({}, ctx);
|
|
await callbacks.get('agent_end')({ messages: [{ role: 'assistant', stopReason: 'stop' }] }, ctx);
|
|
assert.deepEqual(events.slice(firstEvent).map(event => event.kind), ['prompt', 'done']);
|
|
assert.equal(events.at(-1).stop_reason, 'stop');
|
|
} finally {
|
|
globalThis.__pebrelPiVersion = previousVersion;
|
|
events.splice(firstEvent);
|
|
if (previous === undefined) delete process.env.PEBREL_HOOK_EXE;
|
|
else process.env.PEBREL_HOOK_EXE = previous;
|
|
}
|
|
});
|
|
|
|
test('shutdown discards pending completion and a new turn cannot reuse an old result', async () => {
|
|
const previous = process.env.PEBREL_HOOK_EXE;
|
|
process.env.PEBREL_HOOK_EXE = 'test-hook';
|
|
const firstEvent = events.length;
|
|
try {
|
|
const callbacks = new Map();
|
|
await bridge.default({ on: (kind, callback) => callbacks.set(kind, callback) });
|
|
const ctx = context('shutdown');
|
|
await callbacks.get('agent_start')({}, ctx);
|
|
await callbacks.get('agent_end')({ messages: [{ role: 'assistant', stopReason: 'error' }] }, ctx);
|
|
await callbacks.get('session_shutdown')({}, ctx);
|
|
await callbacks.get('agent_settled')({}, ctx);
|
|
assert.deepEqual(events.slice(firstEvent).map(event => event.kind), ['prompt', 'session-end']);
|
|
await callbacks.get('session_start')({}, ctx);
|
|
await callbacks.get('agent_start')({}, ctx);
|
|
await callbacks.get('agent_settled')({}, ctx);
|
|
assert.equal(events.at(-1).stop_reason, 'unknown');
|
|
} finally {
|
|
events.splice(firstEvent);
|
|
if (previous === undefined) delete process.env.PEBREL_HOOK_EXE;
|
|
else process.env.PEBREL_HOOK_EXE = previous;
|
|
}
|
|
});
|
|
|
|
test('session switch retains one process ordering stream and reports identity before turns', async () => {
|
|
const previous = process.env.PEBREL_HOOK_EXE;
|
|
process.env.PEBREL_HOOK_EXE = 'test-hook';
|
|
try {
|
|
const callbacks = new Map();
|
|
await bridge.default({ on: (kind, callback) => callbacks.set(kind, callback) });
|
|
await callbacks.get('session_start')({}, context('first'));
|
|
await callbacks.get('session_start')({}, context('second'));
|
|
await callbacks.get('agent_start')({}, context('second'));
|
|
await callbacks.get('agent_end')({ messages: [{ role: 'assistant', stopReason: 'stop' }] }, context('second'));
|
|
await callbacks.get('agent_settled')({}, context('second'));
|
|
assert.deepEqual(events.map(event => event.session_id), ['first', 'second', 'second', 'second']);
|
|
assert.deepEqual(events.map(event => event.kind), ['session-start', 'session-start', 'prompt', 'done']);
|
|
assert.equal(new Set(events.map(event => event.bridge_instance)).size, 1);
|
|
for (let i = 1; i < events.length; i++) {
|
|
assert.ok(BigInt(events[i].bridge_sequence) > BigInt(events[i - 1].bridge_sequence));
|
|
assert.notEqual(events[i].event_id, events[i - 1].event_id);
|
|
}
|
|
} finally {
|
|
if (previous === undefined) delete process.env.PEBREL_HOOK_EXE;
|
|
else process.env.PEBREL_HOOK_EXE = previous;
|
|
}
|
|
});
|