Files
TREK/server/tests/unit/plugins/dependencies.test.ts
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +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);
});
});