mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
refactor(plugins): install-time gates, the capability-audit hash chain and the registry through repositories
This commit is contained in:
@@ -124,12 +124,14 @@ afterAll(async () => {
|
||||
describe('plugin boot vs registry scan ordering', () => {
|
||||
it('BOOT-REG-001 a plugin enabled before a restart activates cleanly even though the registry scan runs in a LATER provider\'s onModuleInit', async () => {
|
||||
const dbs = new DatabaseService(dbConn);
|
||||
const userSettings = new PluginUserSettingsService(dbs);
|
||||
// Empty at construction — exactly what PluginRpcRegistryService is before its
|
||||
// own onModuleInit scan has run.
|
||||
const registry = new PluginRpcRegistry();
|
||||
const hostFactory = new PluginRpcHostFactory(dbs, registry as unknown as PluginRpcRegistryService);
|
||||
t = await createTestOrm(dbConn);
|
||||
// Plan 3j Task 3: `PluginRpcHostFactory`'s `audit` callback now takes
|
||||
// `PluginCapabilityAuditRepository`, not `DatabaseService`.
|
||||
const hostFactory = new PluginRpcHostFactory(t.repo(PluginCapabilityAudit), registry as unknown as PluginRpcRegistryService);
|
||||
const userSettings = new PluginUserSettingsService((t as TestOrm).repo(PluginSettingsFields), (t as TestOrm).repo(PluginUserConfig));
|
||||
const auditLogRepo = t.repo(AuditLog);
|
||||
const usersRepo = t.repo(Users);
|
||||
const addonsService = await createTestAddonsService(dbConn, dbs);
|
||||
@@ -236,12 +238,12 @@ describe('plugin boot vs registry scan ordering', () => {
|
||||
let mod2: TestingModule | undefined;
|
||||
try {
|
||||
const dbs2 = new DatabaseService(dbConn);
|
||||
const userSettings2 = new PluginUserSettingsService(dbs2);
|
||||
const userSettings2 = new PluginUserSettingsService(t2.repo(PluginSettingsFields), t2.repo(PluginUserConfig));
|
||||
const registry2 = new PluginRpcRegistry();
|
||||
// Registry already scanned — this test is about the addon check, not the
|
||||
// registry-ordering bug BOOT-REG-001 pins.
|
||||
registry2.register(new DbRpc(userSettings2));
|
||||
const hostFactory2 = new PluginRpcHostFactory(dbs2, registry2 as unknown as PluginRpcRegistryService);
|
||||
const hostFactory2 = new PluginRpcHostFactory(t2.repo(PluginCapabilityAudit), registry2 as unknown as PluginRpcRegistryService);
|
||||
// Built directly on t2 (not createTestAddonsService's sharedTestOrm) so
|
||||
// its repositories share t2's allowGlobalContext: false ORM.
|
||||
const addonsService2 = new AddonsService(
|
||||
|
||||
@@ -9,31 +9,24 @@ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const { testDb } = vi.hoisted(() => {
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database(':memory:');
|
||||
db.exec(`CREATE TABLE plugins (
|
||||
id TEXT PRIMARY KEY, name TEXT, description TEXT, type TEXT, icon TEXT, version TEXT, api_version INTEGER,
|
||||
min_trek_version TEXT, trek_range TEXT, permissions TEXT DEFAULT '[]', capabilities TEXT DEFAULT '{}', dependencies TEXT DEFAULT '{}',
|
||||
operator_egress INTEGER DEFAULT 0, granted_permissions TEXT DEFAULT '', status TEXT, enabled INTEGER DEFAULT 0, config TEXT DEFAULT '{}',
|
||||
source_repo TEXT, source_commit TEXT, sha256 TEXT, author_pubkey TEXT, reviewed_at TEXT, last_error TEXT, updated_at TEXT,
|
||||
update_block_code TEXT, update_block_detail TEXT, update_block_version TEXT);
|
||||
CREATE TABLE plugin_error_log (id INTEGER PRIMARY KEY AUTOINCREMENT, plugin_id TEXT, level TEXT, message TEXT, ts TEXT);
|
||||
CREATE TABLE plugin_settings_fields (plugin_id TEXT, field_key TEXT, label TEXT, input_type TEXT, placeholder TEXT, hint TEXT, required INTEGER, secret INTEGER, scope TEXT, options TEXT, oauth_config TEXT, default_value TEXT, sort_order INTEGER);
|
||||
CREATE TABLE settings (user_id INTEGER, key TEXT, value TEXT);
|
||||
CREATE TABLE plugin_entity_metadata (id INTEGER PRIMARY KEY AUTOINCREMENT, plugin_id TEXT, entity_type TEXT, entity_id INTEGER, key TEXT, value TEXT, updated_at TEXT);
|
||||
CREATE TABLE addons (id TEXT PRIMARY KEY, enabled INTEGER DEFAULT 0);`);
|
||||
return { testDb: db };
|
||||
});
|
||||
vi.mock('../../../src/db/database', () => ({ db: testDb, canAccessTrip: () => undefined }));
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
// Plan 3j Task 3: `discoverPlugins` (called from `runtime.link`) is repository-backed
|
||||
// now (DI1–DI8) — a native `em.insert()` writes every `Opt`-defaulted entity column
|
||||
// (`sort_order`, `installed_at`, `crash_count`, `update_hold`, …), not only the ones
|
||||
// the legacy raw `INSERT` statement named, so a hand-trimmed `:memory:` table missing
|
||||
// those columns throws inside `discoverPlugins`'s own try/catch (silently: the plugin
|
||||
// lands in `skipped`, not `discovered`). A real MikroORM over the full migrated schema
|
||||
// (`createSnapshotTestDb` + `createTestOrm`) replaces the old hand-rolled table set,
|
||||
// same fix `registry.test.ts` needed.
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
vi.mock('../../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { PluginRuntimeService } from '../../../src/nest/plugins/plugin-runtime.service';
|
||||
import { createPluginRuntime } from '../../helpers/plugin-host';
|
||||
|
||||
const testDb = createSnapshotTestDb();
|
||||
const dbConn = testDb;
|
||||
|
||||
let codeRoot: string;
|
||||
let dataRoot: string;
|
||||
let srcRoot: string; // the "developer's" source lives OUTSIDE the plugins volume
|
||||
@@ -71,6 +64,7 @@ afterAll(async () => {
|
||||
delete process.env.TREK_PLUGINS_DATA_DIR;
|
||||
delete process.env.TREK_PLUGINS_ENABLED;
|
||||
delete process.env.TREK_PLUGINS_DEV_LINK;
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
describe('PluginRuntimeService dev-link', () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* install a pinned version through the full verify -> extract -> validate ->
|
||||
* move -> register pipeline (with the network download mocked).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, afterAll } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
@@ -16,21 +16,20 @@ vi.mock('../../../src/nest/plugins/install/safe-fetch', async (orig) => ({
|
||||
safeDownload,
|
||||
}));
|
||||
|
||||
const { testDb } = vi.hoisted(() => {
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE plugins (id TEXT PRIMARY KEY, name TEXT, description TEXT, type TEXT, icon TEXT, version TEXT,
|
||||
api_version INTEGER, min_trek_version TEXT, trek_range TEXT, permissions TEXT, capabilities TEXT DEFAULT '{}', dependencies TEXT DEFAULT '{}', operator_egress INTEGER DEFAULT 0, granted_permissions TEXT, status TEXT, enabled INTEGER DEFAULT 0, config TEXT,
|
||||
source_repo TEXT, source_commit TEXT, sha256 TEXT, reviewed_at TEXT, author_pubkey TEXT, updated_at TEXT,
|
||||
update_block_code TEXT, update_block_detail TEXT, update_block_version TEXT, update_hold INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE plugin_settings_fields (plugin_id TEXT, field_key TEXT, label TEXT, input_type TEXT, placeholder TEXT, hint TEXT, required INTEGER, secret INTEGER, scope TEXT, options TEXT, oauth_config TEXT, default_value TEXT, sort_order INTEGER);
|
||||
CREATE TABLE plugin_error_log (id INTEGER PRIMARY KEY AUTOINCREMENT, plugin_id TEXT, level TEXT, message TEXT, ts TEXT);`);
|
||||
return { testDb: db };
|
||||
});
|
||||
vi.mock('../../../src/db/database', () => ({ db: testDb, canAccessTrip: () => undefined }));
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
// Plan 3j Task 3: `PluginRegistryService`'s own statements (RG#) and its
|
||||
// `discoverPlugins`/`setUpdateBlock`/`clearUpdateBlock` calls are all
|
||||
// repository-backed now — no more raw `DatabaseService`/`db/database` mock.
|
||||
// A real MikroORM over the full migrated schema (`createSnapshotTestDb` +
|
||||
// `createTestOrm`) replaces the old hand-rolled `:memory:` table set; every
|
||||
// `testDb.prepare(...)` fixture/assertion below still works unchanged since
|
||||
// the real schema is a strict superset of the old one's columns.
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { Plugins } from '../../../src/db/entities/Plugins.entity';
|
||||
import { PluginActions } from '../../../src/db/entities/PluginActions.entity';
|
||||
import { PluginSettingsFields } from '../../../src/db/entities/PluginSettingsFields.entity';
|
||||
import { PluginErrorLog } from '../../../src/db/entities/PluginErrorLog.entity';
|
||||
|
||||
import { PluginRegistryService, RegistryError, __clearRegistryCacheForTests } from '../../../src/nest/plugins/registry/registry.service';
|
||||
import type { ManifestPreview } from '../../../src/nest/plugins/registry/registry.service';
|
||||
@@ -77,19 +76,31 @@ const REGISTRY = {
|
||||
],
|
||||
};
|
||||
|
||||
const testDb = createSnapshotTestDb();
|
||||
let t: TestOrm;
|
||||
|
||||
let dataRoot: string;
|
||||
let codeRoot: string;
|
||||
let svc: PluginRegistryService;
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await createTestOrm(testDb);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await t.close();
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'reg-data-'));
|
||||
codeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'reg-code-'));
|
||||
process.env.TREK_PLUGINS_DATA_DIR = dataRoot;
|
||||
process.env.TREK_PLUGINS_DIR = codeRoot;
|
||||
testDb.exec('DELETE FROM plugins; DELETE FROM plugin_settings_fields; DELETE FROM plugin_error_log');
|
||||
resetTestDb(testDb);
|
||||
t.clear();
|
||||
__clearRegistryCacheForTests();
|
||||
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, json: async () => REGISTRY }) as unknown as Response));
|
||||
svc = new PluginRegistryService(new DatabaseService(dbConn));
|
||||
svc = new PluginRegistryService(t.repo(Plugins), t.repo(PluginActions), t.repo(PluginSettingsFields), t.repo(PluginErrorLog));
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
@@ -275,7 +286,7 @@ describe('PluginRegistryService', () => {
|
||||
],
|
||||
};
|
||||
const seedRow = (hold = 0) =>
|
||||
testDb.prepare("INSERT INTO plugins (id, status, enabled, update_hold) VALUES ('flight-tracker','inactive',0,?)").run(hold);
|
||||
testDb.prepare("INSERT INTO plugins (id, name, status, enabled, update_hold) VALUES ('flight-tracker','Flight','inactive',0,?)").run(hold);
|
||||
const holdInDb = () =>
|
||||
(testDb.prepare("SELECT update_hold FROM plugins WHERE id='flight-tracker'").get() as { update_hold: number }).update_hold;
|
||||
|
||||
@@ -377,7 +388,7 @@ describe('PluginRegistryService', () => {
|
||||
it('fetchRegistry soft-fails to an empty registry on a cold cache', async () => {
|
||||
__clearRegistryCacheForTests();
|
||||
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('offline'); }));
|
||||
expect((await new PluginRegistryService(new DatabaseService(dbConn)).fetchRegistry()).plugins).toEqual([]);
|
||||
expect((await new PluginRegistryService(t.repo(Plugins), t.repo(PluginActions), t.repo(PluginSettingsFields), t.repo(PluginErrorLog)).fetchRegistry()).plugins).toEqual([]);
|
||||
});
|
||||
|
||||
it('installs a pinned version end to end (verify -> extract -> register inactive)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user