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

158 lines
6.7 KiB
TypeScript

import {
hostVersion,
normalizedHost,
isValidTrekRange,
minTrekOf,
hostSatisfies,
trekRangeBypassed,
bypassedRange,
__resetHostWarningForTests,
} from '../../../src/nest/plugins/install/host-compat';
import { describe, it, expect, afterEach, vi } from 'vitest';
/**
* The TREK-version gate (#plugins). Every install front door and the activation gate
* answer "can this plugin run here?" through these functions, so their edges are the
* edges of the whole feature.
*/
const APP_VERSION = process.env.APP_VERSION;
afterEach(() => {
if (APP_VERSION === undefined) delete process.env.APP_VERSION;
else process.env.APP_VERSION = APP_VERSION;
delete process.env.TREK_PLUGINS_IGNORE_TREK_RANGE;
__resetHostWarningForTests();
});
describe('isValidTrekRange', () => {
it('accepts the ranges a plugin actually declares', () => {
for (const r of ['>=3.2.0 <4.0.0', '^3.2.0', '~3.2', '>=3', '<4.0.0', '3.x', '*']) {
expect(isValidTrekRange(r), r).toBe(true);
}
});
it('rejects a range no version can ever satisfy — validRange() alone would accept it', () => {
// Syntactically fine, semantically empty. A plugin declaring this would be
// uninstallable everywhere with nothing to tell its author why.
expect(isValidTrekRange('>=4.0.0 <3.0.0')).toBe(false);
});
it('rejects junk without letting semver.minVersion throw out of it', () => {
for (const r of ['latest', 'v3-ish', '3.2+', '', ' ', undefined, null, 42, {}]) {
expect(isValidTrekRange(r), String(r)).toBe(false);
}
});
});
describe('minTrekOf', () => {
it('reads the lower bound off the RANGE, not off the first version-shaped substring', () => {
// The regression this replaces: the old regex took the first X.Y.Z in the string, so
// "<4.0.0" reported a *minimum* of 4.0.0 — the exact inverse of what the plugin says.
expect(minTrekOf('<4.0.0')).toBeUndefined(); // lower bound is 0.0.0 → nothing to show
expect(minTrekOf('>=3.2.0 <4.0.0')).toBe('3.2.0');
expect(minTrekOf('^3.2.0')).toBe('3.2.0');
expect(minTrekOf('~3.2')).toBe('3.2.0');
expect(minTrekOf('>=3')).toBe('3.0.0');
expect(minTrekOf('>=3.2.0-beta.1')).toBe('3.2.0-beta.1');
});
it('has no lower bound to show for an unbounded range', () => {
expect(minTrekOf('*')).toBeUndefined();
});
});
describe('hostSatisfies', () => {
it('admits a host inside the range and refuses one outside it', () => {
expect(hostSatisfies('>=3.2.0 <4.0.0', '3.3.0')).toBe(true);
expect(hostSatisfies('>=3.2.0 <4.0.0', '3.2.0')).toBe(true);
expect(hostSatisfies('>=3.2.0 <4.0.0', '3.1.9')).toBe(false);
expect(hostSatisfies('>=3.2.0 <4.0.0', '4.0.0')).toBe(false); // the exclusive upper bound
});
it('treats a prerelease host as its target release — both edges matter', () => {
// Plain semver would fail 3.4.0-rc.1 against a range that names no prerelease, which
// would make EVERY plugin incompatible the moment TREK shipped an rc.
expect(hostSatisfies('>=3.2.0 <4.0.0', '3.4.0-rc.1')).toBe(true);
// And includePrerelease would order 4.0.0-rc.1 BEFORE 4.0.0, so it would satisfy
// "<4.0.0" — loading a plugin that disclaims TREK 4 onto a build that already carries
// TREK 4's breaking changes. An rc of 4.0 IS 4.0 for compatibility purposes.
expect(hostSatisfies('>=3.2.0 <4.0.0', '4.0.0-rc.1')).toBe(false);
expect(hostSatisfies('>=4.0.0', '4.0.0-rc.1')).toBe(true);
});
it('never blocks when the host version is not semver at all (APP_VERSION=dev)', () => {
// The Docker build ARG defaults to the literal "dev". An unversioned build must stay
// fully usable rather than refusing every plugin it has.
process.env.APP_VERSION = 'dev';
expect(normalizedHost()).toBeNull();
expect(hostSatisfies('>=3.2.0 <4.0.0')).toBe(true);
expect(hostSatisfies(null)).toBe(true);
});
it('warns once — and only once — about an unparseable host, so a misconfig is visible', () => {
process.env.APP_VERSION = 'nonsense';
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
normalizedHost();
normalizedHost();
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0][0]).toMatch(/not a semver version/);
warn.mockRestore();
});
it('refuses an undeclared or unsatisfiable range rather than waving it through', () => {
// The caller decides what an absent range MEANS (reject at install, "unknown" at
// activation) — it must not pass by accident here.
expect(hostSatisfies(null, '3.3.0')).toBe(false);
expect(hostSatisfies('', '3.3.0')).toBe(false);
expect(hostSatisfies('>=4.0.0 <3.0.0', '3.3.0')).toBe(false);
});
it('reads the running version from APP_VERSION, falling back to the package version', () => {
process.env.APP_VERSION = '3.9.1';
expect(hostVersion()).toBe('3.9.1');
expect(hostSatisfies('>=3.2.0 <4.0.0')).toBe(true);
expect(hostSatisfies('>=4.0.0')).toBe(false);
delete process.env.APP_VERSION;
expect(hostVersion()).toMatch(/^\d+\.\d+\.\d+/); // package.json
});
});
/**
* TREK_PLUGINS_IGNORE_TREK_RANGE: an operator's explicit "install it anyway" for a plugin
* whose author has not updated its `trek` range. The gates warn instead of refusing, but
* the truth (hostSatisfies) never changes — the UI still has to say the plugin is
* outside its range.
*/
describe('range bypass (TREK_PLUGINS_IGNORE_TREK_RANGE)', () => {
it('is off by default and only an explicit truthy value turns it on', () => {
expect(trekRangeBypassed()).toBe(false);
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '0';
expect(trekRangeBypassed()).toBe(false);
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1';
expect(trekRangeBypassed()).toBe(true);
});
it('reports a bypassed mismatch only when the bypass is on AND the range does not admit the host', () => {
process.env.APP_VERSION = '3.3.0';
expect(bypassedRange('>=4.0.0')).toBeNull(); // off → the gate refuses, nothing to report
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = 'true';
expect(bypassedRange('>=4.0.0')).toEqual({ trekRange: '>=4.0.0', hostVersion: '3.3.0' });
expect(bypassedRange('>=3.2.0 <4.0.0')).toBeNull(); // fits → not a bypass, a normal install
});
it('covers a plugin that declared no range at all — the same author oversight', () => {
process.env.APP_VERSION = '3.3.0';
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1';
expect(bypassedRange(null)).toEqual({ trekRange: null, hostVersion: '3.3.0' });
expect(bypassedRange(undefined)).toEqual({ trekRange: null, hostVersion: '3.3.0' });
});
it('leaves hostSatisfies telling the truth while bypassed', () => {
process.env.APP_VERSION = '3.3.0';
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1';
expect(hostSatisfies('>=4.0.0')).toBe(false);
});
});