// Case 2 row C2-13b (#1727): scripts/case2-install.sh installs a hub as , and // scripts/case2-precheck.sh's library checks and --library-status mode. Brief: // docs/plans/2026-10-20_case2-rows.md "C2-13b". Every case runs in a scratch home with its own // MOSAIC_CONFIG, a stub stack-copy and fake getent, sudo, id, node, bwrap and systemctl. The library // sync runs in-process against a local bare repo, as the bus would. Nothing here reads or writes // ~/.mosaic-dev, ~/.config/mosaic-dev, /etc or a network host, and no fixture value is a credential. import { test } from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { businessDoc, writeJson } from "../../business/tests/helpers.mjs"; import { BASE, MARKER, git, mappingEntry, repo } from "../../library/tests/helpers.mjs"; import { ensureMirror, mirrorPath, sync } from "../../library/src/index.mjs"; import { gitEnv } from "../../seat/src/checkout.mjs"; import { SAFE } from "../../project/src/git.mjs"; import { tmp } from "./helpers.mjs"; const REPO = fileURLToPath(new URL("../../../", import.meta.url)); const INSTALL = join(REPO, "scripts", "case2-install.sh"); const PRECHECK = join(REPO, "scripts", "case2-precheck.sh"); const COMMIT = "c".repeat(40); const UID = process.getuid(); const GID = process.getgid(); // Made-up values; no output may hold them. const FIXTURE_KEY = "fixture-zai-key-8d41a7"; const PERSONA_TEXT = { bean: "Bean is warm and brief. PERSONA-TEXT-bean-61f0\n", chet: "Chet is dry and exact. PERSONA-TEXT-chet-2b9e\n" }; const sha256 = (data) => createHash("sha256").update(data).digest("hex"); const mode = (p) => statSync(p).mode & 0o777; function script(file, text) { mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, `#!/usr/bin/env bash\n${text}\n`); chmodSync(file, 0o755); return file; } // A scratch : a home, an input business file, a persona staging dir and fakes. function world(t, { profile = "library" } = {}) { const root = tmp(t, "case2-install-"); const home = join(root, "home"); const configDir = join(home, ".config", "mosaic-dev"); // claude-session.env is in place before the install, as 2d would leave it on a re-run. const secrets = join(configDir, "secrets"); mkdirSync(secrets, { recursive: true, mode: 0o700 }); chmodSync(configDir, 0o700); writeFileSync(join(secrets, "claude-session.env"), `ZAI_API_KEY=${FIXTURE_KEY}\n`, { mode: 0o600 }); const doc = businessDoc(root); for (const r of Object.values(doc.roles)) r.vars = { ...r.vars, harness: "pi" }; if (profile === "library") { doc.projects.books = { ...mappingEntry(), user: "writer" }; doc.roles.chief.credentials = { gitea: { ...doc.roles.coder.credentials.gitea } }; doc.roles.bean = { definition: "companion", vars: { harness: "pi" } }; doc.roles.bean.credentials = { gitea: { ...doc.roles.coder.credentials.gitea } }; doc.roles.chet = { definition: "companion", vars: { harness: "pi" } }; doc.roles.chet.credentials = { gitea: { ...doc.roles.coder.credentials.gitea } }; } else doc.vars["bus.tester"] = true; const input = writeJson(join(root, "input", "acme.json"), doc); const staging = join(root, "staging"); mkdirSync(staging, { mode: 0o700 }); const personas = []; for (const [instance, name] of [["bean", "Bean"], ["chet", "Chet"]]) { writeFileSync(join(staging, `${instance}.md`), PERSONA_TEXT[instance], { mode: 0o600 }); personas.push({ instance, name, bytes: Buffer.byteLength(PERSONA_TEXT[instance]), sha256: sha256(PERSONA_TEXT[instance]) }); } writeFileSync(join(staging, "manifest.json"), `${JSON.stringify({ version: 1, personas }, null, 2)}\n`, { mode: 0o600 }); const bin = join(root, "bin"); const log = join(root, "calls.log"); script(join(bin, "stack-copy"), `echo "stack-copy $*" >>'${log}' [ "$1" = copy ] && [ "$3" = --copies ] || exit 4 mkdir -p "$4/$2/scripts" && : >"$4/$2/scripts/mosaic" && echo "copied: $4/$2" for file in scripts/case2-persona-import.mjs packages/harness/src/bundle.mjs packages/harness/src/tools.mjs packages/organization/src/rails.mjs packages/organization/src/autonomy.mjs packages/bus/src/refusals.mjs packages/business/src/vocabulary.mjs packages/business/src/selector.mjs packages/organization/src/vocabulary.mjs packages/organization/src/selector.mjs packages/plugins/model-provider/cliproxy/index.mjs; do mkdir -p "$4/$2/$(dirname "$file")" && cp '${REPO}'"$file" "$4/$2/$file" || exit 1 done mkdir -p "$4/$2/node_modules" && cp -R '${REPO}node_modules/yaml' "$4/$2/node_modules/yaml"`); script(join(bin, "systemctl"), `echo "systemctl $*" >>'${log}'`); const me = spawnSync("id", ["-un"], { encoding: "utf8" }).stdout.trim(); script(join(bin, "getent"), `[ "$1" = passwd ] && [ "$2" = '${me}' ] && echo '${me}:x:${UID}:${GID}::${home}:/bin/bash'`); script(join(bin, "sudo"), "exit 1"); script(join(bin, "id"), `if [ "$1" = -G ]; then echo '${GID}'; elif [ "$1" = -u ] && [ -n "\${CASE2_TEST_UID:-}" ]; then echo "$CASE2_TEST_UID"; else exec /usr/bin/id "$@"; fi`); script(join(bin, "node"), `if [ "$1" = --version ]; then echo v24.3.0; exit 0; fi\nexec '${process.execPath}' "$@"`); script(join(bin, "bwrap"), "exit 0"); const units = join(root, "units"); mkdirSync(units); writeFileSync(join(units, "mosaic-webui.service"), "[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --port 443 --business acme\n"); writeFileSync(join(units, "mosaic-bus@.service"), "[Service]\nEnvironment=PATH=/usr/bin\n"); const sys = join(root, "sys"); mkdirSync(join(sys, "net", "ipv4"), { recursive: true }); mkdirSync(join(sys, "kernel"), { recursive: true }); writeFileSync(join(sys, "net", "ipv4", "ip_unprivileged_port_start"), "1024\n"); writeFileSync(join(sys, "kernel", "apparmor_restrict_unprivileged_userns"), "1\n"); const notify = join(home, ".mosaic-dev", "notify", "acme"); mkdirSync(notify, { recursive: true, mode: 0o700 }); writeFileSync(join(notify, "notify.json"), '{"notifyVersion":1,"binding":null}\n', { mode: 0o600 }); return { root, home, configDir, secrets, input, staging, bin, log, units, sys, doc, config: join(configDir, "config.json"), business: join(configDir, "businesses", "acme.json"), personaDir: join(configDir, "personas", "acme"), slot: join(secrets, "acme"), dataRoot: join(home, ".mosaic-dev"), }; } function env(x, extra = {}) { return { PATH: `${x.bin}:/usr/bin:/bin`, HOME: x.home, MOSAIC_CONFIG: x.config, CASE2_STACK_COPY: join(x.bin, "stack-copy"), ...extra }; } function clean(out) { assert.doesNotMatch(out, new RegExp(FIXTURE_KEY), "the output holds the fixture key"); assert.doesNotMatch(out, /PERSONA-TEXT/, "the output holds persona text"); assert.doesNotMatch(out, new RegExp(MARKER), "the output holds her text"); } function install(x, { profile = "library", args = null, extra = {} } = {}) { const argv = args ?? ["--business", "acme", "--business-file", x.input, "--commit", COMMIT, "--profile", profile, ...(profile === "library" ? ["--personas", x.staging] : [])]; const r = spawnSync(INSTALL, argv, { encoding: "utf8", timeout: 120000, env: env(x, extra) }); const out = r.stdout + r.stderr; clean(out); return { code: r.status, out }; } function precheck(x, { args = ["--business", "acme", "--profile", "library"] } = {}) { const r = spawnSync(PRECHECK, args, { encoding: "utf8", timeout: 60000, env: env(x, { CASE2_UNIT_DIR: x.units, CASE2_PROC_SYS: x.sys, CASE2_GETENT: join(x.bin, "getent") }), }); const out = r.stdout + r.stderr; clean(out); return { code: r.status, out }; } const status = (x) => precheck(x, { args: ["--business", "acme", "--library-status"] }); function refused(r, pattern) { assert.equal(r.code, 1, r.out); assert.match(r.out, pattern); } const calls = (x) => (existsSync(x.log) ? readFileSync(x.log, "utf8") : ""); // ---- case2-install.sh ---- test("the library install writes the business file, personas and slot dir, makes the copy and starts nothing", (t) => { const x = world(t); const r = install(x); assert.equal(r.code, 0, r.out); assert.equal(mode(join(x.dataRoot, '.deployed')), 0o444); assert.deepEqual(JSON.parse(readFileSync(join(x.dataRoot, '.deployed'), 'utf8')), { instance: 'dev' }); assert.equal(mode(x.business), 0o600); const doc = JSON.parse(readFileSync(x.business, "utf8")); for (const [instance, name] of [["bean", "Bean"], ["chet", "Chet"]]) { assert.equal(doc.roles[instance].vars["persona.name"], name); assert.equal(doc.roles[instance].vars["persona.sha256"], sha256(PERSONA_TEXT[instance])); assert.equal(doc.roles[instance].vars.harness, "pi"); const file = join(x.personaDir, `${instance}.md`); assert.equal(readFileSync(file, "utf8"), PERSONA_TEXT[instance]); assert.equal(mode(file), 0o600); } assert.equal(mode(join(x.configDir, "personas")), 0o700); assert.equal(mode(x.personaDir), 0o700); assert.equal(mode(x.slot), 0o700); assert.deepEqual(readdirSync(x.slot), []); assert.equal(calls(x), `stack-copy copy ${COMMIT} --copies ${join(x.home, ".local", "share", "mosaic-stack", "copies")}\n`); assert.doesNotMatch(r.out, /release/); assert.match(r.out, /business acme validates/); assert.match(r.out, /--library-status/); assert.ok(existsSync(x.config)); }); test("a library re-run passes and leaves every file as it was", (t) => { const x = world(t); assert.equal(install(x).code, 0); const before = readFileSync(x.business); const r = install(x); assert.equal(r.code, 0, r.out); assert.match(r.out, /already matches/); assert.match(r.out, /personas already installed/); assert.deepEqual(readFileSync(x.business), before); }); test("a re-run refuses business-file-differs and leaves the file unchanged", (t) => { const x = world(t); assert.equal(install(x).code, 0); const doc = JSON.parse(readFileSync(x.business, "utf8")); doc.vars["tracker.pollSeconds"] = 120; writeJson(x.business, doc); const before = readFileSync(x.business); refused(install(x), /business-file-differs/); assert.deepEqual(readFileSync(x.business), before); }); test("a re-run refuses persona-differs when an installed persona isn't the staged one, and writes nothing", (t) => { const x = world(t); assert.equal(install(x).code, 0); rmSync(x.business); writeFileSync(join(x.personaDir, "bean.md"), "Someone else. PERSONA-TEXT-other\n"); refused(install(x), /persona-differs/); assert.equal(existsSync(x.business), false); }); test("the factory install writes no library mapping, persona or slot dir", (t) => { const x = world(t, { profile: "factory" }); const r = install(x, { profile: "factory" }); assert.equal(r.code, 0, r.out); const doc = JSON.parse(readFileSync(x.business, "utf8")); assert.equal(Object.values(doc.projects).some((p) => p.kind === "library"), false); assert.equal(existsSync(join(x.configDir, "personas")), false); assert.equal(existsSync(x.slot), false); assert.doesNotMatch(r.out, /--library-status/); }); test("the factory install refuses a library project and bus.tester off, before anything is written", (t) => { const x = world(t, { profile: "factory" }); const doc = JSON.parse(readFileSync(x.input, "utf8")); doc.projects.books = { ...mappingEntry(), user: "writer" }; doc.roles.chief.credentials = { gitea: { ...doc.roles.coder.credentials.gitea } }; writeJson(x.input, doc); refused(install(x, { profile: "factory" }), /profile-library-business/); delete doc.projects.books; doc.vars["bus.tester"] = false; writeJson(x.input, doc); refused(install(x, { profile: "factory" }), /bus-tester-off/); assert.equal(calls(x), ""); assert.equal(existsSync(x.business), false); }); test("the library install refuses bus.tester on and a persona var in the input", (t) => { const x = world(t); const doc = JSON.parse(readFileSync(x.input, "utf8")); doc.vars["bus.tester"] = true; writeJson(x.input, doc); refused(install(x), /bus-tester-on/); doc.vars["bus.tester"] = false; doc.roles.bean.vars["persona.name"] = "Bean"; writeJson(x.input, doc); refused(install(x), /persona-var-input/); assert.equal(calls(x), ""); }); test("the install refuses root, a business id mismatch, another --home and a config dir with group bits", (t) => { const x = world(t); refused(install(x, { extra: { CASE2_TEST_UID: "0" } }), /run-as-root/); refused(install(x, { args: ["--business", "other", "--business-file", x.input, "--commit", COMMIT, "--personas", x.staging] }), /business-id-mismatch/); refused(install(x, { args: ["--business", "acme", "--business-file", x.input, "--commit", COMMIT, "--personas", x.staging, "--home", x.root] }), /home-mismatch/); chmodSync(x.configDir, 0o750); refused(install(x), /config-dir-mode/); assert.equal(calls(x), ""); assert.equal(existsSync(x.business), false); }); // The selectors (rename design r2): --org/--business and --org-file/--business-file are two // families, each its own scope. A bad selector refuses with exit 4 before anything is read or written. const rest = (x) => ["--commit", COMMIT, "--profile", "library", "--personas", x.staging]; test("the install takes --org and --org-file, alone or matching their old spelling in either order", (t) => { for (const sel of [ (f) => ["--org", "acme", "--org-file", f], (f) => ["--business", "acme", "--org", "acme", "--business-file", f, "--org-file", f], (f) => ["--org", "acme", "--business", "acme", "--org-file", f, "--business-file", f], ]) { const x = world(t); const r = install(x, { args: [...sel(x.input), ...rest(x)] }); assert.equal(r.code, 0, r.out); assert.equal(JSON.parse(readFileSync(x.business, "utf8")).id, "acme"); assert.equal(calls(x), `stack-copy copy ${COMMIT} --copies ${join(x.home, ".local", "share", "mosaic-stack", "copies")}\n`); } }); test("the install refuses a bad selector in either family before it reads or writes anything", (t) => { const x = world(t); const f = x.input, g = `${x.root}/input/../input/acme.json`; // join() would normalize it to f assert.notEqual(g, f); const usage = "usage: case2-install.sh --business ID --business-file FILE --commit SHA [--profile factory|library] [--personas DIR] [--home DIR]\n"; const notId = (v) => `case2-install: --business must be a business id: ${v}\n`; for (const [args, out] of [ [["--business", "acme", "--org", "beta", "--business-file", f], "case2-install: --business acme and --org beta conflict\n"], [["--org", "acme", "--business", "beta", "--business-file", f], "case2-install: --org acme and --business beta conflict\n"], [["--business", "acme", "--business", "acme", "--business-file", f], "case2-install: --business given twice\n"], [["--org", "acme", "--org", "acme", "--business-file", f], "case2-install: --org given twice\n"], [["--business-file", f, "--org"], usage], [["--business-file", f, "--business"], usage], [["--org", "", "--business-file", f], usage], [["--org", "--business-file", f], notId("--business-file")], [["--business", "-x", "--business-file", f], notId("-x")], [["--org", "Acme", "--business-file", f], notId("Acme")], [["--org", "a.b", "--business-file", f], notId("a.b")], // The file family compares the text given: the same file named another way still conflicts. [["--org", "acme", "--business-file", f, "--org-file", g], `case2-install: --business-file ${f} and --org-file ${g} conflict\n`], [["--org", "acme", "--org-file", g, "--business-file", f], `case2-install: --org-file ${g} and --business-file ${f} conflict\n`], [["--org", "acme", "--org-file", f, "--org-file", f], "case2-install: --org-file given twice\n"], [["--org", "acme", "--business-file", f, "--business-file", f], "case2-install: --business-file given twice\n"], [["--org", "acme", "--org-file"], usage], [["--org", "acme", "--org-file", ""], usage], [["--org", "acme", "--org-file", "--commit", COMMIT], "case2-install: --org-file needs one value, not --commit\n"], ]) { const last = ["--business-file", "--business", "--org", "--org-file"].includes(args.at(-1)); const r = install(x, { args: last ? args : [...args, ...rest(x)] }); assert.equal(r.code, 4, args.join(" ")); assert.equal(r.out, out, args.join(" ")); } // A selector spelling taken as another option's value is that option's text. const r = install(x, { args: ["--org", "acme", "--business-file", f, "--commit", "--org", "--profile", "library", "--personas", x.staging] }); assert.equal(r.code, 4, r.out); assert.equal(r.out, "case2-install: --commit must be a full 40-character commit id: --org\n"); assert.equal(calls(x), ""); assert.equal(existsSync(x.business), false); }); test("the library install needs --personas", (t) => { const x = world(t); const r = install(x, { args: ["--business", "acme", "--business-file", x.input, "--commit", COMMIT] }); assert.equal(r.code, 4, r.out); assert.match(r.out, /needs --personas/); }); test("a validate refusal removes the business file this run wrote", (t) => { const x = world(t); unlinkSync(x.doc.roles.pm.credentials.gitea.file); const r = install(x); refused(r, /validate: .*the business file this run wrote is removed/); assert.equal(existsSync(x.business), false); }); test("N4: a validate refusal on a re-run leaves the matching business file in place, byte for byte", (t) => { const x = world(t); assert.equal(install(x).code, 0); const before = readFileSync(x.business); unlinkSync(x.doc.roles.pm.credentials.gitea.file); const r = install(x); refused(r, /validate: /); assert.doesNotMatch(r.out, /is removed/); assert.deepEqual(readFileSync(x.business), before); }); // ---- case2-precheck.sh, the library profile (D6a) ---- test("a clean library host passes the precheck with no mirror yet", (t) => { const x = world(t); assert.equal(install(x).code, 0); const r = precheck(x); assert.equal(r.code, 0, r.out); assert.match(r.out, /ok {6}business file .* with no group or other bit/); assert.match(r.out, /ok {6}no history for acme yet/); assert.match(r.out, new RegExp(`ok {6}persona bean sha256 ${sha256(PERSONA_TEXT.bean)}`)); assert.match(r.out, new RegExp(`ok {6}persona chet sha256 ${sha256(PERSONA_TEXT.chet)}`)); assert.match(r.out, /ok {6}no Discord binding/); assert.equal(existsSync(mirrorPath({ business: { id: "acme", projects: { books: x.doc.projects.books } }, project: "books", dataRoot: x.dataRoot })), false); }); test("the library precheck refuses a persona file at 0644", (t) => { const x = world(t); assert.equal(install(x).code, 0); chmodSync(join(x.personaDir, "bean.md"), 0o644); refused(precheck(x), /REFUSED persona-mode: .*bean\.md must be .* 0600 file \(mode 644\)/); }); test("the library precheck refuses a persona whose digest isn't persona.sha256", (t) => { const x = world(t); assert.equal(install(x).code, 0); writeFileSync(join(x.personaDir, "chet.md"), "Not Chet. PERSONA-TEXT-other\n"); refused(precheck(x), /REFUSED persona-mismatch: persona chet has sha256 [0-9a-f]{64}, and persona.sha256 is /); }); test("the library precheck refuses a persona dir with group bits", (t) => { const x = world(t); assert.equal(install(x).code, 0); chmodSync(x.personaDir, 0o750); refused(precheck(x), /REFUSED persona-mode: .*personas\/acme must be .* 0700 directory/); }); test("the library precheck refuses a business file at 0644", (t) => { const x = world(t); assert.equal(install(x).code, 0); chmodSync(x.business, 0o644); refused(precheck(x), /REFUSED business-file-mode: .*acme\.json .*\(mode 644\)/); }); test("the library precheck refuses a changes.jsonl at 0644", (t) => { const x = world(t); assert.equal(install(x).code, 0); const history = join(x.configDir, "businesses", ".history", "acme"); mkdirSync(history, { recursive: true, mode: 0o700 }); writeFileSync(join(history, "changes.jsonl"), "", { mode: 0o644 }); chmodSync(join(history, "changes.jsonl"), 0o644); refused(precheck(x), /REFUSED history-mode: .*changes\.jsonl must be .* 0600 file \(mode 644\)/); chmodSync(join(history, "changes.jsonl"), 0o600); assert.equal(precheck(x).code, 0); }); test("the library precheck refuses a Discord binding", (t) => { const x = world(t); assert.equal(install(x).code, 0); mkdirSync(join(x.dataRoot, "discord"), { recursive: true }); writeFileSync(join(x.dataRoot, "discord", "her.json"), "{}\n", { mode: 0o600 }); refused(precheck(x), /REFUSED discord-binding: .*her\.json exists/); }); // ---- case2-precheck.sh --library-status (D6b) ---- // The context the bus builds for books (runtime.mjs:161-164), and a sync the way the bus's timer // runs it, against a local bare clone through the command parameter. function library(x) { const context = { business: { id: "acme", projects: { books: x.doc.projects.books } }, project: "books", dataRoot: x.dataRoot }; mkdirSync(x.dataRoot, { recursive: true, mode: 0o700 }); chmodSync(x.dataRoot, 0o700); const run = (bare, slot) => sync(context, { slot, command: (kind, url, { mirror, branch }) => ({ argv: [...SAFE, "-c", "protocol.file.allow=always", "-c", "core.createObject=rename", "-C", mirror, "fetch", "--no-tags", "--no-write-fetch-head", "--end-of-options", bare, `+refs/heads/${branch}:refs/remotes/library/${branch}`], env: { ...gitEnv(), LC_ALL: "C", GIT_CEILING_DIRECTORIES: dirname(mirror) }, }), }); return { context, run }; } test("--library-status refuses library-mirror-missing before the bus has made the mirror", (t) => { const x = world(t); assert.equal(install(x).code, 0); refused(status(x), /REFUSED library-mirror-missing: library books has no mirror at .*the bus makes it when it starts/); }); test("--library-status refuses library-never-synced on a fresh mirror", async (t) => { const x = world(t); assert.equal(install(x).code, 0); await ensureMirror(library(x).context); const r = status(x); refused(r, /REFUSED library-never-synced: library books has no fetch yet/); assert.doesNotMatch(r.out, /library-last-error/); }); test("--library-status names a recorded lastError before the null lastFetch it leaves (N3)", async (t) => { const x = world(t); assert.equal(install(x).code, 0); const lib = library(x); await lib.run("/nonexistent", join(x.slot, "library-gitea")); const r = status(x); refused(r, /REFUSED library-last-error: library books: library-credential-missing at \d{4}-\d\d-\d\dT/); assert.doesNotMatch(r.out, /library-never-synced/); }); test("--library-status prints each R-problem's path as an escaped JSON string, then the count, and passes", async (t) => { const x = world(t); assert.equal(install(x).code, 0); const odd = "Odd‮Name\u0085.md"; const f = repo(t, { ...BASE, [odd]: `${MARKER}\n` }); const bare = join(f.root, "remote.git"); git(f.root, "clone", "-q", "--bare", "--no-local", f.repo, bare); writeFileSync(join(x.slot, "library-gitea"), "placeholder-not-a-token\n", { mode: 0o600 }); const { state } = await library(x).run(bare, join(x.slot, "library-gitea")); assert.equal(state.lastError, null); const r = status(x); assert.equal(r.code, 0, r.out); assert.match(r.out, new RegExp(`ok {6}library books last fetched \\S+, serving ${f.commit}`)); assert.match(r.out, /^problem {3}R2 "Inbox Note\.md"$/m); assert.match(r.out, /^problem {3}R\d+ "Odd\\u202eName\\u0085\.md"$/m); assert.doesNotMatch(r.out, /[\u0085‮]/); const lines = r.out.split("\n").filter((l) => l.startsWith("problem ")); assert.equal(lines.length, state.problems.length); assert.match(r.out, new RegExp(`^problems ${state.problems.length}$`, "m")); assert.match(r.out, /case2-precheck: library status passed/); }); test("--library-status takes no --profile", (t) => { const x = world(t); const r = precheck(x, { args: ["--business", "acme", "--library-status", "--profile", "library"] }); assert.equal(r.code, 4, r.out); }); test("Vikunja 614: install preserves the explicit notifier decision bytes and mode and creates none when absent", (t) => { const x = world(t); const file = join(x.dataRoot, "notify", "acme", "notify.json"); const before = readFileSync(file), beforeMode = mode(file); const r = install(x); assert.equal(r.code, 0, r.out); assert.deepEqual(readFileSync(file), before); assert.equal(mode(file), beforeMode); const absent = world(t), missing = join(absent.dataRoot, "notify", "acme", "notify.json"); rmSync(missing); const fresh = install(absent); assert.equal(fresh.code, 0, fresh.out); assert.equal(existsSync(missing), false); }); // Row 322 (#1818), F8/F9: a clone without dependencies and a dependency-equipped copy. import { cpSync } from "node:fs"; function dependencyFreeClone(t) { const x = world(t); const clone = join(x.root, "clone"); mkdirSync(join(clone, "scripts"), { recursive: true }); for (const file of ["case2-install.sh", "case2-persona-import.mjs", "selector.sh"]) { cpSync(join(REPO, "scripts", file), join(clone, "scripts", file)); } mkdirSync(join(clone, 'packages/cli/src'), { recursive: true }); cpSync(join(REPO, 'packages/cli/src/deployment.mjs'), join(clone, 'packages/cli/src/deployment.mjs')); writeJson(x.config, { dataRoot: x.dataRoot }); // Isolate the persona step; bootstrap and business validation are covered above. script(join(clone, "scripts", "bootstrap.sh"), "exit 0"); script(join(clone, "scripts", "mosaic"), "exit 0"); assert.equal(existsSync(join(clone, "node_modules")), false); const run = () => { const r = spawnSync(join(clone, "scripts", "case2-install.sh"), [ "--business", "acme", "--business-file", x.input, "--commit", COMMIT, "--profile", "library", "--personas", x.staging, ], { env: env(x), encoding: "utf8", timeout: 30000 }); const out = r.stdout + r.stderr; clean(out); return { code: r.status, out }; }; return { ...x, clone, run }; } test("row 322 F8: library personas install from the copy with no clone node_modules", (t) => { const x = dependencyFreeClone(t); const r = x.run(); assert.equal(r.code, 0, r.out); for (const instance of ["bean", "chet"]) { assert.equal(readFileSync(join(x.personaDir, `${instance}.md`), "utf8"), PERSONA_TEXT[instance]); assert.equal(mode(join(x.personaDir, `${instance}.md`)), 0o600); } assert.equal(existsSync(join(x.clone, "node_modules")), false); }); // Without selector.sh beside it the install refuses before parsing, rather than skip the selector rules. test("the install refuses when its selector.sh is missing, before it reads or writes anything", (t) => { const x = dependencyFreeClone(t); rmSync(join(x.clone, "scripts", "selector.sh")); const r = x.run(); assert.equal(r.code, 4, r.out); assert.ok(r.out.endsWith(`case2-install: cannot read ${join(x.clone, "scripts", "selector.sh")}\n`), r.out); assert.equal(existsSync(x.business), false); }); test("row 322 F9: importer refusal keeps the error line rather than the Node banner", (t) => { const x = dependencyFreeClone(t); const copy = join(x.home, ".local", "share", "mosaic-stack", "copies", COMMIT); // Real Node module-load error: the error precedes its stack and version banner. const fake = join(x.bin, "stack-copy"); writeFileSync(fake, `${readFileSync(fake, "utf8")}\nrm -rf '${copy}/node_modules/yaml'\n`); const r = x.run(); refused(r, /persona-import: Error \[ERR_MODULE_NOT_FOUND\]: Cannot find package 'yaml'/); const refusal = r.out.split("\n").filter((line) => line.includes("refused:")); assert.equal(refusal.length, 1, r.out); assert.doesNotMatch(r.out, /Node\.js v|\n\s+at /); assert.equal(existsSync(x.business), false); assert.equal(existsSync(x.personaDir), false); }); test('C2-21: library install supplies chief identity and books publish for its real read grant', (t) => { const x = world(t); const out = install(x); assert.equal(out.code, 0, out.out); const doc = JSON.parse(readFileSync(x.business, 'utf8')); assert.deepEqual(doc.roles.chief.publish, ['books']); assert.ok(doc.roles.chief.identity?.name); assert.ok(doc.roles.chief.identity?.email); assert.equal(doc.roles.chief.identity?.gitea?.url, new URL(doc.projects.books.remote).origin); assert.ok(doc.roles.chief.identity?.gitea?.user); assert.notEqual(doc.roles.chief.identity?.gitea?.user, 'writer'); }); test('row334 install Next tells root to select proxy only when configured', t => { const x = world(t), r = install(x); assert.equal(r.code, 0, r.out); assert.match(r.out, /Next: root runs case2-unit.sh[^\n]+add --proxy when the config has a proxy section/); }); test('M3 install Next names the SSO, accounts-init and dedicated-uid steps before root re-pins with --sso', t => { const x = world(t), r = install(x); assert.equal(r.code, 0, r.out); const done = r.out.split('\n').findIndex(l => l.startsWith('case2-install: done. .deployed names this instance')); assert.ok(done >= 0, r.out); assert.match(r.out.split('\n')[done], /in order \(run sheet section 7\): prepare console\.sso and its client secret slots in .+; a human runs `mosaic accounts init` as [^;]+; [^ ]+ stays a dedicated uid with a non-login shell, the only member of its primary group\.$/); assert.match(r.out.split('\n')[done + 1], /^case2-install: Next: root runs case2-unit\.sh --copy [0-9a-f]{40} --business \S+ with --sso in the serve flags \(factory: --sso --send;/); // The installer names the human step; it never runs it. assert.equal(existsSync(join(x.configDir, 'accounts.json')), false); });