mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(plugins): TREK_PLUGINS_IGNORE_TREK_RANGE turns the version gate into a warning (#2226)
* feat(plugins): TREK_PLUGINS_IGNORE_TREK_RANGE turns the version gate into a warning A plugin whose author has not updated its `trek` range for the running TREK can now be installed, updated and activated when the operator sets TREK_PLUGINS_IGNORE_TREK_RANGE to a truthy value. Every gate (registry picker, post-extraction re-check, sideload, dev-link, activation) warns instead of refusing; a missing range is tolerated the same way; "install latest" takes the newest published version. The plugin-API version gate is not lifted and the Discover compatibility verdict stays truthful. The admin is warned at every step: each bypass is logged, install/upload/link/ update responses carry a `trekRangeBypassed` marker, and both admin panels show a "Version checks off" pill, an "Install anyway" confirm dialog before a registry install, the same notice after a path that could not ask first, and a persistent chip on the row while the plugin runs outside its range. Nine new i18n keys in every locale; wiki pages updated (Environment-Variables, Admin-Plugins, Plugin-Development, Security-Hardening). * refactor(plugins): one useRangeBypass hook behind both admin shells SonarQube flagged PR 2226 at 10.8% duplicated new code: the range-bypass logic had been added to the desktop and phone plugin panels as identical copies. The marker/warning types, the row chip, the "Install anyway" offer, the dialog copy and the confirm-or-notice state now live in useRangeBypass (next to useInstanceSettings, the existing shared-logic precedent); each panel keeps only its own dialog/sheet markup. * docs(claude): record the SonarQube gate rules and the no-English-placeholder i18n rule The CI SonarCloud gate (Sonar way, on new code: >=80% coverage, <=3% duplication, A ratings, hotspots reviewed) was only discoverable by waiting 20-25 minutes for the workflow. The root CLAUDE.md now states the conditions, the desktop/mobile shell-mirror duplication trap and its shared-hook fix, and how to check a PR with the sonarqube MCP first; client and server CLAUDE.md point at it. shared/CLAUDE.md says every locale needs a real translation — parity cannot tell an English placeholder from a translation. * docs(claude): keep CLAUDE.md to commands, invariants, gates and philosophy Verified every concrete claim in the five CLAUDE.md files against the code and fixed the stale ones: the websocket server is the Nest realtime gateway, MCP tools live in <domain>.mcp.ts files, the client "native shell" section described code that was never written, offline maps also prefetch vector basemaps, plus several renamed files and wrong table names. Then trimmed everything that drifts on its own: dates, migration history, counts, and inventories of stores, slices, domains, sync files and CLI commands. What stays is the commands, the invariants with the file that enforces each one, what CI actually runs, the reference implementations to copy, and the rules for new code. Each file now opens with that rule.
This commit is contained in:
@@ -103,6 +103,26 @@ describe('PluginRuntimeService dev-link', () => {
|
||||
await expect(runtime.link(writeSource('nativeplug', { native: true }))).rejects.toThrow(/native/);
|
||||
});
|
||||
|
||||
it('links a dir outside its TREK range when TREK_PLUGINS_IGNORE_TREK_RANGE is set, and says so', async () => {
|
||||
process.env.APP_VERSION = '3.3.0';
|
||||
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1';
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
await expect(runtime.link(writeSource('oldplug-bypass', { trek: '>=2.0.0 <3.0.0' }))).resolves.toMatchObject({
|
||||
id: 'oldplug-bypass', trekRangeBypassed: { trekRange: '>=2.0.0 <3.0.0', hostVersion: '3.3.0' },
|
||||
});
|
||||
await expect(runtime.link(writeSource('rangeless-bypass', { trek: '' }))).resolves.toMatchObject({
|
||||
id: 'rangeless-bypass', trekRangeBypassed: { trekRange: null, hostVersion: '3.3.0' },
|
||||
});
|
||||
// A dir that fits is a plain link — no marker to alarm anyone with.
|
||||
await expect(runtime.link(writeSource('fits-bypass'))).resolves.toMatchObject({ trekRangeBypassed: null });
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
delete process.env.APP_VERSION;
|
||||
delete process.env.TREK_PLUGINS_IGNORE_TREK_RANGE;
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a local dir whose TREK range this server does not satisfy', async () => {
|
||||
// Dev-link is the third install front door, and it hands TREK code to RUN against real
|
||||
// data — the fact that the author is standing right there is not a reason to skip the
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* HTTP route works through the host→child invoke path, using its own isolated
|
||||
* db. Proves the full activate → route → deactivate loop.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach, beforeEach, vi } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
@@ -712,6 +712,36 @@ describe('TREK host-version gating', () => {
|
||||
cleanup('boot-broken');
|
||||
});
|
||||
|
||||
describe('with TREK_PLUGINS_IGNORE_TREK_RANGE set', () => {
|
||||
beforeEach(() => { process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1'; });
|
||||
afterEach(() => { delete process.env.TREK_PLUGINS_IGNORE_TREK_RANGE; });
|
||||
|
||||
it('activates a plugin this TREK has outgrown, warning in the log instead of refusing', async () => {
|
||||
process.env.APP_VERSION = '4.0.0';
|
||||
seedRanged('outgrown-ok', '>=3.2.0 <4.0.0');
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const rt = createPluginRuntime(new DatabaseService(dbConn));
|
||||
await rt.activate('outgrown-ok');
|
||||
expect(rt.isActive('outgrown-ok')).toBe(true);
|
||||
expect(warn.mock.calls.some((c) => /outgrown-ok.*>=3\.2\.0 <4\.0\.0.*4\.0\.0/.test(String(c[0])))).toBe(true);
|
||||
warn.mockRestore();
|
||||
await rt.deactivate('outgrown-ok');
|
||||
cleanup('outgrown-ok');
|
||||
});
|
||||
|
||||
it('activates a plugin that never declared a range', async () => {
|
||||
process.env.APP_VERSION = '3.3.0';
|
||||
seedRanged('undeclared-ok', null);
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const rt = createPluginRuntime(new DatabaseService(dbConn));
|
||||
await rt.activate('undeclared-ok');
|
||||
expect(rt.isActive('undeclared-ok')).toBe(true);
|
||||
warn.mockRestore();
|
||||
await rt.deactivate('undeclared-ok');
|
||||
cleanup('undeclared-ok');
|
||||
});
|
||||
});
|
||||
|
||||
it('checks the version BEFORE permissions — an unrunnable plugin gets no consent dialog', async () => {
|
||||
// Consenting to permissions would not make it start, so offering the dialog is a lie.
|
||||
process.env.APP_VERSION = '4.0.0';
|
||||
@@ -749,6 +779,17 @@ describe('plugin-API version gating', () => {
|
||||
cleanup('future-api');
|
||||
});
|
||||
|
||||
it('still refuses a newer apiVersion when the TREK-range bypass is on — it is not a range problem', async () => {
|
||||
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1';
|
||||
try {
|
||||
seedApi('future-api-bypass', 2);
|
||||
await expect(createPluginRuntime(new DatabaseService(dbConn)).activate('future-api-bypass')).rejects.toMatchObject({ code: 'API_VERSION_INCOMPATIBLE' });
|
||||
} finally {
|
||||
delete process.env.TREK_PLUGINS_IGNORE_TREK_RANGE;
|
||||
cleanup('future-api-bypass');
|
||||
}
|
||||
});
|
||||
|
||||
it('activates normally when apiVersion is within what this TREK supports', async () => {
|
||||
seedApi('current-api', 1);
|
||||
const rt = createPluginRuntime(new DatabaseService(dbConn));
|
||||
|
||||
@@ -387,7 +387,7 @@ describe('PluginRegistryService', () => {
|
||||
safeDownload.mockResolvedValue({ bytes: artifact, sha256: sha });
|
||||
|
||||
const out = await svc.install('flight-tracker');
|
||||
expect(out).toEqual({ id: 'flight-tracker', version: '1.0.0' });
|
||||
expect(out).toEqual({ id: 'flight-tracker', version: '1.0.0', trekRangeBypassed: null });
|
||||
|
||||
// moved into place + registered inactive with provenance
|
||||
expect(fs.existsSync(path.join(codeRoot, 'flight-tracker', 'trek-plugin.json'))).toBe(true);
|
||||
@@ -496,7 +496,7 @@ describe('PluginRegistryService', () => {
|
||||
it('installs a signed plugin and pins the author key (TOFU)', async () => {
|
||||
const k = signingKey();
|
||||
stageSignedArtifact(k.pubB64, k.sign);
|
||||
await expect(svc.install('flight-tracker')).resolves.toEqual({ id: 'flight-tracker', version: '1.0.0' });
|
||||
await expect(svc.install('flight-tracker')).resolves.toEqual({ id: 'flight-tracker', version: '1.0.0', trekRangeBypassed: null });
|
||||
const row = testDb.prepare("SELECT author_pubkey FROM plugins WHERE id='flight-tracker'").get() as { author_pubkey: string };
|
||||
expect(row.author_pubkey).toBe(k.pubB64);
|
||||
});
|
||||
@@ -865,6 +865,91 @@ describe('TREK-version gating on install', () => {
|
||||
await expect(svc.install('multi')).rejects.toMatchObject({ code: 'TREK_VERSION_INCOMPATIBLE' });
|
||||
});
|
||||
|
||||
/**
|
||||
* TREK_PLUGINS_IGNORE_TREK_RANGE: the operator's explicit "install it anyway". Every gate
|
||||
* above turns into a warning + a `trekRangeBypassed` marker in the response, so the admin
|
||||
* is told the author never vouched for this TREK. The registry's compat VERDICT stays
|
||||
* truthful — the UI needs it to say "outside its range" while still offering the button.
|
||||
*/
|
||||
describe('range bypass (TREK_PLUGINS_IGNORE_TREK_RANGE)', () => {
|
||||
beforeEach(() => { process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1'; });
|
||||
afterEach(() => { delete process.env.TREK_PLUGINS_IGNORE_TREK_RANGE; });
|
||||
|
||||
it('install-latest fetches the newest PUBLISHED version and reports the ignored range', async () => {
|
||||
process.env.APP_VERSION = '3.3.0'; stubMulti();
|
||||
stageMulti('2.1.0', 0);
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
await expect(svc.install('multi')).resolves.toEqual({
|
||||
id: 'multi', version: '2.1.0', trekRangeBypassed: { trekRange: '>=3.4.0 <4.0.0', hostVersion: '3.3.0' },
|
||||
});
|
||||
expect(warn.mock.calls.some((c) => /multi.*>=3\.4\.0 <4\.0\.0.*3\.3\.0/.test(String(c[0])))).toBe(true);
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
it('installs an explicitly-pinned version this TREK cannot run', async () => {
|
||||
process.env.APP_VERSION = '3.3.0'; stubMulti();
|
||||
stageMulti('2.1.0', 0);
|
||||
await expect(svc.install('multi', { version: '2.1.0' })).resolves.toMatchObject({
|
||||
version: '2.1.0', trekRangeBypassed: { trekRange: '>=3.4.0 <4.0.0', hostVersion: '3.3.0' },
|
||||
});
|
||||
});
|
||||
|
||||
it('a version that fits is a normal install — no marker, nothing to warn about', async () => {
|
||||
process.env.APP_VERSION = '3.5.0'; stubMulti();
|
||||
stageMulti('2.1.0', 0);
|
||||
await expect(svc.install('multi')).resolves.toEqual({ id: 'multi', version: '2.1.0', trekRangeBypassed: null });
|
||||
});
|
||||
|
||||
it('browse keeps telling the truth about compatibility', async () => {
|
||||
process.env.APP_VERSION = '3.3.0'; stubMulti();
|
||||
const [item] = await svc.browse(true);
|
||||
expect(item).toMatchObject({ compatible: false, latestCompatible: '2.0.0', hostVersion: '3.3.0' });
|
||||
});
|
||||
|
||||
it("re-gate on the ARTIFACT's range warns instead of refusing, with the artifact's range", async () => {
|
||||
process.env.APP_VERSION = '3.9.0';
|
||||
__clearRegistryCacheForTests();
|
||||
const artifact = makeArtifact({ id: 'flight-tracker', name: 'Flight', version: '1.0.0', type: 'widget', trek: '>=3.2.0 <3.4.0' });
|
||||
const sha = createHash('sha256').update(artifact).digest('hex');
|
||||
REGISTRY.plugins[0].versions[0].sha256 = sha;
|
||||
safeDownload.mockResolvedValue({ bytes: artifact, sha256: sha });
|
||||
await expect(svc.install('flight-tracker')).resolves.toMatchObject({
|
||||
id: 'flight-tracker', trekRangeBypassed: { trekRange: '>=3.2.0 <3.4.0', hostVersion: '3.9.0' },
|
||||
});
|
||||
expect(fs.existsSync(path.join(codeRoot, 'flight-tracker'))).toBe(true);
|
||||
});
|
||||
|
||||
it('installs an artifact that declares no range at all, saying so', async () => {
|
||||
process.env.APP_VERSION = '3.9.0';
|
||||
__clearRegistryCacheForTests();
|
||||
const artifact = makeArtifact({ id: 'flight-tracker', name: 'Flight', version: '1.0.0', type: 'widget', trek: undefined });
|
||||
const sha = createHash('sha256').update(artifact).digest('hex');
|
||||
REGISTRY.plugins[0].versions[0].sha256 = sha;
|
||||
safeDownload.mockResolvedValue({ bytes: artifact, sha256: sha });
|
||||
await expect(svc.install('flight-tracker')).resolves.toMatchObject({
|
||||
trekRangeBypassed: { trekRange: null, hostVersion: '3.9.0' },
|
||||
});
|
||||
});
|
||||
|
||||
it('sideload: stages an incompatible or rangeless archive and reports it', () => {
|
||||
process.env.APP_VERSION = '3.9.0';
|
||||
const old = svc.stageUpload(makeArtifact({ id: 'my-upload', name: 'Up', version: '1.0.0', type: 'integration', trek: '>=3.0.0 <3.5.0' }));
|
||||
expect(old).toMatchObject({ id: 'my-upload', trekRangeBypassed: { trekRange: '>=3.0.0 <3.5.0', hostVersion: '3.9.0' } });
|
||||
fs.rmSync(old.stagingDir, { recursive: true, force: true });
|
||||
const rangeless = svc.stageUpload(makeArtifact({ id: 'my-upload', name: 'Up', version: '1.0.0', type: 'integration', trek: undefined }));
|
||||
expect(rangeless).toMatchObject({ trekRangeBypassed: { trekRange: null, hostVersion: '3.9.0' } });
|
||||
fs.rmSync(rangeless.stagingDir, { recursive: true, force: true });
|
||||
const fits = svc.stageUpload(makeArtifact({ id: 'my-upload', name: 'Up', version: '1.0.0', type: 'integration' }));
|
||||
expect(fits.trekRangeBypassed).toBeNull();
|
||||
fs.rmSync(fits.stagingDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('does NOT lift the plugin-API version gate — that one is a real ABI break', () => {
|
||||
const bytes = makeArtifact({ id: 'my-upload', name: 'Up', version: '1.0.0', type: 'integration', apiVersion: 2 });
|
||||
expect(() => svc.stageUpload(bytes)).toThrow('plugin requires plugin-API v2; this TREK supports v1');
|
||||
});
|
||||
});
|
||||
|
||||
it('reports the newest version this TREK CAN run, so the UI can offer it', async () => {
|
||||
process.env.APP_VERSION = '3.3.0'; stubMulti();
|
||||
const [item] = await svc.browse(true);
|
||||
|
||||
@@ -188,6 +188,14 @@ describe('derivePlugins', () => {
|
||||
expect(derivePlugins({}).permissionsOff).toBe(false);
|
||||
});
|
||||
|
||||
it('ignoreTrekRange is off unless explicitly truthy — the version gate must never bypass by accident', () => {
|
||||
expect(derivePlugins({}).ignoreTrekRange).toBe(false);
|
||||
expect(derivePlugins({ TREK_PLUGINS_IGNORE_TREK_RANGE: '' }).ignoreTrekRange).toBe(false);
|
||||
expect(derivePlugins({ TREK_PLUGINS_IGNORE_TREK_RANGE: 'off' }).ignoreTrekRange).toBe(false);
|
||||
expect(derivePlugins({ TREK_PLUGINS_IGNORE_TREK_RANGE: '1' }).ignoreTrekRange).toBe(true);
|
||||
expect(derivePlugins({ TREK_PLUGINS_IGNORE_TREK_RANGE: 'TRUE' }).ignoreTrekRange).toBe(true);
|
||||
});
|
||||
|
||||
it('allowPrivateEgress is a bool (supervisor normalizes it to the literal "on" for the child)', () => {
|
||||
expect(derivePlugins({ TREK_PLUGIN_ALLOW_PRIVATE_EGRESS: 'on' }).allowPrivateEgress).toBe(true);
|
||||
expect(derivePlugins({ TREK_PLUGIN_ALLOW_PRIVATE_EGRESS: 'true' }).allowPrivateEgress).toBe(true);
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
isValidTrekRange,
|
||||
minTrekOf,
|
||||
hostSatisfies,
|
||||
trekRangeBypassed,
|
||||
bypassedRange,
|
||||
__resetHostWarningForTests,
|
||||
} from '../../../src/nest/plugins/install/host-compat';
|
||||
|
||||
@@ -18,6 +20,7 @@ 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();
|
||||
});
|
||||
|
||||
@@ -114,3 +117,40 @@ describe('hostSatisfies', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,16 +105,16 @@ describe('signature refusal codes survive to the client', () => {
|
||||
// must install exactly that one — the compat gate stays server-side in selectVersion.
|
||||
describe('POST :id/update — explicit version', () => {
|
||||
it('forwards the requested version to the runtime', async () => {
|
||||
vi.mocked(runtime.update).mockResolvedValue({ version: '1.2.0', activated: true, newPermissions: [], newEgress: [] });
|
||||
vi.mocked(runtime.update).mockResolvedValue({ version: '1.2.0', activated: true, newPermissions: [], newEgress: [], trekRangeBypassed: null });
|
||||
|
||||
const res = await controller.update('flight-tracker', { version: '1.2.0' });
|
||||
|
||||
expect(vi.mocked(runtime.update).mock.calls[0]).toEqual(['flight-tracker', { version: '1.2.0' }]);
|
||||
expect(res).toEqual({ version: '1.2.0', activated: true, newPermissions: [], newEgress: [] });
|
||||
expect(res).toEqual({ version: '1.2.0', activated: true, newPermissions: [], newEgress: [], trekRangeBypassed: null });
|
||||
});
|
||||
|
||||
it('an omitted body keeps the default newest-compatible resolution', async () => {
|
||||
vi.mocked(runtime.update).mockResolvedValue({ version: '2.0.0', activated: true, newPermissions: [], newEgress: [] });
|
||||
vi.mocked(runtime.update).mockResolvedValue({ version: '2.0.0', activated: true, newPermissions: [], newEgress: [], trekRangeBypassed: null });
|
||||
|
||||
await controller.update('flight-tracker');
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('POST :id/update — explicit version', () => {
|
||||
// must not touch it.
|
||||
describe('update hold wiring', () => {
|
||||
it('an explicit-version update recomputes the hold as deliberate', async () => {
|
||||
vi.mocked(runtime.update).mockResolvedValue({ version: '1.2.0', activated: true, newPermissions: [], newEgress: [] });
|
||||
vi.mocked(runtime.update).mockResolvedValue({ version: '1.2.0', activated: true, newPermissions: [], newEgress: [], trekRangeBypassed: null });
|
||||
|
||||
await controller.update('flight-tracker', { version: '1.2.0' });
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('update hold wiring', () => {
|
||||
});
|
||||
|
||||
it('a default update recomputes as non-deliberate (clears a stale hold)', async () => {
|
||||
vi.mocked(runtime.update).mockResolvedValue({ version: '2.0.0', activated: true, newPermissions: [], newEgress: [] });
|
||||
vi.mocked(runtime.update).mockResolvedValue({ version: '2.0.0', activated: true, newPermissions: [], newEgress: [], trekRangeBypassed: null });
|
||||
|
||||
await controller.update('flight-tracker');
|
||||
|
||||
@@ -144,7 +144,7 @@ describe('update hold wiring', () => {
|
||||
});
|
||||
|
||||
it('an explicit-version install recomputes the hold as deliberate', async () => {
|
||||
vi.mocked(registry.install).mockResolvedValue({ id: 'flight-tracker', version: '1.2.0' });
|
||||
vi.mocked(registry.install).mockResolvedValue({ id: 'flight-tracker', version: '1.2.0', trekRangeBypassed: null });
|
||||
|
||||
await controller.install({ id: 'flight-tracker', version: '1.2.0' });
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('update hold wiring', () => {
|
||||
});
|
||||
|
||||
it('a dependency-resolution install does NOT touch the hold', async () => {
|
||||
vi.mocked(registry.installWithDependencies).mockResolvedValue({ installed: ['flight-tracker'], requiredAddons: [] });
|
||||
vi.mocked(registry.installWithDependencies).mockResolvedValue({ installed: ['flight-tracker'], requiredAddons: [], trekRangeBypassed: null });
|
||||
|
||||
await controller.install({ id: 'flight-tracker', withDependencies: true });
|
||||
|
||||
|
||||
@@ -48,6 +48,47 @@ describe('PluginsService.list', () => {
|
||||
expect(out.plugins[0]).toMatchObject({ id: 'flight', name: 'Flight', status: 'inactive' });
|
||||
});
|
||||
|
||||
describe('TREK-range bypass', () => {
|
||||
const APP_VERSION = process.env.APP_VERSION;
|
||||
afterEach(() => {
|
||||
delete process.env.TREK_PLUGINS_IGNORE_TREK_RANGE;
|
||||
if (APP_VERSION === undefined) delete process.env.APP_VERSION;
|
||||
else process.env.APP_VERSION = APP_VERSION;
|
||||
});
|
||||
const seed = () =>
|
||||
testDb
|
||||
.prepare("INSERT INTO plugins (id, name, type, status, version, trek_range) VALUES ('old','Old','widget','inactive','1.0.0','>=3.0.0 <4.0.0')")
|
||||
.run();
|
||||
|
||||
it('reports the switch off and an outgrown plugin as hostIncompatible by default', () => {
|
||||
process.env.APP_VERSION = '4.1.0';
|
||||
seed();
|
||||
const out = new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
expect(out.ignoreTrekRange).toBe(false);
|
||||
expect(out.plugins[0]).toMatchObject({ dependencyStatus: 'hostIncompatible', trekRangeBypassed: null });
|
||||
});
|
||||
|
||||
it('with the switch on, the plugin may activate but the row still says it is outside its range', () => {
|
||||
process.env.APP_VERSION = '4.1.0';
|
||||
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1';
|
||||
seed();
|
||||
const out = new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
expect(out.ignoreTrekRange).toBe(true);
|
||||
expect(out.plugins[0]).toMatchObject({
|
||||
dependencyStatus: 'ok',
|
||||
trekRangeBypassed: { trekRange: '>=3.0.0 <4.0.0', hostVersion: '4.1.0' },
|
||||
});
|
||||
});
|
||||
|
||||
it('a plugin inside its range carries no marker even with the switch on', () => {
|
||||
process.env.APP_VERSION = '3.5.0';
|
||||
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1';
|
||||
seed();
|
||||
const out = new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
expect(out.plugins[0]).toMatchObject({ dependencyStatus: 'ok', trekRangeBypassed: null });
|
||||
});
|
||||
});
|
||||
|
||||
it('surfaces updateHold as a boolean (held plugins leave the update banner)', () => {
|
||||
testDb
|
||||
.prepare("INSERT INTO plugins (id, name, type, status, version, update_hold) VALUES ('held','Held','widget','inactive','1.0.0',1)")
|
||||
|
||||
Reference in New Issue
Block a user