Files
stack/packages/cli/tests/channel-recovery.test.mjs
jason.woltjeandClaude Opus 5.5 008746b6ed plugins: channel kind for the CLI and Console chat paths (row 164, #1661)
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]>
2026-10-18 14:24:42 -05:00

40 lines
2.1 KiB
JavaScript

// Removing or disabling every channel must leave the proved management writer usable.
import test from "node:test";
import assert from "node:assert/strict";
import { writeFileSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { provedRuntime } from "./channel-fixture.mjs";
test("both channels off recover through the existing human CLI, socket and organization writer", { timeout: 20000 }, async t => {
const s = await provedRuntime(t);
const off = await s.cli(["threads"]);
assert.equal(off.status, 3, off.stderr);
assert.match(off.stderr, /mosaic plugins.*mosaic plugins set/s);
const view = await s.cli(["plugins", "--json"]);
assert.equal(view.status, 0, view.stderr);
const state = JSON.parse(view.stdout);
assert.deepEqual(state.plugins.channel, []);
const patch = join(s.root, "enable.json");
writeFileSync(patch, JSON.stringify({ revision: state.revision, plugins: { channel: ["cli", "webui"] } }));
const done = await s.cli(["plugins", "set", "--file", patch]);
assert.equal(done.status, 0, done.stderr);
assert.deepEqual(JSON.parse(done.stdout).plugins.channel, ["cli", "webui"]);
assert.deepEqual(JSON.parse(readFileSync(s.file, "utf8")).plugins.channel, ["cli", "webui"]);
const receipt = s.history().at(-1);
assert.equal(receipt.actor, "jason");
assert.equal(receipt.state, "committed");
const threads = await s.cli(["threads"]);
assert.deepEqual([threads.status, threads.stdout, threads.stderr], [0, "no threads\n", ""]);
const before = readFileSync(s.file, "utf8");
const stale = await s.cli(["plugins", "set", "--file", patch]);
assert.notEqual(stale.status, 0);
assert.match(stale.stderr, /revision-conflict/);
assert.equal(readFileSync(s.file, "utf8"), before);
// Removing the key is also off, and does not remove the recovery read path.
const now = JSON.parse((await s.cli(["plugins"])).stdout);
writeFileSync(patch, JSON.stringify({ revision: now.revision, plugins: { channel: null } }));
assert.equal((await s.cli(["plugins", "set", "--file", patch])).status, 0);
assert.equal((await s.cli(["threads"])).status, 3);
assert.equal((await s.cli(["plugins"])).status, 0);
});