mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
feat(plugins): persist enable-intent across restarts + redesign admin page
The deactivation-on-deploy bug: `status` conflated the admin's ON/OFF intent with runtime health, so a boot crash flipped status to 'error' and the plugin never rebooted after the next deploy. Migration 156 adds an `enabled` flag (admin intent) separate from `status` (runtime health); boot now retries every enabled plugin regardless of last status, and a crash no longer erases the intent. Admin → Plugins redesign: - ON/OFF is a ToggleSwitch bound to `enabled`; runtime health shows separately as a coloured status dot, with the last error inline when it crashed. - "Update → vX" badge when the registry has a newer version (one click updates and reactivates). - Reviewed/unreviewed trust badges, cleaner cards, nicer empty state, registry browser marks already-installed plugins. i18n across all locales.
This commit is contained in:
@@ -12,7 +12,7 @@ const { testDb } = vi.hoisted(() => {
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database(':memory:');
|
||||
db.exec(`CREATE TABLE plugins (
|
||||
id TEXT PRIMARY KEY, status TEXT, permissions TEXT DEFAULT '[]', granted_permissions TEXT DEFAULT '[]',
|
||||
id TEXT PRIMARY KEY, status TEXT, enabled INTEGER DEFAULT 0, permissions TEXT DEFAULT '[]', granted_permissions TEXT DEFAULT '[]',
|
||||
config TEXT DEFAULT '{}', last_error TEXT, updated_at 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, scope TEXT, secret INTEGER);
|
||||
@@ -115,11 +115,19 @@ describe('PluginRuntimeService (M2 end-to-end)', () => {
|
||||
process.env.TREK_PLUGINS_ENABLED = 'true';
|
||||
});
|
||||
|
||||
it('deactivate stops the plugin and marks it inactive', async () => {
|
||||
it('deactivate stops the plugin and clears the enabled intent', async () => {
|
||||
await runtime.deactivate('counter');
|
||||
expect(runtime.isActive('counter')).toBe(false);
|
||||
const row = testDb.prepare("SELECT status FROM plugins WHERE id = 'counter'").get() as { status: string };
|
||||
const row = testDb.prepare("SELECT status, enabled FROM plugins WHERE id = 'counter'").get() as { status: string; enabled: number };
|
||||
expect(row.status).toBe('inactive');
|
||||
expect(row.enabled).toBe(0);
|
||||
});
|
||||
|
||||
it('activate sets the enabled intent so it survives a reboot', async () => {
|
||||
await runtime.deactivate('counter');
|
||||
await runtime.activate('counter');
|
||||
const row = testDb.prepare("SELECT enabled FROM plugins WHERE id = 'counter'").get() as { enabled: number };
|
||||
expect(row.enabled).toBe(1);
|
||||
});
|
||||
|
||||
it('tolerates malformed granted_permissions / config JSON on activate', async () => {
|
||||
@@ -132,10 +140,11 @@ describe('PluginRuntimeService (M2 end-to-end)', () => {
|
||||
await rt.deactivate('messy');
|
||||
});
|
||||
|
||||
it('onModuleInit boots plugins that are active in the DB', async () => {
|
||||
it('onModuleInit boots every ENABLED plugin — even one left in error state', async () => {
|
||||
fs.mkdirSync(path.join(codeRoot, 'booter', 'server'), { recursive: true });
|
||||
fs.writeFileSync(path.join(codeRoot, 'booter', 'server', 'index.js'), 'module.exports = { async onLoad() {} };');
|
||||
testDb.prepare("INSERT INTO plugins (id, status, granted_permissions, config) VALUES ('booter','active','[]','{}')").run();
|
||||
// status='error' from a previous crash, but enabled=1 → must still boot
|
||||
testDb.prepare("INSERT INTO plugins (id, status, enabled, granted_permissions, config) VALUES ('booter','error',1,'[]','{}')").run();
|
||||
|
||||
const rt = new PluginRuntimeService();
|
||||
rt.onModuleInit(); // fire-and-forget spawn
|
||||
@@ -144,6 +153,17 @@ describe('PluginRuntimeService (M2 end-to-end)', () => {
|
||||
await rt.deactivate('booter');
|
||||
});
|
||||
|
||||
it('onModuleInit does NOT boot a disabled plugin', async () => {
|
||||
fs.mkdirSync(path.join(codeRoot, 'sleeper', 'server'), { recursive: true });
|
||||
fs.writeFileSync(path.join(codeRoot, 'sleeper', 'server', 'index.js'), 'module.exports = { async onLoad() {} };');
|
||||
testDb.prepare("INSERT INTO plugins (id, status, enabled, granted_permissions, config) VALUES ('sleeper','inactive',0,'[]','{}')").run();
|
||||
|
||||
const rt = new PluginRuntimeService();
|
||||
rt.onModuleInit();
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
expect(rt.isActive('sleeper')).toBe(false);
|
||||
});
|
||||
|
||||
it('outboundHostsOf extracts declared http:outbound hosts', () => {
|
||||
testDb.prepare("INSERT INTO plugins (id, status, granted_permissions, config) VALUES ('net','inactive','[\"db:own\",\"http:outbound:api.x.com\",\"http:outbound:*.y.com\"]','{}')").run();
|
||||
const rt = new PluginRuntimeService();
|
||||
|
||||
Reference in New Issue
Block a user