Files
pebrel/scripts/tests/test_pi_bridge.mjs
T
kuddev 8564334d66 Integrate complete 1.9.0 desktop changes and Windows session recovery
Include cloud settings, panel sizing, command groups, Agent integration settings and runtime input fixes from main. Bind native Codex recovery to the live pane process and preserve verified IDs; add CMD recovery choices and an explicit exit path when identity discovery fails.
2026-09-21 19:25:35 +08:00

256 lines
12 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';
import { spawnSync } from 'node:child_process';
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 { on() {}, 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 }));
test('a missing helper fails silently without terminating the agent process', () => {
const asset = join(directory, 'missing-helper-bridge.mjs');
writeFileSync(asset, stripTypeScriptTypes(embedded));
const child = spawnSync(process.execPath, ['--input-type=module', '-e', `
const bridge = await import(${JSON.stringify(pathToFileURL(asset).href)});
const callbacks = new Map();
await bridge.default({ on: (event, callback) => callbacks.set(event, callback) });
await callbacks.get('agent_start')({}, {});
await new Promise(resolve => setTimeout(resolve, 20));
console.log('agent-alive');
`], {
encoding: 'utf8', windowsHide: true,
env: { ...process.env, PEBREL_HOOK_EXE: join(directory, 'missing-helper.exe') },
});
assert.equal(child.status, 0, child.stderr);
assert.match(child.stdout, /agent-alive/);
assert.doesNotMatch(child.stderr, /Unhandled|ENOENT/);
});
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;
}
});