128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
|
|
type LifecycleRunner = (args: string[]) => boolean;
|
|
type LifecycleRegister = (api: unknown, runner: LifecycleRunner) => void;
|
|
type Handler = (event: Record<string, unknown>, ctx: Record<string, unknown>) => unknown;
|
|
|
|
const lifecycleModuleUrl = new URL('../../framework/runtime/pi/lease-lifecycle.ts', import.meta.url)
|
|
.href;
|
|
const { registerLeaseLifecycleHooks } = (await import(lifecycleModuleUrl)) as {
|
|
registerLeaseLifecycleHooks: LifecycleRegister;
|
|
};
|
|
|
|
function fakePi() {
|
|
const handlers = new Map<string, Handler[]>();
|
|
return {
|
|
handlers,
|
|
api: {
|
|
on(event: string, handler: Handler) {
|
|
handlers.set(event, [...(handlers.get(event) ?? []), handler]);
|
|
},
|
|
},
|
|
async emit(event: string, value: Record<string, unknown> = {}) {
|
|
const results = [];
|
|
for (const handler of handlers.get(event) ?? []) {
|
|
results.push(await handler(value, {}));
|
|
}
|
|
return results;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('Pi compaction and runtime-generation lease lifecycle', () => {
|
|
test('pre-compaction and first post-compaction context independently revoke', async () => {
|
|
const pi = fakePi();
|
|
const calls: string[][] = [];
|
|
registerLeaseLifecycleHooks(pi.api as never, (args) => {
|
|
calls.push(args);
|
|
return true;
|
|
});
|
|
|
|
expect(await pi.emit('session_before_compact', { reason: 'threshold' })).toEqual([undefined]);
|
|
expect(calls.at(-1)).toEqual([
|
|
'--runtime',
|
|
'pi',
|
|
'--reason',
|
|
'pi-session-before-compact:threshold',
|
|
]);
|
|
|
|
await pi.emit('session_compact', { reason: 'threshold' });
|
|
await pi.emit('context', { messages: [] });
|
|
expect(calls.at(-1)).toEqual([
|
|
'--runtime',
|
|
'pi',
|
|
'--reason',
|
|
'pi-context-after-compact:threshold',
|
|
]);
|
|
const afterFirstContext = calls.length;
|
|
await pi.emit('context', { messages: [] });
|
|
expect(calls).toHaveLength(afterFirstContext);
|
|
});
|
|
|
|
test.each(['reload', 'new', 'resume', 'fork'])(
|
|
'%s bumps generation before reuse',
|
|
async (reason) => {
|
|
const pi = fakePi();
|
|
const calls: string[][] = [];
|
|
registerLeaseLifecycleHooks(pi.api as never, (args) => {
|
|
calls.push(args);
|
|
return true;
|
|
});
|
|
|
|
await pi.emit('session_start', { reason });
|
|
expect(calls).toEqual([
|
|
['--runtime', 'pi', '--reason', `pi-session-start:${reason}`, '--bump-generation'],
|
|
]);
|
|
},
|
|
);
|
|
|
|
test('startup leaves the launcher generation intact and tools locally open', async () => {
|
|
const pi = fakePi();
|
|
const calls: string[][] = [];
|
|
registerLeaseLifecycleHooks(pi.api as never, (args) => {
|
|
calls.push(args);
|
|
return true;
|
|
});
|
|
|
|
await pi.emit('session_start', { reason: 'startup' });
|
|
await pi.emit('session_start');
|
|
expect(calls).toEqual([]);
|
|
expect(await pi.emit('tool_call', { toolName: 'bash' })).toEqual([undefined]);
|
|
});
|
|
|
|
test('failed post-compaction revoke blocks tools until context retries successfully', async () => {
|
|
const pi = fakePi();
|
|
const outcomes = [false, true];
|
|
registerLeaseLifecycleHooks(pi.api as never, () => outcomes.shift() ?? true);
|
|
|
|
await pi.emit('session_compact');
|
|
await pi.emit('context');
|
|
expect(await pi.emit('tool_call', { toolName: 'bash' })).toEqual([
|
|
{
|
|
block: true,
|
|
reason: expect.stringContaining('lease lifecycle revoke failed'),
|
|
},
|
|
]);
|
|
|
|
await pi.emit('context');
|
|
expect(await pi.emit('tool_call', { toolName: 'bash' })).toEqual([undefined]);
|
|
});
|
|
|
|
test('failed lifecycle revoke cancels compaction and closes later tool calls', async () => {
|
|
const pi = fakePi();
|
|
registerLeaseLifecycleHooks(pi.api as never, () => false);
|
|
|
|
const compact = await pi.emit('session_before_compact', { reason: 'manual' });
|
|
expect(compact).toEqual([{ cancel: true }]);
|
|
|
|
await pi.emit('session_start', { reason: 'resume' });
|
|
const tool = await pi.emit('tool_call', { toolName: 'bash' });
|
|
expect(tool).toEqual([
|
|
{
|
|
block: true,
|
|
reason: expect.stringContaining('lease lifecycle revoke failed'),
|
|
},
|
|
]);
|
|
});
|
|
});
|