// Q247 / row 324 (#1820): optional business tracker; role credential rules remain exact. import { test } from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { rmSync } from "node:fs"; import { join } from "node:path"; import { loadBusiness, validateBusinessDocument } from "../src/index.mjs"; import { businessDoc, REPO, REPO_ROLES, systemConfig, tmp, writeJson } from "./helpers.mjs"; function fixture(t) { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const doc = businessDoc(root); const config = systemConfig(root); const file = join(root, "config", "businesses", "acme.json"); return { root, doc, config, file }; } const check = (x) => validateBusinessDocument(x.doc, x.file, { rolesDir: REPO_ROLES }); test("Q247: omitting tracker loads as null and preserves independent role bots", (t) => { const x = fixture(t); delete x.doc.tracker; writeJson(x.file, x.doc); const b = loadBusiness("acme", { dir: join(x.root, "config"), rolesDir: REPO_ROLES }); assert.equal(b.tracker, null); assert.equal(b.roles.pm.tracker.bot, x.doc.roles.pm.tracker.bot); assert.equal(b.roles.pm.credentials.vikunja.file, x.doc.roles.pm.credentials.vikunja.file); assert.equal(Object.hasOwn(x.doc, "tracker"), false); }); test("Q247: real business validate passes without a sync credential reference or file", (t) => { const x = fixture(t); rmSync(x.doc.tracker.sync.credentials.vikunja.file); delete x.doc.tracker; writeJson(x.file, x.doc); const r = spawnSync(process.execPath, [join(REPO, "packages/organization/src/cli.mjs"), "validate", "acme"], { env: { ...process.env, HOME: x.root, MOSAIC_CONFIG: x.config, MOSAIC_ROLES_DIR: REPO_ROLES }, encoding: "utf8", timeout: 30000, }); assert.equal(r.status, 0, r.stdout + r.stderr); assert.equal(JSON.parse(r.stdout).business, "acme"); assert.doesNotMatch(r.stderr, /credential reference problem|not found/); }); test("Q247: present tracker retains its values and validation without mutating the input", (t) => { const x = fixture(t); const before = structuredClone(x.doc); const b = check(x); assert.equal(b.tracker.sync.bot, x.doc.tracker.sync.bot); assert.equal(b.tracker.sync.botId, x.doc.tracker.sync.botId); assert.equal(b.tracker.sync.credentials.vikunja.file, x.doc.tracker.sync.credentials.vikunja.file); assert.deepEqual(b.tracker.labels, {}); assert.deepEqual(x.doc, before); assert.ok(Object.isFrozen(b.tracker.sync)); }); test("Q247: present null, undefined and malformed trackers still refuse", (t) => { const x = fixture(t); for (const tracker of [null, undefined, false, [], {}, { sync: {} }]) { x.doc.tracker = tracker; assert.throws(() => check(x), /tracker/, String(tracker)); } }); test("Q247: absent business tracker does not waive a role's Vikunja bot requirement", (t) => { const x = fixture(t); delete x.doc.tracker; delete x.doc.roles.pm.tracker; assert.throws(() => check(x), /roles\.pm needs "tracker"/); });