import type { TojuPluginManifest } from '../../../../shared-kernel'; import { resolvePluginLoadOrder } from './plugin-dependency-resolver.logic'; function manifest(id: string, overrides: Partial = {}): TojuPluginManifest { return { apiVersion: '1.0.0', compatibility: { minimumTojuVersion: '1.0.0' }, description: `${id} plugin`, entrypoint: './main.js', id, kind: 'client', schemaVersion: 1, title: id, version: '1.0.0', ...overrides }; } describe('plugin dependency resolver', () => { it('orders required dependencies before dependants', () => { const featurePlugin = manifest('feature.chat', { relationships: { requires: [{ id: 'library.base' }] } }); const result = resolvePluginLoadOrder([{ manifest: featurePlugin }, { manifest: manifest('library.base') }]); expect(result.blocked).toEqual([]); expect(result.ordered.map((entry) => entry.id)).toEqual(['library.base', 'feature.chat']); }); it('uses priority then plugin id for otherwise independent plugins', () => { const result = resolvePluginLoadOrder([ { manifest: manifest('plugin.zed') }, { manifest: manifest('plugin.bootstrap', { load: { priority: 'bootstrap' } }) }, { manifest: manifest('plugin.alpha') } ]); expect(result.ordered.map((entry) => entry.id)).toEqual([ 'plugin.bootstrap', 'plugin.alpha', 'plugin.zed' ]); }); it('blocks missing dependencies and leaves valid plugins loadable', () => { const blockedPlugin = manifest('plugin.blocked', { relationships: { requires: [{ id: 'missing.library' }] } }); const result = resolvePluginLoadOrder([{ manifest: manifest('plugin.valid') }, { manifest: blockedPlugin }]); expect(result.ordered.map((entry) => entry.id)).toEqual(['plugin.valid']); expect(result.blocked).toContainEqual({ message: 'Missing required plugin missing.library', pluginId: 'plugin.blocked', reason: 'missingDependency' }); }); it('detects duplicate ids and cycles', () => { const result = resolvePluginLoadOrder([ { manifest: manifest('plugin.duplicate') }, { manifest: manifest('plugin.duplicate') }, { manifest: manifest('plugin.a', { relationships: { after: ['plugin.b'] } }) }, { manifest: manifest('plugin.b', { relationships: { after: ['plugin.a'] } }) } ]); expect(result.blocked).toEqual(expect.arrayContaining([ { message: 'Duplicate plugin id', pluginId: 'plugin.duplicate', reason: 'duplicate' }, { message: 'Plugin load order contains a cycle', pluginId: 'plugin.a', reason: 'cycle' }, { message: 'Plugin load order contains a cycle', pluginId: 'plugin.b', reason: 'cycle' } ])); }); });