Files
jason.woltje 87db40e0cd fix(cli,seat,discord,organization,control-board,harness,plugins,webui): review round: launcher reasons, session, discord reads, mosaic-config and keys by label (#1873)
Review "changes 8" fix round for org-redact6.

- launcher: a harness module's throw names its system code or class,
  never its message; preflight, loader, bundle, checkout, sandbox and
  route text is relayed with host paths replaced (relay()).
- claude-code and pi preflights name MOSAIC_CLAUDE_BIN and the
  repository's node_modules, never a path; persona and skill bundle
  errors and sandboxProblem name their file or the sandbox by label.
- seat session: thread store, legacy store, overlap, busy lock and
  newRun refusals name the place under the data root.
- discord: unknown keys, binding names and turn ids are described when
  not plain; approvals, journal and repository read errors name the file
  and code; git drops stderr lines that carry a host path and a spawn
  failure names its code only.
- seat, control-board: registration fields and seen keys are described
  when not plain; invalid seat name and layout state the constraint.
- organization: the registry names the config directory and registry
  file by label; --project is quoted only when it is a plain name;
  systemConfigProblem moves to its own module.
- scripts/mosaic-config.mjs: the config by where it came from, the data
  root and its marker by role, keys and arguments only when plain, and
  error codes in place of raw messages. Exit codes unchanged.
- webui sso-config: an unknown console key is named only when plain.

Sentinel tests in each package's path-redaction-labels file fail on
BASE; pins that quoted a path now assert the label and its absence.
2026-11-11 08:53:31 -06:00

221 lines
11 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { projectFilePath } from "../src/index.mjs";
import { businessDoc, REPO, rolesCopy, systemConfig, tmp, tokenFile, writeJson } from "./helpers.mjs";
const CLI = join(REPO, "packages", "organization", "src", "cli.mjs");
test('C2-04: business validate and resolve skip a library before code projects and credential checks', async t => {
const { mappingEntry } = await import('../../library/tests/helpers.mjs');
const { rmSync } = await import('node:fs');
const s = setup(doc => { doc.roles.chief.credentials = { gitea: { ...doc.roles.coder.credentials.gitea } }; doc.projects = { books: mappingEntry(), ...doc.projects }; });
t.after(() => rmSync(s.root, { recursive: true, force: true }));
writeJson(projectFilePath(s.doc.projects.stack.root), { projectVersion: 1, id: 'stack', vars: { 'tracker.project': 3 } });
const checked = run(s, 'validate', 'acme');
assert.equal(checked.code, 0, checked.err);
assert.deepEqual(JSON.parse(checked.out).projects, { books: 'library', stack: 'valid' });
assert.doesNotMatch(checked.err, /project books: no project file/);
const library = run(s, 'resolve', 'acme', 'coder', '--project', 'books');
assert.equal(library.code, 0, library.err);
assert.equal(JSON.parse(library.out).project, null);
const code = run(s, 'resolve', 'acme', 'coder', '--project', 'stack');
assert.equal(code.code, 0, code.err);
assert.equal(JSON.parse(code.out).vars['tracker.project'], 3);
});
// A complete scratch setup: system config, business file, roles copy.
// HOME points into the scratch tree too, so nothing reads the real one.
function setup(mutate) {
const root = tmp();
const config = systemConfig(root);
const doc = businessDoc(root);
mutate?.(doc, root);
writeJson(join(root, "config", "businesses", `${doc.id}.json`), doc);
const env = { PATH: process.env.PATH, HOME: join(root, "home"), MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: rolesCopy(root) };
return { root, doc, env };
}
function run(s, ...args) {
const proc = spawnSync(process.execPath, [CLI, ...args], { env: s.env, encoding: "utf8" });
return { code: proc.status, out: proc.stdout, err: proc.stderr };
}
test("usage errors exit 4", () => {
const s = setup();
assert.equal(run(s).code, 4);
assert.equal(run(s, "show", "acme").code, 4);
assert.equal(run(s, "validate").code, 4);
assert.equal(run(s, "validate", "acme", "extra").code, 4);
assert.equal(run(s, "resolve", "acme").code, 4);
assert.equal(run(s, "resolve", "acme", "pm", "--project").code, 4);
assert.equal(run(s, "resolve", "acme", "pm", "--project", "a", "--project", "b").code, 4);
assert.match(run(s).err, /usage: mosaic business validate/);
});
test("validate: a good business exits 0 and prints instance digests", () => {
const s = setup();
const r = run(s, "validate", "acme");
assert.equal(r.code, 0, r.err);
const out = JSON.parse(r.out);
assert.equal(out.business, "acme");
assert.deepEqual(out.projects, { stack: "absent" });
assert.deepEqual(Object.keys(out.instances), ["pm", "cto", "coder", "reviewer", "chief"]);
for (const digest of Object.values(out.instances)) assert.match(digest, /^[0-9a-f]{64}$/);
assert.match(r.err, /warning: project stack: no project file/);
assert.doesNotMatch(r.out + r.err, /placeholder-not-a-token/);
});
test("validate: project files", () => {
const s = setup();
const file = projectFilePath(s.doc.projects.stack.root);
writeJson(file, { projectVersion: 1, id: "stack", vars: { "tracker.project": 3 } });
const r = run(s, "validate", "acme");
assert.equal(r.code, 0, r.err);
assert.deepEqual(JSON.parse(r.out).projects, { stack: "valid" });
writeJson(file, { projectVersion: 1, id: "other" });
assert.match(run(s, "validate", "acme").err, /has id that does not match the declared project key/);
assert.equal(run(s, "validate", "acme").code, 2);
writeJson(file, { projectVersion: 1, id: "stack", roles: { ghost: {} } });
const ghost = run(s, "validate", "acme");
assert.equal(ghost.code, 2);
assert.match(ghost.err, /role instance ghost/);
writeJson(file, { projectVersion: 1, id: "stack", vars: { harness: "pi" } });
assert.equal(run(s, "validate", "acme").code, 2);
});
// PR #1916 (org-redact5): the project root is a config value, so a mismatched
// project file is named by its checked project key, never by path.
test("validate: a project file with another id names the project key, not the project root", () => {
const sentinel = "sk-test-NOT-A-SECRET-1234";
const s = setup((doc, root) => { doc.projects.stack.root = join(root, sentinel, "project"); mkdirSync(doc.projects.stack.root, { recursive: true }); });
writeJson(projectFilePath(s.doc.projects.stack.root), { projectVersion: 1, id: "other" });
const r = run(s, "validate", "acme");
assert.equal(r.code, 2);
assert.match(r.err, /project file of project stack has id that does not match the declared project key/);
assert.ok(!r.err.includes(sentinel), r.err);
assert.ok(!r.err.includes(s.doc.projects.stack.root), r.err);
});
test("validate: missing files and a broken system config", () => {
const s = setup();
const missing = run(s, "validate", "nobody");
assert.equal(missing.code, 4);
assert.match(missing.err, /business file nobody\.json not found/);
assert.equal(run(s, "validate", "Bad!").code, 2);
const noConfig = { ...s, env: { ...s.env, MOSAIC_CONFIG: join(s.root, "absent", "config.json") } };
const r = run(noConfig, "validate", "acme");
assert.equal(r.code, 3);
assert.match(r.err, /system config problem/);
writeFileSync(s.env.MOSAIC_CONFIG, "{");
assert.equal(run(s, "validate", "acme").code, 3);
});
test("validate: credential reference problems exit 2 and name each one", () => {
const s = setup((doc, root) => {
chmodSync(doc.roles.cto.credentials.gitea.file, 0o644);
doc.roles.coder.credentials.vikunja.file = tokenFile(join(root, "data", "tokens"), "coder.token");
doc.roles.reviewer.credentials.vikunja.expires = "2026-01-01";
});
const r = run(s, "validate", "acme");
assert.equal(r.code, 2);
assert.equal(r.out, "");
assert.match(r.err, /roles\.cto\.credentials\.gitea file: mode 644/);
assert.match(r.err, /roles\.coder\.credentials\.vikunja file: inside the data root/);
assert.match(r.err, /expired on 2026-01-01/);
assert.match(r.err, /3 credential reference problem/);
});
// Row 194 (brief item 7, sage-1605 pick a): a human create writes store references dated the
// create day. Until issue writes a file, the date describes no token, so validate skips it. An
// issued store reference keeps the date check.
test("validate: a not-issued store agent dated today passes, and an issued one with a past expiry fails", () => {
const today = new Date().toISOString().slice(0, 10);
const s = setup((doc) => {
doc.roles.coder.credentials = { gitea: { store: true, rotateBy: today }, vikunja: { store: true, expires: today } };
});
const r = run(s, "validate", "acme");
assert.equal(r.code, 0, r.err);
assert.match(r.err, /roles\.coder\.credentials\.gitea store file: not issued/);
assert.match(r.err, /roles\.coder\.credentials\.vikunja store file: not issued/);
assert.doesNotMatch(r.err, /expire|rotation/);
const issued = setup((doc) => {
doc.roles.coder.credentials = { gitea: { store: true, rotateBy: today }, vikunja: { store: true, expires: "2026-01-01" } };
});
tokenFile(join(issued.root, "config", "secrets", "agents", "acme", "coder"), "vikunja");
const e = run(issued, "validate", "acme");
assert.equal(e.code, 2);
assert.match(e.err, /roles\.coder\.credentials\.vikunja store file: expired on 2026-01-01/);
});
test("validate: a token file inside the repository is refused", () => {
const s = setup((doc) => { doc.roles.pm.credentials.gitea.file = join(REPO, "roles", "pm.md"); });
const r = run(s, "validate", "acme");
assert.equal(r.code, 2);
assert.match(r.err, /inside .*; token files live outside the repository and dataRoot/);
});
test("validate: role definitions come from MOSAIC_ROLES_DIR", () => {
const s = setup();
const before = JSON.parse(run(s, "validate", "acme").out).instances;
const pmFile = join(s.env.MOSAIC_ROLES_DIR, "pm.json");
const pm = JSON.parse(readFileSync(pmFile, "utf8"));
pm.tools = pm.tools.filter((t) => t !== "write");
writeJson(pmFile, pm);
const after = JSON.parse(run(s, "validate", "acme").out).instances;
assert.notEqual(after.pm, before.pm);
assert.equal(after.cto, before.cto);
pm.authority.withinRole.push("deploy");
writeJson(pmFile, pm);
const gated = run(s, "validate", "acme");
assert.equal(gated.code, 2);
assert.match(gated.err, /always gated/);
});
test("resolve: prints one instance's record", () => {
const s = setup();
writeJson(projectFilePath(s.doc.projects.stack.root), {
projectVersion: 1, id: "stack", vars: { "tracker.project": 3 }, roles: { coder: { vars: { "limits.tools": ["read", "bash"] } } },
});
const r = run(s, "resolve", "acme", "coder", "--project", "stack");
assert.equal(r.code, 0, r.err);
const coder = JSON.parse(r.out);
assert.equal(coder.project, "stack");
assert.equal(coder.vars["tracker.project"], 3);
assert.deepEqual(coder.limits.tools, ["read", "bash"]);
assert.equal(coder.limits.network, "none");
assert.equal(coder.contract, join(s.env.MOSAIC_ROLES_DIR, "coder.md"));
assert.doesNotMatch(r.out, /placeholder-not-a-token/);
const chief = JSON.parse(run(s, "resolve", "acme", "chief").out);
assert.equal(chief.project, null);
assert.ok(chief.limits.authority.withinRole.includes("role.launch"));
const pm = JSON.parse(run(s, "resolve", "acme", "pm").out);
assert.ok(!pm.limits.authority.withinRole.includes("role.launch"));
const validated = JSON.parse(run(s, "validate", "acme").out).instances;
assert.equal(chief.digest, validated.chief);
});
test("resolve: refusals", () => {
const s = setup((doc) => { chmodSync(doc.roles.reviewer.credentials.gitea.file, 0o604); });
assert.equal(run(s, "resolve", "acme", "pm").code, 0);
const reviewer = run(s, "resolve", "acme", "reviewer");
assert.equal(reviewer.code, 2);
assert.equal(reviewer.out, "");
assert.match(reviewer.err, /mode 604/);
assert.equal(run(s, "resolve", "acme", "ghost").code, 2);
assert.equal(run(s, "resolve", "acme", "constructor").code, 2);
assert.match(run(s, "resolve", "acme", "pm", "--project", "ghost").err, /declares no project "ghost"/);
// #1873: a --project value that is not a plain name is never echoed.
const odd = run(s, "resolve", "acme", "pm", "--project", "/srv/sk-test-NOT-A-SECRET-1234/x");
assert.equal(odd.code, 2);
assert.match(odd.err, /declares no project by that name/);
assert.doesNotMatch(odd.err, /sk-test|NOT-A-SECRET|\/srv\//);
assert.equal(run(s, "resolve", "acme", "pm", "--project", "stack").code, 4);
});