68 lines
2.9 KiB
TypeScript
68 lines
2.9 KiB
TypeScript
|
|
import { describe, it, expect, vi } from 'vitest';
|
||
|
|
import { composePlugins } from '../src/proxymodel/plugins/compose.js';
|
||
|
|
import type { ProxyModelPlugin, PluginSessionContext } from '../src/proxymodel/plugin.js';
|
||
|
|
import type { JsonRpcRequest, JsonRpcResponse } from '../src/types.js';
|
||
|
|
|
||
|
|
const fakeCtx = {} as PluginSessionContext;
|
||
|
|
|
||
|
|
function plugin(name: string, hooks: Partial<ProxyModelPlugin> = {}): ProxyModelPlugin {
|
||
|
|
return { name, ...hooks };
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('composePlugins', () => {
|
||
|
|
it('returns the single plugin when given one', () => {
|
||
|
|
const p = plugin('only');
|
||
|
|
expect(composePlugins([p])).toBe(p);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('throws when given an empty list', () => {
|
||
|
|
expect(() => composePlugins([])).toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('chains onSessionCreate / onSessionDestroy in order', async () => {
|
||
|
|
const calls: string[] = [];
|
||
|
|
const a = plugin('a', {
|
||
|
|
onSessionCreate: async () => { calls.push('a-create'); },
|
||
|
|
onSessionDestroy: async () => { calls.push('a-destroy'); },
|
||
|
|
});
|
||
|
|
const b = plugin('b', {
|
||
|
|
onSessionCreate: async () => { calls.push('b-create'); },
|
||
|
|
onSessionDestroy: async () => { calls.push('b-destroy'); },
|
||
|
|
});
|
||
|
|
const composed = composePlugins([a, b]);
|
||
|
|
await composed.onSessionCreate!(fakeCtx);
|
||
|
|
await composed.onSessionDestroy!(fakeCtx);
|
||
|
|
expect(calls).toEqual(['a-create', 'b-create', 'a-destroy', 'b-destroy']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('first non-null onToolCallBefore short-circuits the chain', async () => {
|
||
|
|
const aSpy = vi.fn(async () => null);
|
||
|
|
const bSpy = vi.fn(async (): Promise<JsonRpcResponse> => ({ jsonrpc: '2.0', id: 1, result: 'B' }));
|
||
|
|
const cSpy = vi.fn(async (): Promise<JsonRpcResponse> => ({ jsonrpc: '2.0', id: 1, result: 'C' }));
|
||
|
|
const composed = composePlugins([
|
||
|
|
plugin('a', { onToolCallBefore: aSpy }),
|
||
|
|
plugin('b', { onToolCallBefore: bSpy }),
|
||
|
|
plugin('c', { onToolCallBefore: cSpy }),
|
||
|
|
]);
|
||
|
|
const req: JsonRpcRequest = { jsonrpc: '2.0', id: 1, method: 'tools/call' };
|
||
|
|
const res = await composed.onToolCallBefore!('foo', {}, req, fakeCtx);
|
||
|
|
expect(res?.result).toBe('B');
|
||
|
|
expect(cSpy).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('onToolsList pipelines through plugins (each transforms the previous output)', async () => {
|
||
|
|
const composed = composePlugins([
|
||
|
|
plugin('a', { onToolsList: async (tools) => [...tools, { name: 'a-added', description: '' }] }),
|
||
|
|
plugin('b', { onToolsList: async (tools) => [...tools, { name: 'b-added', description: '' }] }),
|
||
|
|
]);
|
||
|
|
const out = await composed.onToolsList!([{ name: 'orig', description: '' }], fakeCtx);
|
||
|
|
expect(out.map((t) => t.name)).toEqual(['orig', 'a-added', 'b-added']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does not declare hooks that no plugin provides (no-op composition stays minimal)', () => {
|
||
|
|
const composed = composePlugins([plugin('a'), plugin('b')]);
|
||
|
|
expect(composed.onSessionCreate).toBeUndefined();
|
||
|
|
expect(composed.onToolsList).toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|