Files
accounted/lib/extensions/__tests__/registry.test.ts
T
Jakob Wennberg ef5a84a5d5 feat: make extension system packageable via enriched ExtensionContext
Enrich ExtensionContext with supabase, emit(), settings, storage, log,
and services so extensions can receive everything through dependency
injection instead of importing core modules directly.

- Add context factory and inject context into event handlers via registry
- Move supplier invoice journal entry creation to core event handler
- Add services.ingestTransactions to ExtensionContext for enable-banking
- Create catch-all API route for extension-declared apiRoutes
- Migrate 5 extensions to accept context with dynamic import fallbacks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:24:21 +01:00

162 lines
4.5 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { extensionRegistry, setContextFactory } from '../registry'
import { eventBus } from '@/lib/events/bus'
import type { Extension, ExtensionContext } from '../types'
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn().mockResolvedValue({
from: vi.fn(),
storage: { from: vi.fn() },
}),
}))
beforeEach(() => {
extensionRegistry.clear()
eventBus.clear()
})
function makeExtension(overrides: Partial<Extension> = {}): Extension {
return {
id: 'test-ext',
name: 'Test Extension',
version: '1.0.0',
...overrides,
}
}
describe('ExtensionRegistry', () => {
it('register() stores extension, queryable via get() and getAll()', () => {
const ext = makeExtension()
extensionRegistry.register(ext)
expect(extensionRegistry.get('test-ext')).toBe(ext)
expect(extensionRegistry.getAll()).toEqual([ext])
})
it('register() wires event handlers to the bus', async () => {
const handler = vi.fn()
const ext = makeExtension({
id: 'event-ext',
eventHandlers: [{ eventType: 'journal_entry.committed', handler }],
})
extensionRegistry.register(ext)
await eventBus.emit({
type: 'journal_entry.committed',
payload: { entry: { id: 'e1' } as never, userId: 'u1' },
})
expect(handler).toHaveBeenCalled()
expect(handler.mock.calls[0][0]).toEqual({ entry: { id: 'e1' }, userId: 'u1' })
})
it('register() skips duplicate registration (same id)', () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const ext1 = makeExtension()
const ext2 = makeExtension({ name: 'Duplicate' })
extensionRegistry.register(ext1)
extensionRegistry.register(ext2)
// Original is kept
expect(extensionRegistry.get('test-ext')!.name).toBe('Test Extension')
expect(extensionRegistry.getAll()).toHaveLength(1)
consoleSpy.mockRestore()
})
it('unregister() removes extension and unsubscribes handlers', async () => {
const handler = vi.fn()
const ext = makeExtension({
id: 'removable',
eventHandlers: [{ eventType: 'journal_entry.committed', handler }],
})
extensionRegistry.register(ext)
extensionRegistry.unregister('removable')
expect(extensionRegistry.get('removable')).toBeUndefined()
await eventBus.emit({
type: 'journal_entry.committed',
payload: { entry: { id: 'e1' } as never, userId: 'u1' },
})
expect(handler).not.toHaveBeenCalled()
})
it('getByCapability() filters correctly', () => {
const ext1 = makeExtension({
id: 'with-settings',
settingsPanel: { label: 'Test', path: '/test' },
})
const ext2 = makeExtension({ id: 'without-settings' })
extensionRegistry.register(ext1)
extensionRegistry.register(ext2)
const withSettings = extensionRegistry.getByCapability('settingsPanel')
expect(withSettings).toHaveLength(1)
expect(withSettings[0].id).toBe('with-settings')
})
it('clear() removes all extensions and unsubscribes all handlers', async () => {
const handler1 = vi.fn()
const handler2 = vi.fn()
extensionRegistry.register(
makeExtension({
id: 'ext1',
eventHandlers: [{ eventType: 'journal_entry.committed', handler: handler1 }],
})
)
extensionRegistry.register(
makeExtension({
id: 'ext2',
eventHandlers: [{ eventType: 'journal_entry.drafted', handler: handler2 }],
})
)
extensionRegistry.clear()
expect(extensionRegistry.getAll()).toHaveLength(0)
await eventBus.emit({
type: 'journal_entry.committed',
payload: { entry: { id: 'e1' } as never, userId: 'u1' },
})
await eventBus.emit({
type: 'journal_entry.drafted',
payload: { entry: { id: 'e1' } as never, userId: 'u1' },
})
expect(handler1).not.toHaveBeenCalled()
expect(handler2).not.toHaveBeenCalled()
})
it('handler receives ExtensionContext when context factory is set', async () => {
const handler = vi.fn()
const mockCtx = { extensionId: 'ctx-ext', userId: 'u1' } as ExtensionContext
setContextFactory((_supabase, _userId, _extId) => mockCtx)
const ext = makeExtension({
id: 'ctx-ext',
eventHandlers: [{ eventType: 'journal_entry.committed', handler }],
})
extensionRegistry.register(ext)
await eventBus.emit({
type: 'journal_entry.committed',
payload: { entry: { id: 'e1' } as never, userId: 'u1' },
})
expect(handler).toHaveBeenCalledWith(
{ entry: { id: 'e1' }, userId: 'u1' },
mockCtx
)
})
})