Files
stack/packages/bus/tests/plugins-verb.test.mjs
T
jason.woltjeandClaude Opus 5.5 fba879f6a1 mosaic plugins set: a patch changes only the kinds it names; null removes a kind (vikunja:19/309, row 145)
Jason-approved merge, rebased: git diff f4d7b91a9..0e7d1d8b8 from
work/vikunja-19-309, stacked on 305. Business reviewer verdict dfd44af2
approve on 0e7d1d8b8; Jason resolved merge decision 12a20e47 via the CLI.

No conflict. packages/plugins/README.md, which refactor also changed,
merged with the source commit's +/- lines; the other four paths are the
source blobs. Filbert reviews the rebased diff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 17:50:59 -05:00

93 lines
7.6 KiB
JavaScript

// vikunja:19/305 (REQ-PLUG-1): the plug-in switch is a human-only broker action, logged with its
// actor, and a module that doesn't exist is refused. Same path as the limits verbs.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Store } from '../src/store.mjs';
import { Broker } from '../src/broker.mjs';
const biz = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' }, roles: { pm: { authority: { withinRole: [], crossRole: [] } }, cto: { authority: { withinRole: [], crossRole: [] } } } } };
test('plugins and plugins.set refuse every non-human session and a forged actor, and pass only the patch with the verified actor', (t) => {
const root = mkdtempSync(join(tmpdir(), 'plugins-verb-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const calls = [];
const b = new Broker({ store, businesses: biz, organization: (id, verb, args, actor) => (calls.push([id, verb, args, actor]), { ok: true }) });
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const role = b.bindLaunch({ business: 'demo', role: 'pm', run: 'pm-run', harness: 'pi' });
const reader = b.bindReader({ business: 'demo' });
const patch = { revision: 'a'.repeat(64), plugins: { 'search-source': 'searxng-url' } };
for (const cap of [role, reader]) for (const verb of ['plugins', 'plugins.set']) assert.throws(() => b.request(cap, { verb, args: verb === 'plugins' ? {} : patch }), /human-required/);
assert.equal(calls.length, 0);
assert.throws(() => b.request(human, { verb: 'plugins.set', args: { ...patch, actor: 'forged' } }), /invalid-request/);
assert.throws(() => b.request(human, { verb: 'plugins.set', args: { revision: patch.revision } }), /invalid-request/);
assert.throws(() => b.request(human, { verb: 'plugins', args: { x: 1 } }), /invalid-request/);
b.request(human, { verb: 'plugins' });
b.request(human, { verb: 'plugins.set', args: patch });
assert.deepEqual(calls, [['demo', 'plugins', {}, 'jason'], ['demo', 'plugins.set', patch, 'jason']]);
});
test('runtime: the switch changes the organization file, the receipt carries the actor, and a bad shape, an unknown module and a stale revision are refused with nothing written', async (t) => {
const { startBroker } = await import('../src/runtime.mjs');
const { businessDoc, rolesCopy, systemFor, writeJson } = await import('../../business/tests/helpers.mjs');
const root = mkdtempSync(join(tmpdir(), 'plugins-runtime-'));
const rolesDir = rolesCopy(root);
writeJson(join(root, 'businesses/demo.json'), businessDoc(root, 'demo'));
mkdirSync(join(root, 'data'), { mode: 0o700 });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const file = join(root, 'businesses/demo.json');
const log = () => readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
const view = bus.broker.request(human, { verb: 'plugins' });
assert.deepEqual(view.plugins, {});
const done = bus.broker.request(human, { verb: 'plugins.set', args: { revision: view.revision, plugins: { 'search-source': 'searxng-url' } } });
assert.deepEqual(done.plugins, { 'search-source': 'searxng-url' });
assert.deepEqual(JSON.parse(readFileSync(file, 'utf8')).plugins, { 'search-source': 'searxng-url' });
const last = log().at(-1);
assert.deepEqual([last.actor, last.state], ['jason', 'committed']);
assert.deepEqual(last.changes, [{ path: 'plugins', old: null, new: { 'search-source': 'searxng-url' }, oldPresent: false, newPresent: true }]);
const before = readFileSync(file, 'utf8');
const entries = log().length;
const code = (args) => { try { bus.broker.request(human, { verb: 'plugins.set', args }); } catch (e) { return e.code; } return null; };
assert.equal(code({ revision: done.revision, plugins: { 'search-source': 'no-such-module' } }), 'unknown-plugin');
assert.equal(code({ revision: done.revision, plugins: { 'search-source': ['searxng-url'] } }), 'invalid-plugins');
assert.equal(code({ revision: done.revision, plugins: { 'made-up': [] } }), 'invalid-plugins');
assert.equal(code({ revision: view.revision, plugins: { 'search-source': 'harness-builtin' } }), 'revision-conflict');
assert.equal(readFileSync(file, 'utf8'), before);
assert.equal(log().length, entries, 'a refused write logs nothing');
// A real module switches, and the old value is in the receipt.
const next = bus.broker.request(human, { verb: 'plugins.set', args: { revision: done.revision, plugins: { 'search-source': 'harness-builtin' } } });
assert.deepEqual(log().at(-1).changes.map((c) => [c.path, c.old, c.new]), [['plugins.search-source', 'searxng-url', 'harness-builtin']]);
assert.equal(next.plugins['search-source'], 'harness-builtin');
});
test('vikunja:19/309 through the broker, a patch switches the kinds it names and keeps the rest; null removes one kind and is trailed', async (t) => {
const { startBroker } = await import('../src/runtime.mjs');
const { businessDoc, rolesCopy, systemFor, writeJson } = await import('../../business/tests/helpers.mjs');
const root = mkdtempSync(join(tmpdir(), 'plugins-merge-'));
const rolesDir = rolesCopy(root);
writeJson(join(root, 'businesses/demo.json'), { ...businessDoc(root, 'demo'), plugins: { 'usage-source': ['meter'] } });
mkdirSync(join(root, 'data'), { mode: 0o700 });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const log = () => readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
const set = (revision, plugins) => bus.broker.request(human, { verb: 'plugins.set', args: { revision, plugins } });
let view = bus.broker.request(human, { verb: 'plugins' });
assert.deepEqual(view.plugins, { 'usage-source': ['meter'] });
view = set(view.revision, { 'search-source': 'searxng-url' });
assert.deepEqual(view.plugins, { 'usage-source': ['meter'], 'search-source': 'searxng-url' }, 'usage-source kept');
assert.deepEqual(log().at(-1).changes.map((c) => c.path), ['plugins.search-source']);
view = set(view.revision, { 'usage-source': null });
assert.deepEqual(view.plugins, { 'search-source': 'searxng-url' });
assert.deepEqual(log().at(-1).changes, [{ path: 'plugins.usage-source', old: ['meter'], new: null, oldPresent: true, newPresent: false }]);
const code = (plugins) => { try { set(view.revision, plugins); } catch (e) { return e.code; } return null; };
assert.equal(code({ 'made-up': null }), 'invalid-plugins');
assert.equal(code({}), 'invalid-plugins');
assert.equal(code({ 'search-source': 'no-such-module' }), 'unknown-plugin');
});