Adds plugin kind `channel` (interfaceVersion 1, dispatch(request, {call}))
with cli and webui modules. Project and thread surfaces and thread
talk/follow in the CLI and the Console chat go through the selected
channel. An empty or absent channel selection is off: the CLI exits 3 and
the Console answers 503, each naming `mosaic plugins` and `mosaic plugins
set` with the business selector. Management plugins and plugins.set stay
on the core transport. The loader gains a synchronous snapshot(kind)
getter; loaded(), modules() and last-good handling are unchanged. No lock
is held across dispatch. serve.mjs, public/ and the bus are untouched.
Owner Rocko. Reviewer Dewey: r1 changes (comment 28980, B1: the recovery
sentence was never asserted), r2 approve (comment 28984) on candidate
manifest ae2fcfda...1fc8, the B1 assert killing mutant D1. Design accepted
by Mos (mos-0768, mos-0773). Fixture rewrite at project.test.mjs:47 acked
by Mos (mos-0789) on Dewey's line-by-line check: lines 48-51 identical,
one added assert. Sage gate on e5b10d275: 25 steps green, 2081 package tests, 156 queue node tests, 422 script selftests, 0 failed.
Live stays as pinned. The re-pin that takes this live puts `mosaic
plugins set` in Jason's window right after activate (mos-0768).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
63 lines
3.5 KiB
JavaScript
63 lines
3.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { main, run } from '../src/cli.mjs';
|
|
import { fixture, humanEnv, io, tmp } from './helpers.mjs';
|
|
import { writeJson } from '../../business/tests/helpers.mjs';
|
|
|
|
test('project init/check use declared business repo and never contact the bus', async t => {
|
|
const root = tmp(t), f = fixture(root);
|
|
f.doc.projects.stack.repo = join(root, 'records');
|
|
writeJson(join(root, 'config/businesses/acme.json'), f.doc);
|
|
const x = io(humanEnv(f.env));
|
|
const deps = { transport: () => assert.fail('local project command contacted bus') };
|
|
await main(['project', 'init', 'acme', 'stack'], x, deps);
|
|
assert.ok(existsSync(join(root, 'records/spec/north-star.md')));
|
|
const checked = io(humanEnv(f.env));
|
|
assert.equal(await run(['project', 'check', 'acme', 'stack'], checked, deps), 0);
|
|
assert.match(checked.out.text, /valid|checked/);
|
|
writeFileSync(join(root, 'records/unknown'), 'bad');
|
|
const bad = io(humanEnv(f.env));
|
|
assert.equal(await run(['project', 'check', 'acme', 'stack'], bad, deps), 2);
|
|
assert.match(bad.err.text, /unknown/);
|
|
});
|
|
|
|
test('project commands validate args and refuse undeclared projects and agent init', async t => {
|
|
const root = tmp(t), f = fixture(root);
|
|
await assert.rejects(main(['project', 'init', 'acme', 'missing'], io(humanEnv(f.env)), {}), /declare/);
|
|
await assert.rejects(main(['project', 'init', 'acme', 'stack'], io({ ...f.env, CODEX_THREAD_ID: 'fixture-agent' }), {}), /agent/i);
|
|
for (const args of [[], ['other', 'acme', 'stack'], ['init', 'acme'], ['check', 'acme', 'stack', '--write']])
|
|
assert.equal(await run(['project', ...args], io(humanEnv(f.env)), {}), 4);
|
|
});
|
|
|
|
// Row 128 (#1622): create and list are bus verbs; they take their own arguments, and init and
|
|
// check keep theirs (a name for create is not the init/check <business> <project> pair).
|
|
test('project create and list validate their arguments before any bus call', async t => {
|
|
const root = tmp(t), f = fixture(root);
|
|
const deps = { transport: () => assert.fail('a usage error reached the bus') };
|
|
for (const args of [['create'], ['create', 'a', 'b'], ['create', 'a', '--repo'], ['create', 'a', '--repo', 'x', '--repo', 'y'], ['create', 'a', '--write'], ['list', 'extra'], ['list', '--repo', 'x']]) {
|
|
const x = io(humanEnv(f.env));
|
|
// --business is given, so only the arity check can produce the exit 4 (callFor would too, without it).
|
|
assert.equal(await run(['project', ...args, '--business', 'acme'], x, deps), 4, args.join(' '));
|
|
assert.match(x.err.text, /mosaic project create <name>/);
|
|
}
|
|
const listed = [];
|
|
const managementReads = [], transport = () => async verb => verb === 'plugins' ? (managementReads.push(verb), { business: 'acme', revision: 'a'.repeat(64), plugins: { channel: ['cli', 'webui'] } }) : (listed.push(verb), []);
|
|
const x = io(humanEnv(f.env));
|
|
assert.equal(await run(['project', 'list', '--business', 'acme'], x, { transport }), 0);
|
|
assert.deepEqual(listed, ['project.list']);
|
|
assert.equal(x.out.text, 'no projects\n');
|
|
assert.deepEqual(managementReads, ['plugins']);
|
|
});
|
|
|
|
test('project dispatcher reaches the human CLI', async t => {
|
|
const root = tmp(t), f = fixture(root);
|
|
const result = spawnSync(new URL('../../../scripts/mosaic', import.meta.url).pathname, ['project'], {
|
|
encoding: 'utf8', env: humanEnv(f.env),
|
|
});
|
|
assert.equal(result.status, 4);
|
|
assert.match(result.stderr, /mosaic project/);
|
|
});
|