Files
stack/packages/cli/tests/case2-install.test.mjs

584 lines
29 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Case 2 row C2-13b (#1727): scripts/case2-install.sh installs a hub as <user>, 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 <user>: 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, "[email protected]"), "[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);
});