packages/cli adds mosaic inbox, decide, tasks, agents and trail over the human-cli transport, and mosaic bus start, stop and status as the trusted host (unit mosaic-bus@<business>, scripts/bus-service.sh). The host boots packages/bus/src/process.mjs, passes config.trackers from the tracker.* variables (lead decision 70), and runs a notifier child. The notifier DMs each open blocking decision once and sends an 08:00 America/Chicago digest, journaled in notify/<business>/sent.jsonl at 0600 with no Discord ids. A torn journal tail is copied aside and truncated; a malformed line, a directory looser than 0700 or a symlinked journal refuses (lead decision 71). packages/discord gains dmRecipient, createDm and notify.mjs. Candidate agents/rocko/work/slice1-s4, baseb9b6cf00, build.patch b52f7d68, manifest e858504e (29 files). Darkwing approved round 2 on #1521 (comment 26855), Filbert approved round 2 (comment 26856). The packet's mutant table lists M28 as killed; it survived, and BUILD-LOG records the correction. Integration gate in a worktree on2557e29dwith the patch applied: bus 67, business 60, cli 49, control-board 124, discord 178, ledger 78, mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all with no failures. Conversation is 149/3, the same K1, K3 and K10 cases that fail on the base; S4 doesn't touch the package. Every scripts/test-*.sh is green, with test-release 14/14 and test-task 98/98 on the existing gate2 compose network. A scratch test, not in this commit, booted the real host with trackers against S3's fake Vikunja: the adapter went ready and a task.close on a missing task answered task-not-found after a Vikunja read. Co-Authored-By: Claude Opus 5.5 <[email protected]>
114 lines
4.4 KiB
JavaScript
114 lines
4.4 KiB
JavaScript
// Shared fixtures. Every test works in its own temporary directory and
|
|
// points MOSAIC_CONFIG there; nothing reads the real ~/.config, a real
|
|
// token, a real binding, or the real human transport.
|
|
|
|
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { PassThrough } from "node:stream";
|
|
import { BusError, Broker } from "../../bus/src/broker.mjs";
|
|
import { Store } from "../../bus/src/store.mjs";
|
|
import { businessDoc, rolesCopy, systemConfig, writeJson } from "../../business/tests/helpers.mjs";
|
|
import { AGENT_MARKERS } from "../src/transport.mjs";
|
|
|
|
export function tmp(t, prefix = "mosaic-cli-") {
|
|
const root = mkdtempSync(join(tmpdir(), prefix));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
return root;
|
|
}
|
|
|
|
// The environment with every agent marker removed: the test runner itself
|
|
// may run under an agent, and the commands refuse there.
|
|
export function humanEnv(extra = {}) {
|
|
const env = { ...process.env, ...extra };
|
|
for (const k of AGENT_MARKERS) delete env[k];
|
|
delete env.NODE_TEST_CONTEXT;
|
|
return env;
|
|
}
|
|
|
|
// A config directory with the system config, the roles and business `id`.
|
|
export function fixture(root, id = "acme", edit = (doc) => doc) {
|
|
const config = systemConfig(root);
|
|
const roles = rolesCopy(root);
|
|
const doc = edit(businessDoc(root, id));
|
|
writeJson(join(root, "config", "businesses", `${id}.json`), doc);
|
|
return { config, roles, dataRoot: join(root, "data"), doc, env: humanEnv({ MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: roles }) };
|
|
}
|
|
|
|
export function notifyConfig(dataRoot, business, binding) {
|
|
// The journal sits beside notify.json and refuses a directory looser than 0700.
|
|
mkdirSync(join(dataRoot, "notify", business), { recursive: true, mode: 0o700 });
|
|
return writeJson(join(dataRoot, "notify", business, "notify.json"), { notifyVersion: 1, binding });
|
|
}
|
|
|
|
// An io for main(): captured stdout and stderr, a stdin that is not a TTY.
|
|
export function io(env = humanEnv()) {
|
|
const out = { text: "" };
|
|
const err = { text: "" };
|
|
const stdin = new PassThrough();
|
|
stdin.isTTY = false;
|
|
return {
|
|
env,
|
|
stdin,
|
|
stdout: { write: (s) => ((out.text += s), true) },
|
|
stderr: { write: (s) => ((err.text += s), true) },
|
|
out,
|
|
err,
|
|
};
|
|
}
|
|
|
|
export const OPTIONS = [
|
|
{ key: "yes", text: "Allow" },
|
|
{ key: "no", text: "Decline" },
|
|
];
|
|
|
|
export const BUSINESSES = {
|
|
demo: {
|
|
id: "demo",
|
|
human: "jason",
|
|
arbiters: { technical: "cto", delivery: "pm" },
|
|
roles: {
|
|
pm: { authority: { withinRole: ["message.send"], crossRole: [] } },
|
|
cto: { authority: { withinRole: ["message.send"], crossRole: [] } },
|
|
coder: { authority: { withinRole: ["message.send"], crossRole: ["task.scope.change"] } },
|
|
},
|
|
},
|
|
};
|
|
|
|
// An in-process broker with a claimed coder and the human's capability.
|
|
// transport(business) mirrors humanTransport: errors carry the bus code.
|
|
export function broker(t) {
|
|
const root = tmp(t, "mosaic-cli-bus-");
|
|
const store = new Store(root);
|
|
t.after(() => store.close());
|
|
const b = new Broker({ store, businesses: BUSINESSES });
|
|
const coder = b.bindLaunch({ business: "demo", role: "coder", run: "coder-run", harness: "pi", address: "coder-run" });
|
|
b.request(coder, { verb: "role.claim" });
|
|
const human = b.bindHuman({ business: "demo", human: "jason", via: "cli", outsideAgent: true });
|
|
const reader = b.bindReader({ business: "demo" });
|
|
const calls = [];
|
|
const transport = (business) => async (verb, args = {}) => {
|
|
calls.push({ business, verb, args });
|
|
try {
|
|
return structuredClone(b.request(human, { verb, args }));
|
|
} catch (e) {
|
|
if (!(e instanceof BusError)) throw e;
|
|
const error = new Error(e.code);
|
|
error.code = e.code;
|
|
throw error;
|
|
}
|
|
};
|
|
const raise = (action, extra = {}) =>
|
|
b.request(coder, { verb: "decision.raise", args: { action, question: "Push the release?", options: OPTIONS, recommendation: "no", blocking: false, ...extra } });
|
|
return { b, store, coder, human, reader, transport, calls, raise, read: (verb, args = {}) => structuredClone(b.request(reader, { verb, args })) };
|
|
}
|
|
|
|
// A script standing in for packages/bus/src/human-cli.mjs: it reads one
|
|
// request on stdin and answers from the table in its first argument.
|
|
export function fakeCli(root, body) {
|
|
const file = join(root, "fake-human-cli.mjs");
|
|
writeFileSync(file, body);
|
|
chmodSync(file, 0o600);
|
|
return file;
|
|
}
|