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); });