Files
stack/packages/organization/tests/cli.test.mjs

203 lines
9.6 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, 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);
});
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 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, /cto-gitea\.token: mode 644/);
assert.match(r.err, /coder\.token: inside .*data/);
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, /gitea file .*secrets\/agents\/acme\/coder\/gitea: not issued/);
assert.match(r.err, /vikunja file .*secrets\/agents\/acme\/coder\/vikunja: 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, /vikunja file .*coder\/vikunja: 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"/);
assert.equal(run(s, "resolve", "acme", "pm", "--project", "stack").code, 4);
});