mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
724c88c201
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.
136 lines
5.1 KiB
TypeScript
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);
|
|
});
|
|
});
|