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]>
68 lines
3.6 KiB
JavaScript
68 lines
3.6 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { join } from "node:path";
|
|
import { bootConfig, loadSystem } from "../src/config.mjs";
|
|
import { writeJson } from "../../business/tests/helpers.mjs";
|
|
import { fixture, tmp } from "./helpers.mjs";
|
|
|
|
test("bootConfig builds the broker's boot message for one business, with no trackers key when no project names one", (t) => {
|
|
const f = fixture(tmp(t));
|
|
const system = loadSystem({ env: f.env });
|
|
const warnings = [];
|
|
const boot = bootConfig({ system, businessId: "acme", env: f.env, warn: (w) => warnings.push(w) });
|
|
assert.equal(boot.dataRoot, f.dataRoot);
|
|
assert.deepEqual(Object.keys(boot.businesses), ["acme"]);
|
|
assert.deepEqual(boot.launches, []);
|
|
assert.deepEqual(boot.readers, ["acme"]);
|
|
assert.equal("trackers" in boot, false);
|
|
assert.match(warnings[0], /no project file sets tracker\.project/);
|
|
});
|
|
|
|
test("trackers come from the tracker.* variables of the one project that names a tracker project", (t) => {
|
|
const root = tmp(t);
|
|
const f = fixture(root);
|
|
writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 12 } });
|
|
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
|
assert.deepEqual(boot.trackers, { acme: { baseUrl: "http://127.0.0.1:3456", project: 12, pollSeconds: 60, reconcileMinutes: 60 } });
|
|
});
|
|
|
|
test("with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict", (t) => {
|
|
const root = tmp(t);
|
|
const f = fixture(root, "acme", (doc) => {
|
|
doc.projects.web = { root: join(root, "web") };
|
|
return doc;
|
|
});
|
|
writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 12 } });
|
|
writeJson(join(root, "web", ".mosaic", "project.json"), { projectVersion: 1, id: "web", vars: {} });
|
|
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
|
|
assert.deepEqual(boot.trackers, { acme: { baseUrl: "http://127.0.0.1:3456", project: 12, pollSeconds: 60, reconcileMinutes: 60 } });
|
|
});
|
|
|
|
test("two projects that each name a tracker project refuse, since the boot shape holds one", (t) => {
|
|
const root = tmp(t);
|
|
const f = fixture(root, "acme", (doc) => {
|
|
doc.projects.web = { root: join(root, "web") };
|
|
return doc;
|
|
});
|
|
writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 12 } });
|
|
writeJson(join(root, "web", ".mosaic", "project.json"), { projectVersion: 1, id: "web", vars: { "tracker.project": 13 } });
|
|
assert.throws(() => bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env }), (e) => e.exitCode === 3 && /stack, web each set tracker\.project/.test(e.message));
|
|
});
|
|
|
|
test("a business without tracker.baseUrl gets no trackers entry", (t) => {
|
|
const f = fixture(tmp(t), "acme", (doc) => {
|
|
delete doc.vars["tracker.baseUrl"];
|
|
return doc;
|
|
});
|
|
const warnings = [];
|
|
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env, warn: (w) => warnings.push(w) });
|
|
assert.equal("trackers" in boot, false);
|
|
assert.deepEqual(warnings, []);
|
|
});
|
|
|
|
test("an unknown business and a broken system config refuse with exit 3", (t) => {
|
|
const f = fixture(tmp(t));
|
|
assert.throws(() => bootConfig({ system: loadSystem({ env: f.env }), businessId: "nope", env: f.env }), (e) => e.exitCode === 3);
|
|
assert.throws(() => loadSystem({ env: { ...f.env, MOSAIC_CONFIG: join(f.dataRoot, "missing.json") } }), (e) => e.exitCode === 3);
|
|
});
|