|
|
@@ -0,0 +1,144 @@
|
|
|
+import assert from 'node:assert/strict';
|
|
|
+import fs from 'node:fs';
|
|
|
+import { pathToFileURL } from 'node:url';
|
|
|
+
|
|
|
+const [, , inputPath] = process.argv;
|
|
|
+assert.ok(inputPath, 'pass the plugin module path');
|
|
|
+const pluginURL = pathToFileURL(fs.realpathSync(inputPath));
|
|
|
+const marker = '<EXTREMELY_IMPORTANT>\nYou have superpowers.';
|
|
|
+let generation = 0;
|
|
|
+
|
|
|
+function reply(flavor, session) {
|
|
|
+ return flavor === 'v1' ? { data: session } : session;
|
|
|
+}
|
|
|
+
|
|
|
+function makeEvent(flavor, sessionID) {
|
|
|
+ const text = { type: 'text', text: 'Execute the assigned task' };
|
|
|
+ return {
|
|
|
+ sessionID,
|
|
|
+ messages: [flavor === 'v1'
|
|
|
+ ? { info: { role: 'user', sessionID }, parts: [text] }
|
|
|
+ : { role: 'user', content: [text] }],
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function bootstrapCount(event) {
|
|
|
+ return event.messages.flatMap((message) => message.parts ?? message.content ?? []).filter(
|
|
|
+ (part) => part.type === 'text' && part.text.startsWith(marker)
|
|
|
+ ).length;
|
|
|
+}
|
|
|
+
|
|
|
+async function makeHarness(flavor, fetchSession) {
|
|
|
+ const mod = await import(`${pluginURL.href}?session-test=${++generation}`);
|
|
|
+ const lookups = [];
|
|
|
+ const registered = [];
|
|
|
+ const get = async (id) => {
|
|
|
+ lookups.push(id);
|
|
|
+ return fetchSession(id, lookups.length);
|
|
|
+ };
|
|
|
+ let invoke;
|
|
|
+ if (flavor === 'v1') {
|
|
|
+ const hooks = await mod.SuperpowersPlugin({
|
|
|
+ client: { session: { get: ({ path: { id } }) => get(id) } },
|
|
|
+ directory: '.',
|
|
|
+ });
|
|
|
+ invoke = (event) => hooks['experimental.chat.messages.transform']({}, event);
|
|
|
+ } else {
|
|
|
+ await mod.default.setup({
|
|
|
+ skill: { transform: async (transform) => transform({ add: (skill) => registered.push(skill) }) },
|
|
|
+ session: {
|
|
|
+ get: ({ sessionID }) => get(sessionID),
|
|
|
+ hook: async (name, callback) => { if (name === 'context') invoke = callback; },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ assert.equal(typeof invoke, 'function');
|
|
|
+ return { invoke, lookups, registered };
|
|
|
+}
|
|
|
+
|
|
|
+for (const flavor of ['v1', 'v2']) {
|
|
|
+ for (const [kind, extra, expected] of [
|
|
|
+ ['root', {}, 1],
|
|
|
+ ['child', { parentID: 'parent' }, 0],
|
|
|
+ ['fork', { fork: { sessionID: 'origin' } }, 1],
|
|
|
+ ]) {
|
|
|
+ const id = `${flavor}-${kind}`;
|
|
|
+ const h = await makeHarness(flavor, () => reply(flavor, { id, ...extra }));
|
|
|
+ const event = makeEvent(flavor, id);
|
|
|
+ await h.invoke(event);
|
|
|
+ assert.equal(bootstrapCount(event), expected, `${id}: first request`);
|
|
|
+ await h.invoke(event);
|
|
|
+ assert.equal(bootstrapCount(event), expected, `${id}: repeated event`);
|
|
|
+ const fresh = makeEvent(flavor, id);
|
|
|
+ await h.invoke(fresh);
|
|
|
+ assert.equal(bootstrapCount(fresh), expected, `${id}: fresh request`);
|
|
|
+ assert.deepEqual(h.lookups, [id], `${id}: cache successful classification`);
|
|
|
+ if (flavor === 'v2' && kind === 'child') {
|
|
|
+ assert.ok(h.registered.some((skill) => skill.id === 'brainstorming'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const failures = [
|
|
|
+ ['throws', () => { throw new Error('temporary lookup failure'); }],
|
|
|
+ ['missing', () => undefined],
|
|
|
+ ['null', () => null],
|
|
|
+ ['empty', () => reply(flavor, {})],
|
|
|
+ ['wrong-id', () => reply(flavor, { id: 'different-session' })],
|
|
|
+ ['invalid-parent', (id) => reply(flavor, { id, parentID: 42 })],
|
|
|
+ ];
|
|
|
+ if (flavor === 'v1') {
|
|
|
+ failures.push(['resolved-http-error', () => ({
|
|
|
+ data: undefined,
|
|
|
+ error: { name: 'UnknownError', data: { message: 'temporary 503' } },
|
|
|
+ response: { ok: false, status: 503 },
|
|
|
+ })]);
|
|
|
+ }
|
|
|
+ for (const [kind, firstResult] of failures) {
|
|
|
+ const id = `${flavor}-${kind}`;
|
|
|
+ const h = await makeHarness(flavor, (sessionID, call) => call === 1
|
|
|
+ ? firstResult(sessionID)
|
|
|
+ : reply(flavor, { id: sessionID, parentID: 'parent' }));
|
|
|
+ const counts = [];
|
|
|
+ for (let step = 0; step < 2; step++) {
|
|
|
+ const event = makeEvent(flavor, id);
|
|
|
+ await h.invoke(event);
|
|
|
+ counts.push(bootstrapCount(event));
|
|
|
+ }
|
|
|
+ assert.deepEqual(counts, [1, 0], `${id}: recover on the next request`);
|
|
|
+ assert.deepEqual(h.lookups, [id, id], `${id}: never cache the failure`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const isolated = await makeHarness(flavor, (id) => reply(flavor,
|
|
|
+ id === 'child-session' ? { id, parentID: 'parent' } : { id }));
|
|
|
+ for (const [id, expected] of [['root-session', 1], ['child-session', 0], ['root-session', 1], ['child-session', 0]]) {
|
|
|
+ const event = makeEvent(flavor, id);
|
|
|
+ await isolated.invoke(event);
|
|
|
+ assert.equal(bootstrapCount(event), expected);
|
|
|
+ }
|
|
|
+ assert.deepEqual(isolated.lookups, ['root-session', 'child-session']);
|
|
|
+
|
|
|
+ const bounded = await makeHarness(flavor, (id) => reply(flavor, { id, parentID: 'parent' }));
|
|
|
+ for (let index = 0; index <= 512; index++) {
|
|
|
+ const event = makeEvent(flavor, `eviction-${index}`);
|
|
|
+ await bounded.invoke(event);
|
|
|
+ assert.equal(bootstrapCount(event), 0);
|
|
|
+ }
|
|
|
+ const evicted = makeEvent(flavor, 'eviction-0');
|
|
|
+ await bounded.invoke(evicted);
|
|
|
+ assert.equal(bootstrapCount(evicted), 0);
|
|
|
+ assert.equal(bounded.lookups.filter((id) => id === 'eviction-0').length, 2);
|
|
|
+
|
|
|
+ const restarted = await makeHarness(flavor, (id) => reply(flavor, { id, parentID: 'parent' }));
|
|
|
+ const afterRestart = makeEvent(flavor, 'eviction-0');
|
|
|
+ await restarted.invoke(afterRestart);
|
|
|
+ assert.equal(bootstrapCount(afterRestart), 0);
|
|
|
+ assert.deepEqual(restarted.lookups, ['eviction-0']);
|
|
|
+
|
|
|
+ const unknown = await makeHarness(flavor, () => { throw new Error('must not look up a missing ID'); });
|
|
|
+ const noID = makeEvent(flavor, undefined);
|
|
|
+ await unknown.invoke(noID);
|
|
|
+ assert.equal(bootstrapCount(noID), 1);
|
|
|
+ assert.deepEqual(unknown.lookups, []);
|
|
|
+}
|
|
|
+
|
|
|
+console.log('Session classification, recovery and cache lifetime passed');
|