Files
TREK/server/tests/unit/plugins/dependencies.test.ts
T
2026-10-06 21:50:59 +02:00

136 lines
5.1 KiB
TypeScript

/**
* Pure dependency-reasoning helpers (#plugins). Parsing, addon/plugin gating,
* dependent lookup, and cycle-safe enable ordering.
*/
import {
parseDependencies,
disabledRequiredAddons,
resolveDependencyState,
findDependents,
findDependentsTransitive,
enableOrder,
DependencyCycleError,
type PluginDepRow,
} from '../../../src/nest/plugins/dependencies';
import { describe, it, expect } from 'vitest';
const row = (
id: string,
opts: Partial<PluginDepRow> & {
deps?: { requiredAddons?: string[]; pluginDependencies?: { id: string; version: string }[] };
} = {},
): PluginDepRow => ({
id,
version: opts.version ?? '1.0.0',
enabled: opts.enabled ?? 0,
dependencies: opts.deps
? JSON.stringify({
requiredAddons: opts.deps.requiredAddons ?? [],
pluginDependencies: opts.deps.pluginDependencies ?? [],
})
: (opts.dependencies ?? '{}'),
});
const map = (...rows: PluginDepRow[]) => new Map(rows.map((r) => [r.id, r]));
describe('parseDependencies', () => {
it('returns empty lists for null/garbage/legacy blobs', () => {
expect(parseDependencies(null)).toEqual({ requiredAddons: [], pluginDependencies: [] });
expect(parseDependencies('not json')).toEqual({ requiredAddons: [], pluginDependencies: [] });
expect(parseDependencies('{}')).toEqual({ requiredAddons: [], pluginDependencies: [] });
});
it('filters non-string addons and malformed deps', () => {
const d = parseDependencies(
JSON.stringify({
requiredAddons: ['budget', 5],
pluginDependencies: [{ id: 'koffi', version: '^1' }, { id: 'x' }],
}),
);
expect(d.requiredAddons).toEqual(['budget']);
expect(d.pluginDependencies).toEqual([{ id: 'koffi', version: '^1' }]);
});
});
describe('disabledRequiredAddons', () => {
it('lists only the disabled required addons', async () => {
const deps = parseDependencies(JSON.stringify({ requiredAddons: ['budget', 'journey'], pluginDependencies: [] }));
expect(await disabledRequiredAddons(deps, (a) => Promise.resolve(a === 'budget'))).toEqual(['journey']);
expect(await disabledRequiredAddons(deps, () => Promise.resolve(true))).toEqual([]);
});
});
describe('resolveDependencyState', () => {
const deps = parseDependencies(
JSON.stringify({
requiredAddons: [],
pluginDependencies: [
{ id: 'a', version: '>=1.0.0' },
{ id: 'b', version: '^2.0.0' },
{ id: 'c', version: '^1.0.0' },
],
}),
);
it('classifies missing / mismatch / satisfied (active + disabled)', () => {
const installed = map(
row('a', { version: '1.5.0', enabled: 1 }), // satisfied + active
row('b', { version: '1.0.0', enabled: 0 }), // installed but ^2 not satisfied → mismatch
// c missing
);
const s = resolveDependencyState(deps, installed);
expect(s.satisfiedActive).toEqual(['a']);
expect(s.versionMismatch).toEqual([{ id: 'b', wanted: '^2.0.0', installed: '1.0.0' }]);
expect(s.missing.map((m) => m.id)).toEqual(['c']);
expect(s.satisfiedDisabled).toEqual([]);
});
it('reports a satisfied-but-disabled dep as an auto-enable candidate', () => {
const s = resolveDependencyState(
parseDependencies(JSON.stringify({ requiredAddons: [], pluginDependencies: [{ id: 'a', version: '^1.0.0' }] })),
map(row('a', { version: '1.2.0', enabled: 0 })),
);
expect(s.satisfiedDisabled).toEqual(['a']);
});
});
describe('findDependents', () => {
it('finds direct + transitive dependents', () => {
const rows = [
row('base'),
row('mid', { deps: { pluginDependencies: [{ id: 'base', version: '*' }] } }),
row('top', { deps: { pluginDependencies: [{ id: 'mid', version: '*' }] } }),
row('unrelated'),
];
expect(findDependents('base', rows)).toEqual(['mid']);
expect(findDependentsTransitive('base', rows).sort()).toEqual(['mid', 'top']);
});
});
describe('enableOrder', () => {
it('orders dependencies before dependents', () => {
const installed = map(
row('base'),
row('mid', { deps: { pluginDependencies: [{ id: 'base', version: '*' }] } }),
row('top', { deps: { pluginDependencies: [{ id: 'mid', version: '*' }] } }),
);
expect(enableOrder(['top'], installed)).toEqual(['base', 'mid', 'top']);
});
it('ignores edges to uninstalled deps', () => {
const installed = map(row('top', { deps: { pluginDependencies: [{ id: 'ghost', version: '*' }] } }));
expect(enableOrder(['top'], installed)).toEqual(['top']);
});
it('dedupes across multiple roots', () => {
const installed = map(
row('base'),
row('a', { deps: { pluginDependencies: [{ id: 'base', version: '*' }] } }),
row('b', { deps: { pluginDependencies: [{ id: 'base', version: '*' }] } }),
);
expect(enableOrder(['a', 'b'], installed)).toEqual(['base', 'a', 'b']);
});
it('throws DependencyCycleError on a cycle', () => {
const installed = map(
row('a', { deps: { pluginDependencies: [{ id: 'b', version: '*' }] } }),
row('b', { deps: { pluginDependencies: [{ id: 'a', version: '*' }] } }),
);
expect(() => enableOrder(['a'], installed)).toThrow(DependencyCycleError);
});
});