Something went wrong. Try again.
ATProto-based AI Assistant Chat interface.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128import { definePlugin, type ModelProvider, type Plugin } from '@scn-chat/plugin-api'import pino from 'pino'import { beforeEach, describe, expect, it, vi } from 'vitest'import { loadPlugins, PluginLoadError, type PluginServices } from '../../src/plugins/host.ts'
const logger = pino({ level: 'silent' })const app = { name: 'Test', publicUrl: 'http://127.0.0.1:3000' }let services: PluginServicesbeforeEach(() => { services = { generateText: vi.fn(async () => ({ text: 'generated', finishReason: 'stop' })), updateInfo: vi.fn(async () => {}), userSettings: vi.fn(async () => ({ enabled: true })), }})
const plugin = (id: string, setup: Plugin['setup'] = () => {}, apiVersion = 1) => definePlugin({ id, name: id, apiVersion, setup })
const provider = (id: string) => ({ id }) as ModelProvider
describe('loadPlugins', () => { it('calls each setup once, in config order', async () => { const calls: string[] = [] await loadPlugins( [ plugin('first', () => void calls.push('first')), plugin('second', () => void calls.push('second')), ], { services, logger, app }, ) expect(calls).toEqual(['first', 'second']) })
it('waits for async setups before loading the next plugin', async () => { const calls: string[] = [] const slow = plugin('slow', async () => { await new Promise((resolve) => setTimeout(resolve, 10)) calls.push('slow') }) await loadPlugins([slow, plugin('fast', () => void calls.push('fast'))], { services, logger, app, }) expect(calls).toEqual(['slow', 'fast']) })
it('fails when two plugins share an ID, naming it', async () => { await expect( loadPlugins([plugin('dup'), plugin('dup')], { services, logger, app }), ).rejects.toThrow(/Two plugins use the ID "dup"/) })
it('fails for an unsupported apiVersion, naming the plugin and version', async () => { const promise = loadPlugins([plugin('future', () => {}, 2)], { services, logger, app }) await expect(promise).rejects.toBeInstanceOf(PluginLoadError) await expect(promise).rejects.toThrow(/"future".*version 2/) })
it('fails for an invalid plugin ID', async () => { await expect(loadPlugins([plugin('Bad ID')], { services, logger, app })).rejects.toThrow( /must match/, ) })
it('fails when two plugins register the same provider ID', async () => { const a = plugin('a', (ctx) => ctx.providers.register(provider('anthropic'))) const b = plugin('b', (ctx) => ctx.providers.register(provider('anthropic'))) await expect(loadPlugins([a, b], { services, logger, app })).rejects.toThrow(/anthropic/) })
it('fails when two plugins register the same tool name or ingester ID', async () => { const tool = { name: 'search', description: '', inputSchema: {} as never, run: async () => null, } const toolA = plugin('a', (ctx) => ctx.tools.register(tool)) const toolB = plugin('b', (ctx) => ctx.tools.register(tool)) await expect(loadPlugins([toolA, toolB], { services, logger, app })).rejects.toThrow(/search/) const ingester = { id: 'pdf', accepts: ['application/pdf'], method: 'text' as const, ingest: async () => ({ text: '' }), } const ingA = plugin('a', (ctx) => ctx.ingesters.register(ingester)) const ingB = plugin('b', (ctx) => ctx.ingesters.register(ingester)) await expect(loadPlugins([ingA, ingB], { services, logger, app })).rejects.toThrow(/pdf/) })
it('gives plugins the services through their context', async () => { let seen: unknown const reader = plugin('reader', async (ctx) => { seen = await ctx.userSettings('did:plc:a') await ctx.conversations.updateInfo( 'did:plc:a', 'at://x', { title: 'T' }, { unlessUserTitled: true }, ) }) await loadPlugins([reader], { services, logger, app }) expect(seen).toEqual({ enabled: true }) expect(services.updateInfo).toHaveBeenCalledWith( 'did:plc:a', 'at://x', { title: 'T' }, { unlessUserTitled: true }, ) })
it('runs registered cleanup at close, newest first', async () => { const order: string[] = [] const host = await loadPlugins( [ plugin('a', (ctx) => ctx.onClose(() => void order.push('a'))), plugin('b', (ctx) => ctx.onClose(() => void order.push('b'))), ], { services, logger, app }, ) await host.close() expect(order).toEqual(['b', 'a']) })})