// Case 2 row C2-13a (#1732): scripts/case2-unit.sh renders and installs the hub's two system units,
// and scripts/case2-precheck.sh checks the host before they start. Brief: docs/plans/2026-10-20_case2-rows.md
// "C2-13a". Every case runs against a scratch unit directory, a scratch home and fake systemctl,
// getent, sudo, id, node, bwrap and claude. Nothing here reads or writes /etc/systemd,
// ~/.mosaic-dev or ~/.config/mosaic-dev, and no fixture value is a real credential.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createServer } from "node:net";
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
import { binds, prepareRun, sandboxArgv, sandboxProblem } from "../../seat/src/sandbox.mjs";
import { tmp } from "./helpers.mjs";
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";

const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const UNIT = join(REPO, "scripts", "case2-unit.sh");
const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
const BUS_TEMPLATE = join(REPO, "packages", "cli", "systemd", "mosaic-bus.service.in");
const WEBUI_TEMPLATE = join(REPO, "packages", "webui", "systemd", "mosaic-webui.service.in");
const COPY_A = "a".repeat(40);
const COPY_B = "b".repeat(40);
const UID = process.getuid();
const GID = process.getgid();
// A made-up key value; no output may hold it.
const FIXTURE_KEY = "fixture-zai-key-3c9e51";

// A root-owned executable outside /usr/local/bin, /usr/bin and /bin, for the PATH case.
function outsideNode() {
  const dir = "/usr/lib/systemd";
  for (const name of readdirSync(dir).sort()) {
    const p = join(dir, name);
    const s = statSync(p, { throwIfNoEntry: false });
    if (s?.isFile() && s.uid === 0 && (s.mode & 0o111) && (s.mode & 0o022) === 0 && realpathSync(p) === p) return p;
  }
  throw new Error(`no root-owned executable in ${dir}`);
}
const USR_NODE = realpathSync("/usr/bin/node");
const TRUE_BIN = realpathSync("/usr/bin/true");

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 hub: a home with two copies and a proof file, a unit directory, and fakes.
function hub(t, { uid = UID } = {}) {
  const root = tmp(t, "case2-unit-");
  const home = join(root, "home", "mosaic");
  for (const id of [COPY_A, COPY_B]) {
    mkdirSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts"), { recursive: true });
    writeFileSync(join(home, ".local", "share", "mosaic-stack", "copies", id, "scripts", "mosaic"), "");
  }
  const secrets = join(home, ".config", "mosaic-dev", "secrets");
  mkdirSync(secrets, { recursive: true, mode: 0o700 });
  const proof = join(secrets, "proxy-proof");
  writeFileSync(proof, "made-up-proof\n");
  chmodSync(proof, 0o600);
  const units = join(root, "units");
  mkdirSync(units);
  const bin = join(root, "bin");
  const log = join(root, "systemctl.log");
  script(join(bin, "getent"), `case "$1" in
  passwd) [ "$2" = mosaic ] && echo "mosaic:x:${uid}:${GID}::${home}:/bin/bash" ;;
  group) [ "$2" = "${GID}" ] && echo "mosaic:x:${GID}:" ;;
  *) exit 2 ;;
esac`);
  // is-active answers yes for each unit named in CASE2_TEST_ACTIVE.
  script(join(bin, "systemctl"), `echo "$*" >>'${log}'
if [ "$1" = is-active ]; then case " \${CASE2_TEST_ACTIVE:-} " in *" $3 "*) exit 0 ;; *) exit 3 ;; esac; fi
[ "$*" != "\${CASE2_TEST_FAIL:-}" ]`);
  const dataRoot = join(home, '.mosaic-dev'), query = join(root, 'query.json'), sudoLog = join(root, 'sudo.log');
  mkdirSync(dataRoot, { recursive: true });
  writeJson(query, { dataRoot });
  for (const copy of [COPY_A, COPY_B]) {
    const src = join(home, '.local/share/mosaic-stack/copies', copy, 'packages');
    mkdirSync(join(src, 'cli/src'), { recursive: true }); mkdirSync(join(src, 'webui/src'), { recursive: true });
    writeFileSync(join(src, 'cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>JSON.parse(fs.readFileSync(${JSON.stringify(query)},'utf8'));`);
    writeFileSync(join(src, 'webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
  }
  script(join(bin, 'sudo'), `${CASE_SUDO_SESSION}
echo "$*" >>'${sudoLog}'
[ "$1" = -u ] && [ "$2" = mosaic ] && [ "$3" = -H ] || exit 97
shift 3
[ "$1" = env ] && [ "$2" = -i ] && [ "$3" = 'HOME=${home}' ] || exit 98
if [ -f '${root}/query-hang' ]; then
 trap '' TERM
 (trap '' TERM; echo "$BASHPID" >>'${root}/query-pids'; exec sleep 60) &
 echo "$BASHPID" >>'${root}/query-pids'; wait; exit
fi
if [ -f '${root}/query-orphan' ]; then
 (trap '' TERM; here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here"); printf '%s %s\\n' "$here" "$sid" >'${root}/query-owned'; exec sleep 60) </dev/null >/dev/null 2>&1 &
 echo "$!" >'${root}/query-pids'
 cat '${root}/query-output'; exit 0
fi
if [ -f '${root}/query-output' ]; then cat '${root}/query-output'; exit "$(cat '${root}/query-status')"; fi
case_sudo_run '${root}' "$@"`);
  return { root, home, secrets, proof, units, bin, log, dataRoot, query, sudoLog };
}

const libraryFlags = (h) => ["--port", "443", "--business", "acme", "--public-host", "books.example", "--auth-header", "X-Auth-User",
  "--allow-user", "melanie", "--send", "--proof-header", "X-Proxy-Proof", "--proof-file", h.proof];
const factoryFlags = () => ["--port", "443", "--business", "acme", "--tester-socket"];

function unit(h, { profile = "library", node = USR_NODE, copy = COPY_A, extra = [], flags, env = {} } = {}) {
  const serve = flags ?? (profile === "factory" ? factoryFlags() : libraryFlags(h));
  const args = ["--user", "mosaic", "--home", h.home, "--business", "acme", "--copy", copy, "--node", node, "--profile", profile, ...extra, "--", ...serve];
  const r = spawnSync(UNIT, args, {
    encoding: "utf8",
    timeout: 30000,
    env: { PATH: `${h.bin}:${process.env.PATH}`, CASE2_UNIT_DIR: h.units, CASE2_SYSTEMCTL: join(h.bin, "systemctl"), CASE2_GETENT: join(h.bin, "getent"), ...env },
  });
  return { code: r.status, out: r.stdout, err: r.stderr };
}

const busFile = (h) => join(h.units, "mosaic-bus@.service");
const webuiFile = (h) => join(h.units, "mosaic-webui.service");
const read = (f) => readFileSync(f, "utf8");
const lines = (text, key) => text.split("\n").filter((l) => l.startsWith(`${key}=`)).map((l) => l.slice(key.length + 1));
const copyDir = (h, id = COPY_A) => join(h.home, ".local", "share", "mosaic-stack", "copies", id);
const HARDENING = (h) => ["ProtectSystem=strict", `ReadWritePaths=${h.home}`, "ProtectHome=yes", "PrivateTmp=yes"];

test('row253 factory socket uses actual config and target-uid query without widening hardening', t=>{
 const h=hub(t),custom=join(h.home,'custom');mkdirSync(custom);writeJson(h.query,{dataRoot:custom});
 const r=unit(h,{profile:'factory'});assert.equal(r.code,0,r.out+r.err);
 assert.deepEqual(lines(read(webuiFile(h)),'ExecStart'),[`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(' ')}`]);
 assert.deepEqual(read(webuiFile(h)).split('\n').filter(l=>/^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l)),HARDENING(h));
 assert.match(read(h.sudoLog),new RegExp(`^-u mosaic -H env -i HOME=${h.home} PATH=`, 'm'));
 assert.equal(existsSync(join(custom,'console')),false,'installer never creates the socket parent');
});

test('row253 installer refuses unsafe location and query facts before unit writes',t=>{
 const cases=[
  ['sibling prefix',h=>writeJson(h.query,{dataRoot:h.home+'2/data'}),/outside.*home/],
  ['dot',h=>writeJson(h.query,{dataRoot:h.home+'/./data'}),/normalized/],
  ['dotdot',h=>writeJson(h.query,{dataRoot:h.home+'/x/../data'}),/normalized/],
  ['double slash',h=>writeJson(h.query,{dataRoot:h.home+'//data'}),/normalized/],
  ['trailing slash',h=>writeJson(h.query,{dataRoot:h.home+'/data/'}),/normalized/],
  ['newline',h=>writeJson(h.query,{dataRoot:h.home+'/x\nP10 forged closed'}),/query/],
  ['non ASCII',h=>writeJson(h.query,{dataRoot:h.home+'/é'}),/query/],
  ['middle symlink',h=>{symlinkSync(h.dataRoot,join(h.home,'link'));writeJson(h.query,{dataRoot:join(h.home,'link','nested')});},/symlink/],
  ['long path',h=>writeJson(h.query,{dataRoot:join(h.home,'a'.repeat(108))}),/too.long/],
  ['parent file',h=>writeFileSync(join(h.dataRoot,'console'),'x',{mode:0o600}),/parent-type/],
  ['parent symlink',h=>symlinkSync(h.home,join(h.dataRoot,'console')),/symlink/],
  ['parent mode',h=>{mkdirSync(join(h.dataRoot,'console'));chmodSync(join(h.dataRoot,'console'),0o755);},/parent/],
  ['foreign parent',h=>{mkdirSync(join(h.dataRoot,'console'),{mode:0o700});},/parent/],
  ['unsupported',h=>writeFileSync(join(copyDir(h),'packages/webui/src/cli.mjs'),'// old copy'),/support/],
  ['schema',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true,extra:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
  ['wrong type',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:'true'}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
  ['noisy query',h=>{writeFileSync(join(h.root,'query-output'),'noise\n'+JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'0');},/query/],
  ['failed valid query',h=>{writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));writeFileSync(join(h.root,'query-status'),'1');},/query/],
 ];
 for(const [name,change,why] of cases){
  const h=hub(t,{uid:name==='foreign parent'?UID+4242:UID});change(h);
  const r=unit(h,{profile:'factory'});refuses(r,why);
  assert.equal(existsSync(busFile(h)),false,name);assert.equal(existsSync(webuiFile(h)),false,name);assert.equal(existsSync(h.log),false,name);
  assert.equal((r.out+r.err).includes('P10 forged closed'),false,name);
 }
});

test('row253 query timeout kills its process group and leaves no running child',async t=>{
 const h=hub(t);writeFileSync(join(h.root,'query-hang'),'');
 const r=unit(h,{profile:'factory'});refuses(r,/query/);
 const pids=read(join(h.root,'query-pids')).trim().split('\n').map(Number);assert.ok(pids.length>=2);
 for(const pid of pids){
  let state='gone';
  for(let n=0;n<50;n++){
   try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
   if(state==='gone'||state==='Z')break;
   await new Promise(resolve=>setTimeout(resolve,10));
  }
  assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
 }
 assert.equal(existsSync(h.log),false);assert.equal(existsSync(busFile(h)),false);
});

test('row253 protected homes refuse before any filesystem access',()=>{
 const source=read(UNIT), body=source.slice(source.indexOf('tester_location() {'),source.indexOf('# tester-location-end'));
 for(const home of ['/home/mosaic','/root/mosaic','/run/user/997','/tmp/mosaic','/var/tmp/mosaic']){
  const r=spawnSync('/bin/bash',['-c',body+'\nif tester_location "$1/data" "$1" 997 ""; then exit 99; fi; printf "%s" "$TS_REASON"','--',home],{encoding:'utf8'});
  assert.equal(r.status,0);assert.equal(r.stdout,'protected-private-home');
 }
});

// C4 cases run through both sudo process models. Exec shares the runner's
// session, so its session-wide check would be vacuous; exact recorded PIDs
// are checked in both modes. Hop additionally isolates the fake session.
for(const kind of ['sleep','signals','member']) test(`row253 C4 ${kind} query has exact inner reason and cleans all members`,async t=>{
 for(const mode of ['exec','hop']){
  const h=hub(t),sessions=join(h.root,'session-pids'),queries=join(h.root,'query-pids'),files=[sessions,queries];
  writeFileSync(join(h.root,'session-mode'),mode);
  const member=join(h.root,'q ) '+String.fromCharCode(10)+'mem'),proof=join(h.root,'comm-proof');
  if(kind==='member')symlinkSync('/usr/bin/sleep',member);
  writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),blockingQuerySource(queries,kind,member,proof));
  const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
  const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
  refuses(r,/^case2-unit: tester config query inner-timeout\n$/);
  assert.ok(seconds<7,`${kind}/${mode}: ${seconds}s exceeds outer bound`);
  assert.deepEqual(await pollFixtureGone(files),[],mode);
  if(kind==='member')assert.ok(read(proof).includes('('+member.split('/').at(-1)+') '),'member comm with newline and closing parenthesis was observed');
  assert.equal(existsSync(h.log),false,mode);await stopGuard();
 }
});

test('row253 C4 overflow is output-limit in both sudo modes',async t=>{
 for(const mode of ['exec','hop']){
  const h=hub(t),files=[join(h.root,'session-pids')];writeFileSync(join(h.root,'session-mode'),mode);const capture=cleanupFixture(t,files);
  writeFileSync(join(copyDir(h),'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:${JSON.stringify(h.dataRoot)}});`);
  const r=unit(h,{profile:'factory'});capture();refuses(r,/^case2-unit: tester config query output-limit\n$/);
  assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
  assert.deepEqual(await pollFixtureGone(files),[]);
 }
});

test('row253 C4 stalled supervisor uses the outer reason and explicit residual cleanup',async t=>{
 for(const mode of ['exec','hop']){
  const h=hub(t),file=join(h.root,'session-pids'),files=[file];writeFileSync(join(h.root,'session-mode'),mode);
  writeFileSync(join(h.root,'stall-config'),'');writeFileSync(join(h.root,'stall-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
  const capture=cleanupFixture(t,files);const stopGuard=fixtureGuard(t,files);
  const start=performance.now(),r=unit(h,{profile:'factory'}),seconds=(performance.now()-start)/1000;capture();
  refuses(r,/^case2-unit: tester config query outer-timeout\n$/);
  assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s slack includes root preflight`);
  assert.equal(r.out,'');assert.equal(existsSync(busFile(h)),false);assert.equal(existsSync(h.log),false);
  if(mode==='hop')assert.ok(recordedLivePids(file).length>0,'R3 residual exists before fixture cleanup');
  killRecordedPids(file);assert.deepEqual(await pollFixtureGone(files),[], 'explicit fixture cleanup of R3 residual');await stopGuard();
 }
});

test('row253 C4 inline supervisor is byte-identical in both host scripts',()=>{
 const extract=s=>s.split('# row253-supervisor-begin\n')[1]?.split('# row253-supervisor-end')[0];
 const a=extract(read(UNIT)),b=extract(read(join(REPO,'scripts/case1-accept.sh')));
 assert.ok(a?.length>100);assert.equal(a,b);
});

test('row253 C4 cleanup budget starts after inventory pass one',()=>{
 // A deterministic clock models a slow inventory without host-load timing.
 // All process reads and signals are stand-ins; no real PID is signalled.
 const source=read(UNIT);
 const body=source.slice(source.indexOf('stat_fields() {'),source.indexOf('finish() {'));
 const standins=`
child=424242; sleep_bin=:; seen=0; clocks=0; kills=0
stat_fields() {
 seen=$((seen+1)); ST_STATE=R; ST_SESSION=0
 [ "$seen" != 1 ] || ST_SESSION="$child"
 return 0
}
clock_cs() { clocks=$((clocks+1)); CLOCK_CS=$((clocks*1000)); }
kill() { kills=$((kills+1)); return 0; }
clean_session; rc=$?
printf '%s' "$kills"
exit "$rc"
`;
 const r=spawnSync('/bin/bash',['-c',body+standins],{encoding:'utf8'});
 assert.equal(r.status,0,r.stderr);assert.equal(r.stdout,'1');
});

test('row253 query reaps its group even when a child closes stdout before the query returns',async t=>{
 const h=hub(t);writeFileSync(join(h.root,'query-orphan'),'');
 writeFileSync(join(h.root,'query-output'),JSON.stringify({dataRoot:h.dataRoot,socket:true}));
 let pid;
 const capture=cleanupFixture(t,[join(h.root,'query-owned')]);
 const r=unit(h,{profile:'factory'});capture();
 pid=Number(read(join(h.root,'query-pids')).trim());
 assert.equal(r.code,0,r.out+r.err);
 let state='gone';
 for(let n=0;n<50;n++){
  try{state=read(`/proc/${pid}/stat`).split(') ')[1].split(' ')[0];}catch{state='gone';}
  if(state==='gone'||state==='Z')break;
  await new Promise(resolve=>setTimeout(resolve,10));
 }
 assert.ok(state==='gone'||state==='Z',`query descendant ${pid} still ${state}`);
});

test('row253 config parser is strict',()=>{
 const source=read(UNIT), body=source.slice(source.indexOf('tester_facts() {'),source.indexOf('tester_location() {'));
 const valid=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1" && printf "%s %s" "$TS_DATA" "$TS_MARKED"','--','{"dataRoot":"/srv/mosaic/data","socket":true}'],{encoding:'utf8'});
 assert.equal(valid.status,0);assert.equal(valid.stdout,'/srv/mosaic/data true');
 const facts=['{"dataRoot":"/srv/mosaic/data","socket":true,"extra":1}','{"dataRoot":4,"socket":true}','{"dataRoot":"/srv/mosaic/data","socket":"true"}','{"dataRoot":"/srv/mosaic/data","socket":true}\nP10 forged closed'];
 for(const fact of facts){
  const r=spawnSync('/bin/bash',['-c',body+'\ntester_facts "$1"','--',fact]);
  assert.notEqual(r.status,0,fact);
 }
});

function refuses(r, pattern) {
  assert.equal(r.code, 1, `expected a refusal, got ${r.code}: ${r.out}${r.err}`);
  assert.match(r.err, pattern);
}

// A template copy with one edit, for the template cases.
function template(h, from, name, edit) {
  const file = join(h.root, name);
  writeFileSync(file, edit(read(from)));
  return file;
}

test("a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console", (t) => {
  const h = hub(t);
  const r = unit(h);
  assert.equal(r.code, 0, r.err);
  for (const f of [busFile(h), webuiFile(h)]) assert.equal(statSync(f).mode & 0o777, 0o644);
  const bus = read(busFile(h));
  const webui = read(webuiFile(h));
  assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin"]);
  assert.deepEqual(lines(bus, "ExecStart"), [`${copyDir(h)}/scripts/mosaic bus start %i`]);
  assert.deepEqual(lines(bus, "EnvironmentFile"), [`-${h.home}/.config/mosaic-dev/secrets/claude-session.env`]);
  assert.deepEqual(lines(webui, "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${libraryFlags(h).join(" ")}`]);
  assert.deepEqual(lines(webui, "EnvironmentFile"), []);
  for (const text of [bus, webui]) {
    assert.deepEqual(lines(text, "WantedBy"), ["multi-user.target"]);
    assert.deepEqual(lines(text, "User"), ["mosaic"]);
    assert.deepEqual(lines(text, "Group"), ["mosaic"]);
    assert.deepEqual(lines(text, "NoNewPrivileges"), ["yes"]);
    const hardening = text.split("\n").filter((l) => /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l));
    assert.deepEqual(hardening, HARDENING(h));
    assert.doesNotMatch(text, /@[A-Z_]+@|%h|MOSAIC_CONFIG=|MOSAIC_CLAUDE_BIN|After=|Wants=/);
  }
  assert.deepEqual(lines(bus, "AmbientCapabilities"), []);
  assert.deepEqual(lines(bus, "CapabilityBoundingSet"), []);
  assert.deepEqual(lines(webui, "AmbientCapabilities"), ["CAP_NET_BIND_SERVICE"]);
  assert.deepEqual(lines(webui, "CapabilityBoundingSet"), ["CAP_NET_BIND_SERVICE"]);
  assert.match(r.out, /enabled and started mosaic-bus@acme\.service, then mosaic-webui\.service\n$/);
  assert.deepEqual(read(h.log).trim().split("\n"), [
    "is-active --quiet mosaic-bus@acme.service",
    "is-active --quiet mosaic-webui.service",
    "daemon-reload",
    "enable mosaic-bus@acme.service mosaic-webui.service",
    "start mosaic-bus@acme.service",
    "start mosaic-webui.service",
  ]);
});

test("PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node", (t) => {
  const h = hub(t);
  const node = outsideNode();
  assert.equal(unit(h, { node }).code, 0);
  for (const f of [busFile(h), webuiFile(h)]) assert.deepEqual(lines(read(f), "Environment"), [`PATH=${dirname(node)}:/usr/local/bin:/usr/bin:/bin`]);
  assert.match(lines(read(webuiFile(h)), "ExecStart")[0], new RegExp(`^${node} `));
});

test("a failed bus start refuses with the Console not started", (t) => {
  const h = hub(t);
  const r = unit(h, { env: { CASE2_TEST_FAIL: "start mosaic-bus@acme.service" } });
  refuses(r, /start mosaic-bus@acme\.service failed; the Console is not started/);
  assert.deepEqual(read(h.log).trim().split("\n").slice(2), ["daemon-reload", "enable mosaic-bus@acme.service mosaic-webui.service", "start mosaic-bus@acme.service"]);
});

test("the script never links a unit file", () => {
  assert.doesNotMatch(read(UNIT), /systemctl link/);
});

test("factory renders --pm once and one MOSAIC_CLAUDE_BIN line, and without them both tokens render empty", (t) => {
  const h = hub(t);
  assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
  const bus = read(busFile(h));
  const exec = lines(bus, "ExecStart");
  assert.equal(exec.length, 1);
  assert.match(exec[0], / bus start %i --pm$/);
  assert.equal(exec[0].split("--pm").length, 2);
  assert.deepEqual(lines(bus, "Environment"), ["PATH=/usr/bin:/usr/local/bin:/bin", `MOSAIC_CLAUDE_BIN=${TRUE_BIN}`]);
  assert.deepEqual(lines(read(webuiFile(h)), "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${factoryFlags().join(" ")}`]);

  const plain = hub(t);
  assert.equal(unit(plain, { profile: "factory" }).code, 0);
  const text = read(busFile(plain));
  assert.deepEqual(lines(text, "ExecStart"), [`${copyDir(plain)}/scripts/mosaic bus start %i`]);
  assert.doesNotMatch(text, /MOSAIC_CLAUDE_BIN|@PM_FLAGS@|@CLAUDE_ENV@/);
});

test("a re-install over running units says they are still on the old copy and must restart together, never that it started them", (t) => {
  const h = hub(t);
  assert.equal(unit(h).code, 0);
  const both = unit(h, { copy: COPY_B, env: { CASE2_TEST_ACTIVE: "mosaic-bus@acme.service mosaic-webui.service" } });
  assert.equal(both.code, 0, both.err);
  assert.doesNotMatch(both.out, /started/);
  assert.match(both.out, new RegExp(`mosaic-bus@acme\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
  assert.match(both.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_A}, not copy ${COPY_B}`));
  assert.match(both.out, new RegExp(`restart mosaic-bus@acme\\.service and mosaic-webui\\.service together to put both on copy ${COPY_B}`));
  assert.deepEqual(read(h.log).trim().split("\n").slice(-2), ["start mosaic-bus@acme.service", "start mosaic-webui.service"]);

  // Only the Console ran (the bus had stopped): the bus starts on the new copy, the Console stays.
  const one = unit(h, { copy: COPY_A, env: { CASE2_TEST_ACTIVE: "mosaic-webui.service" } });
  assert.equal(one.code, 0, one.err);
  assert.match(one.out, new RegExp(`started mosaic-bus@acme\\.service on copy ${COPY_A}`));
  assert.match(one.out, new RegExp(`mosaic-webui\\.service was already running, so it is still on copy ${COPY_B}, not copy ${COPY_A}`));
  assert.doesNotMatch(one.out, /started mosaic-webui/);
  assert.match(one.out, /restart mosaic-bus@acme\.service and mosaic-webui\.service together/);
});

test("a re-install to another copy re-renders both units, and an edited installed unit refuses", (t) => {
  const h = hub(t);
  assert.equal(unit(h).code, 0);
  assert.equal(unit(h, { copy: COPY_B }).code, 0);
  assert.match(read(busFile(h)), new RegExp(`copies/${COPY_B}/scripts/mosaic`));
  assert.doesNotMatch(read(busFile(h)) + read(webuiFile(h)), new RegExp(COPY_A));

  const edits = [
    [busFile, (s) => s.replace("User=mosaic", "User=root"), /User= must be exactly once in \[Service\], as User=mosaic/],
    [webuiFile, (s) => s.replace(/^ExecStart=(.*)$/m, "ExecStart=$1 --board http://127.0.0.1:1"), /ExecStart= must be exactly once/],
    [busFile, (s) => s.replace("ProtectHome=yes\n", ""), /ProtectHome= must be exactly once/],
    [webuiFile, (s) => s.replace("PrivateTmp=yes\n", ""), /PrivateTmp= must be exactly once/],
    [busFile, (s) => s.replace("Type=simple", "Type=simple\nX-Note=%h"), /%h is left/],
  ];
  for (const [file, edit, pattern] of edits) {
    const before = read(file(h));
    writeFileSync(file(h), edit(before));
    refuses(unit(h, { copy: COPY_B }), pattern);
    writeFileSync(file(h), before);
  }
  assert.equal(unit(h, { copy: COPY_B }).code, 0);
});

test("a template with a namespace, user or system call restriction refuses", (t) => {
  const h = hub(t);
  for (const line of ["RestrictNamespaces=yes", "PrivateUsers=yes", "SystemCallFilter=@system-service"]) {
    const key = line.split("=")[0];
    for (const [from, env] of [[BUS_TEMPLATE, "CASE2_BUS_TEMPLATE"], [WEBUI_TEMPLATE, "CASE2_WEBUI_TEMPLATE"]]) {
      const file = template(h, from, `t-${key}.in`, (s) => s.replace("NoNewPrivileges=yes", `NoNewPrivileges=yes\n${line}`));
      refuses(unit(h, { env: { [env]: file } }), new RegExp(`refusing ${key}=`));
    }
  }
  assert.equal(existsSync(busFile(h)), false);
});

test("the bus template's EnvironmentFile line must be there and point at the session env, and the Console template must have none", (t) => {
  const h = hub(t);
  const gone = template(h, BUS_TEMPLATE, "no-env.in", (s) => s.replace(/^EnvironmentFile=.*\n/m, ""));
  refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: gone } }), /EnvironmentFile= must be exactly once/);
  const elsewhere = template(h, BUS_TEMPLATE, "env-elsewhere.in", (s) => s.replace("EnvironmentFile=-@SESSION_ENV@", "EnvironmentFile=-/etc/other.env"));
  refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: elsewhere } }), /EnvironmentFile= must be exactly once/);
  const added = template(h, WEBUI_TEMPLATE, "webui-env.in", (s) => s.replace("NoNewPrivileges=yes", "NoNewPrivileges=yes\nEnvironmentFile=-/etc/other.env"));
  refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: added } }), /refusing EnvironmentFile=/);
});

test("a template holding an unknown token, PermissionsStartOnly or a privileged Exec prefix refuses", (t) => {
  const h = hub(t);
  const token = template(h, BUS_TEMPLATE, "token.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
  refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: token } }), /a token is left after the render: @SECRET@/);
  const webuiToken = template(h, WEBUI_TEMPLATE, "wtoken.in", (s) => s.replace("Type=simple", "Type=simple\nX-Note=@SECRET@"));
  refuses(unit(h, { env: { CASE2_WEBUI_TEMPLATE: webuiToken } }), /a token other than one @FLAGS@/);
  const pso = template(h, BUS_TEMPLATE, "pso.in", (s) => s.replace("Type=simple", "Type=simple\nPermissionsStartOnly=yes"));
  refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: pso } }), /refusing PermissionsStartOnly=/);
  for (const pre of ["+/srv/mosaic/x", "-+/srv/mosaic/x"]) {
    const file = template(h, BUS_TEMPLATE, "pre.in", (s) => s.replace("Type=simple", `Type=simple\nExecStartPre=${pre}`));
    refuses(unit(h, { env: { CASE2_BUS_TEMPLATE: file } }), /an Exec line that runs with full privileges: ExecStartPre=/);
  }
});

test("a symlink at either unit path refuses", (t) => {
  for (const name of ["mosaic-bus@.service", "mosaic-webui.service"]) {
    const h = hub(t);
    const target = join(h.root, "elsewhere");
    writeFileSync(target, "");
    symlinkSync(target, join(h.units, name));
    refuses(unit(h), new RegExp(`${name.replace(".", "\\.")} is a symlink`));
  }
});

test("a --node that is relative, not root-owned or under <home> refuses", (t) => {
  const h = hub(t);
  refuses(unit(h, { node: "bin/node" }), /--node must be an absolute path/);
  const mine = script(join(h.root, "opt", "node"), "exit 0");
  refuses(unit(h, { node: mine }), /--node: .* is not owned by root/);
  const inside = script(join(h.home, "bin", "node"), "exit 0");
  refuses(unit(h, { node: inside }), /--node must be outside/);
});

test("a re-install with a different --node or different serve flags refuses", (t) => {
  const h = hub(t);
  assert.equal(unit(h).code, 0);
  // A new node moves PATH too, so the installed unit fails its own Environment= check first.
  refuses(unit(h, { node: outsideNode() }), /installed .*: the Environment= lines must be exactly/);
  const flags = libraryFlags(h).map((f) => (f === "melanie" ? "someone" : f));
  refuses(unit(h, { flags }), /installed .*: ExecStart= must be exactly once/);
  // A line check_unit doesn't fix still has to equal the render.
  writeFileSync(busFile(h), read(busFile(h)).replace(/^Description=.*$/m, "Description=edited"));
  refuses(unit(h), /differs from the render in more than the copy id/);
});

test("an installed pair whose units name different copies refuses", (t) => {
  const h = hub(t);
  assert.equal(unit(h).code, 0);
  writeFileSync(webuiFile(h), read(webuiFile(h)).replaceAll(COPY_A, COPY_B));
  refuses(unit(h), new RegExp(`the installed units name different copies, ${COPY_A} and ${COPY_B}`));
});

test("a copy id that is short, uppercase, holds / or .., or names no directory refuses", (t) => {
  const h = hub(t);
  for (const copy of ["a".repeat(39), "A".repeat(40), `${"a".repeat(19)}/${"a".repeat(20)}`, `..${"a".repeat(38)}`]) {
    refuses(unit(h, { copy }), /--copy must be a full 40-character lower-case commit id/);
  }
  refuses(unit(h, { copy: "c".repeat(40) }), /no copy directory/);
});

test("library refuses a privileged-floor miss, a bad proof pair and every flag outside its set", (t) => {
  const h = hub(t);
  const base = libraryFlags(h);
  const without = (...names) => {
    const out = [];
    for (let i = 0; i < base.length; i++) {
      if (names.includes(base[i])) { if (base[i] !== "--send") i++; continue; }
      out.push(base[i]);
    }
    return out;
  };
  refuses(unit(h, { flags: base.map((f) => (f === "443" ? "7332" : f)) }), /--port must be 1\.\.1023/);
  refuses(unit(h, { flags: without("--proof-header", "--proof-file") }), /serve flag --proof-header is required/);
  refuses(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f)) }), /must be an Authentik uid/);
  assert.equal(unit(h, { flags: base.map((f) => (f === "X-Auth-User" ? "X-Authentik-Uid" : f === "melanie" ? "0".repeat(64) : f)) }).code, 0);
  rmSync(busFile(h));
  rmSync(webuiFile(h));
  for (const [flag, value, pattern] of [
    ["--tester-port", "7333", /case2-unit.sh retires tester TCP; use --tester-socket/],
    ["--tester-socket", null, /the library profile refuses --tester-socket/],
    ["--checkout", "/srv/x", /refusing --checkout/],
    ["--board", "http://127.0.0.1:7331", /refusing --board/],
    ["--sso", null, /refusing --sso/],
    ["--nonsense", "1", /unknown serve flag: --nonsense/],
  ]) {
    refuses(unit(h, { flags: [...base, flag, ...(value === null ? [] : [value])] }), pattern);
  }
  refuses(unit(h, { flags: base.map((f) => (f === "acme" ? "other" : f)) }), /serve --business other is not the unit's business acme/);
  refuses(unit(h, { extra: ["--pm"] }), /the library profile refuses --pm/);
  refuses(unit(h, { extra: ["--claude", TRUE_BIN] }), /the library profile refuses --claude/);

  chmodSync(h.proof, 0o644);
  refuses(unit(h), /mode 0600 or 0400, not 644/);
  chmodSync(h.proof, 0o600);
  const link = join(h.secrets, "proof-link");
  symlinkSync(h.proof, link);
  refuses(unit(h, { flags: base.map((f) => (f === h.proof ? link : f)) }), /must not be a symlink/);
  const dataRoot = join(h.home, ".mosaic-dev");
  mkdirSync(dataRoot, { recursive: true });
  const outside = join(dataRoot, "proof");
  writeFileSync(outside, "made-up-proof\n");
  chmodSync(outside, 0o600);
  refuses(unit(h, { flags: base.map((f) => (f === h.proof ? outside : f)) }), /must be under .*secrets, outside the data root/);
  assert.equal(existsSync(busFile(h)), false);
});

// A non-root test can't make a root-owned file under the scratch home, so the
// owner case names another uid in the passwd database instead.
test("library refuses a proof file not owned by <user>'s uid", (t) => {
  const h = hub(t, { uid: UID + 4242 });
  refuses(unit(h), new RegExp(`proxy proof file must be owned by mosaic \\(uid ${UID + 4242}\\)`));
});

test("factory refuses duplicate socket, a missing --port or --business, and each proxy flag", (t) => {
  const h = hub(t);
  refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--tester-socket'] }), /given twice/);
  refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--port', '443'] }), /given twice/);
  refuses(unit(h, { profile: "factory", flags: ["--business", "acme"] }), /serve flag --port is required/);
  refuses(unit(h, { profile: "factory", flags: ["--port", "443"] }), /serve flag --business is required/);
  for (const [flag, value] of [["--public-host", "x.example"], ["--auth-header", "X-A"], ["--allow-user", "u"], ["--proof-header", "X-P"], ["--proof-file", h.proof], ["--send", null]]) {
    refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), new RegExp(`the factory profile refuses ${flag}`));
  }
  for (const [flag, value, pattern] of [["--checkout", "/srv/x", /refusing --checkout/], ["--board", "http://127.0.0.1:7331", /refusing --board/], ["--sso", null, /refusing --sso/], ["--nonsense", "1", /unknown serve flag/]]) {
    refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), pattern);
  }
});

test("row253 factory refuses retired TCP and a value after the socket marker", (t) => {
  const h = hub(t);
  refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);
  refuses(unit(h, { profile: 'factory', flags: [...factoryFlags(), '7333'] }), /unknown serve flag: 7333/);
});

test("factory refuses a --claude that is relative, not root-owned or under <home>, and a re-install with another --pm or --claude", (t) => {
  const h = hub(t);
  refuses(unit(h, { profile: "factory", extra: ["--claude", "bin/claude"] }), /--claude must be an absolute path/);
  const mine = script(join(h.root, "opt", "claude"), "exit 0");
  refuses(unit(h, { profile: "factory", extra: ["--claude", mine] }), /--claude: .* is not owned by root/);
  const inside = script(join(h.home, "bin", "claude"), "exit 0");
  refuses(unit(h, { profile: "factory", extra: ["--claude", inside] }), /--claude must be outside/);

  assert.equal(unit(h, { profile: "factory", extra: ["--pm", "--claude", TRUE_BIN] }).code, 0);
  refuses(unit(h, { profile: "factory", extra: ["--claude", TRUE_BIN] }), /--pm and --claude must equal the installed ones|ExecStart= must be exactly once/);
  refuses(unit(h, { profile: "factory", extra: ["--pm"] }), /Environment= lines must be exactly|differs from the render/);
  refuses(unit(h, { profile: "factory", extra: ["--pm", "--claude", USR_NODE] }), /Environment= lines must be exactly|differs from the render/);
});

// ---- case2-precheck.sh ----

// A scratch user for the precheck: a config, a business file, an installed Console unit,
// fakes on PATH and a fake /proc/sys.
function host(t, { roles = {}, vars = {}, keyFile = `ZAI_API_KEY=${FIXTURE_KEY}\n`, port = "443", busClaude = null } = {}) {
  const root = tmp(t, "case2-pre-");
  const home = join(root, "home");
  const configDir = join(home, ".config", "mosaic-dev");
  const config = writeJson(join(configDir, "config.json"), {
    configVersion: 1,
    environment: "production",
    dataRoot: join(home, ".mosaic-dev"),
    execution: { backend: "docker", provider: "zai", model: "glm-5.3-flash", adapter: "pi" },
  });
  const doc = businessDoc(root);
  doc.roles.cto.vars = { harness: "pi" };
  for (const [name, v] of Object.entries(roles)) doc.roles[name].vars = v;
  Object.assign(doc.vars, vars);
  writeJson(join(configDir, "businesses", "acme.json"), doc);
  const secrets = join(configDir, "secrets");
  mkdirSync(secrets, { recursive: true, mode: 0o700 });
  if (keyFile !== null) {
    writeFileSync(join(secrets, "claude-session.env"), keyFile);
    chmodSync(join(secrets, "claude-session.env"), 0o600);
  }
  const units = join(root, "units");
  mkdirSync(units);
  writeFileSync(join(units, "mosaic-webui.service"), `[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --port ${port} --business acme\n`);
  writeFileSync(join(units, "mosaic-bus@.service"), `[Service]\nEnvironment=PATH=/usr/bin\n${busClaude ? `Environment=MOSAIC_CLAUDE_BIN=${busClaude}\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 bin = join(root, "bin");
  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 "\${CASE2_TEST_GROUPS:-${GID}}"; else exec /usr/bin/id "$@"; fi`);
  script(join(bin, "node"), `if [ "$1" = --version ]; then echo "\${CASE2_TEST_NODE:-v24.3.0}"; exit 0; fi\nexec '${process.execPath}' "$@"`);
  script(join(bin, "bwrap"), `exit "\${CASE2_TEST_BWRAP:-0}"`);
  return { root, home, configDir, config, secrets, units, sys, bin, dataRoot: join(home, ".mosaic-dev") };
}

// A fake claude: replace answers with the marker and a small prompt; append answers with a large one;
// nomarker answers with a small prompt and no marker; text answers with plain text, not JSON.
function fakeClaude(dir, kind) {
  return script(join(dir, "claude"), `[ "$1" = --version ] && { echo "9.9.9 (Claude Code, ${kind} fake)"; exit 0; }
file=''
while [ $# -gt 0 ]; do [ "$1" = --system-prompt-file ] && file="$2"; shift; done
marker="$(awk '{ print $NF }' "$file")"
${kind === "nomarker" ? "marker=something-else" : ""}
${kind === "text" ? 'echo "$marker"; exit 0' : ""}
tokens=${kind === "append" ? 9000 : 40}
printf '{"result":"%s","usage":{"input_tokens":%s,"cache_read_input_tokens":0}}\\n' "$marker" "$tokens"`);
}

// This host may have a claude in /usr/bin, so this PATH links every /usr/bin entry but that one.
function pathWithoutClaude(x) {
  const usr = join(x.root, "usr-bin");
  mkdirSync(usr);
  for (const name of readdirSync("/usr/bin")) if (name !== "claude") symlinkSync(join("/usr/bin", name), join(usr, name));
  return `${x.bin}:${usr}`;
}

function precheck(x, { profile = "library", env = {}, path } = {}) {
  const r = spawnSync(PRECHECK, ["--business", "acme", "--profile", profile], {
    encoding: "utf8",
    timeout: 60000,
    env: {
      PATH: path ?? `${x.bin}:/usr/bin:/bin`,
      HOME: x.home,
      MOSAIC_CONFIG: x.config,
      CASE2_UNIT_DIR: x.units,
      CASE2_PROC_SYS: x.sys,
      CASE2_GETENT: join(x.bin, "getent"),
      ...env,
    },
  });
  const out = r.stdout + r.stderr;
  assert.doesNotMatch(out, new RegExp(FIXTURE_KEY), "the precheck printed the fixture key");
  return { code: r.status, out };
}

function blocked(r, pattern) {
  assert.equal(r.code, 1, r.out);
  assert.match(r.out, pattern);
}

test("the precheck passes on a good host and prints free memory, without the key", (t) => {
  const x = host(t);
  const r = precheck(x);
  assert.equal(r.code, 0, r.out);
  assert.match(r.out, /ok {6}config validates/);
  assert.match(r.out, /ok {6}bus\.tester is off/);
  assert.match(r.out, /ok {6}net\.ipv4\.ip_unprivileged_port_start 1024 is above the Console's port 443/);
  assert.match(r.out, /claude-session\.env sets ZAI_API_KEY/);
  assert.match(r.out, /free memory: \d+ MiB/);
  assert.doesNotMatch(r.out, /claude .*fake|marker/);
});

test("the precheck refuses each failed host check", (t) => {
  blocked(precheck(host(t), { env: { CASE2_TEST_NODE: "v22.22.2" } }), /REFUSED node v22\.22\.2 is older than 24/);
  blocked(precheck(host(t), { env: { CASE2_TEST_GROUPS: `${GID} 998` } }), /REFUSED id -G prints more than one group/);
  const sudo = host(t);
  script(join(sudo.bin, "sudo"), "exit 0");
  blocked(precheck(sudo), /REFUSED sudo -n true succeeds/);
  blocked(precheck(host(t), { env: { XDG_DATA_HOME: "/elsewhere" } }), /REFUSED XDG_DATA_HOME is \/elsewhere/);
  const home = host(t);
  assert.equal(precheck(home, { env: { XDG_DATA_HOME: `${home.home}/.local/share` } }).code, 0);
  blocked(precheck(host(t), { env: { HOME: "/tmp" } }), /REFUSED HOME is \/tmp/);

  const runs = host(t);
  mkdirSync(join(runs.dataRoot, "launches", "acme"), { recursive: true, mode: 0o755 });
  chmodSync(join(runs.dataRoot, "launches", "acme"), 0o755);
  blocked(precheck(runs), /REFUSED run root .* is not a 0700 directory \(mode 755\)/);
  chmodSync(join(runs.dataRoot, "launches", "acme"), 0o700);
  assert.match(precheck(runs).out, /ok {6}run root .* is 0700/);

  const bad = host(t);
  writeFileSync(bad.config, "{ not json");
  blocked(precheck(bad), /REFUSED config:/);
});

test("the precheck refuses a user missing from passwd, a business file that does not load, and a symlinked run root", (t) => {
  const nouser = host(t);
  script(join(nouser.bin, "getent"), "exit 2");
  blocked(precheck(nouser), /REFUSED user: no .* in the passwd database/);

  const unparsed = host(t);
  writeFileSync(join(unparsed.configDir, "businesses", "acme.json"), "{ not json");
  blocked(precheck(unparsed), /REFUSED business file: cannot read .*acme\.json/);
  const gone = host(t);
  rmSync(join(gone.configDir, "businesses", "acme.json"));
  blocked(precheck(gone), /REFUSED business file: cannot read .*acme\.json: ENOENT/);
  const invalid = host(t);
  const file = join(invalid.configDir, "businesses", "acme.json");
  const doc = JSON.parse(read(file));
  doc.notAField = true;
  writeJson(file, doc);
  const r = precheck(invalid);
  blocked(r, /REFUSED business file: /);
  assert.doesNotMatch(r.out, /REFUSED business file: cannot read/);

  // stat follows a link, so a link to a 0700 directory would read as 0700.
  const link = host(t);
  const target = join(link.root, "elsewhere");
  mkdirSync(target, { mode: 0o700 });
  chmodSync(target, 0o700);
  mkdirSync(join(link.dataRoot, "launches"), { recursive: true });
  symlinkSync(target, join(link.dataRoot, "launches", "acme"));
  blocked(precheck(link), /REFUSED run root .* is a symlink/);
});

test("a bwrap that fails refuses and prints the sysctl and where bwrap is", (t) => {
  const x = host(t);
  blocked(precheck(x, { env: { CASE2_TEST_BWRAP: "1" } }), new RegExp(`REFUSED bwrap .* failed; bwrap: ${join(x.bin, "bwrap")}; kernel\\.apparmor_restrict_unprivileged_userns: 1`));
});

test("an unprivileged port floor at or below the Console's port refuses, and so does a missing Console unit", (t) => {
  for (const floor of ["443", "80"]) {
    const x = host(t);
    writeFileSync(join(x.sys, "net", "ipv4", "ip_unprivileged_port_start"), `${floor}\n`);
    blocked(precheck(x), new RegExp(`REFUSED net\\.ipv4\\.ip_unprivileged_port_start is ${floor}, at or below the Console's port 443`));
  }
  const x = host(t);
  rmSync(join(x.units, "mosaic-webui.service"));
  blocked(precheck(x), /REFUSED no installed Console unit/);
  const noPort = host(t);
  writeFileSync(join(noPort.units, "mosaic-webui.service"), "[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --business acme\n");
  blocked(precheck(noPort), /REFUSED .*mosaic-webui\.service has no --port in its ExecStart/);
  const noFloor = host(t);
  rmSync(join(noFloor.sys, "net", "ipv4", "ip_unprivileged_port_start"));
  blocked(precheck(noFloor), /REFUSED cannot read net\.ipv4\.ip_unprivileged_port_start/);
});

test("bus.tester: library refuses it on, factory needs it on, and factory refuses a library business", (t) => {
  blocked(precheck(host(t, { vars: { "bus.tester": true } })), /REFUSED bus\.tester is on; the library profile needs it off/);
  blocked(precheck(host(t), { profile: "factory" }), /REFUSED bus\.tester is off; the factory profile needs it on/);
  const on = precheck(host(t, { vars: { "bus.tester": true } }), { profile: "factory" });
  assert.equal(on.code, 0, on.out);
  const lib = host(t, { vars: { "bus.tester": true } });
  const file = join(lib.configDir, "businesses", "acme.json");
  const doc = JSON.parse(read(file));
  doc.projects.stack.kind = "library";
  writeJson(file, doc);
  blocked(precheck(lib, { profile: "factory" }), /REFUSED profile-library-business: .* projects stack are library projects/);
});

test("the claude check runs only for a claude-code instance, and refuses a claude that appends or answers wrong", (t) => {
  // No claude-code instance and no claude anywhere on PATH: the precheck passes.
  const none = host(t);
  const r = precheck(none, { path: pathWithoutClaude(none) });
  assert.equal(r.code, 0, r.out);
  assert.doesNotMatch(r.out, /--system-prompt-file|claude is installed/);

  const good = host(t, { roles: { cto: { harness: "claude-code" } } });
  fakeClaude(good.bin, "replace");
  const pass = precheck(good);
  assert.equal(pass.code, 0, pass.out);
  assert.match(pass.out, /claude .*9\.9\.9 \(Claude Code, replace fake\)/);
  assert.match(pass.out, /ok {6}claude replaces its prompt with --system-prompt-file \(40 input tokens\)/);

  const bad = host(t, { roles: { cto: { harness: "claude-code" } } });
  fakeClaude(bad.bin, "append");
  blocked(precheck(bad), /REFUSED claude adds to its own prompt with --system-prompt-file: 9000 input tokens/);

  const nomarker = host(t, { roles: { cto: { harness: "claude-code" } } });
  fakeClaude(nomarker.bin, "nomarker");
  blocked(precheck(nomarker), /REFUSED claude's reply to the marker prompt lacks the marker \(40 input tokens\)/);

  const text = host(t, { roles: { cto: { harness: "claude-code" } } });
  fakeClaude(text.bin, "text");
  blocked(precheck(text), /REFUSED claude gave no JSON reply to the marker prompt/);

  const missing = host(t, { roles: { cto: { harness: "claude-code" } } });
  blocked(precheck(missing, { path: pathWithoutClaude(missing) }), /REFUSED an instance runs claude-code and no claude is installed/);
});

test("the claude check runs the bus unit's MOSAIC_CLAUDE_BIN over claude on PATH", (t) => {
  const pinned = join(tmp(t, "case2-claude-"), "pinned");
  const replace = fakeClaude(pinned, "replace");
  const x = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: replace });
  fakeClaude(x.bin, "append");
  const r = precheck(x);
  assert.equal(r.code, 0, r.out);
  assert.match(r.out, /replace fake/);

  const pinnedBad = join(tmp(t, "case2-claude-"), "pinned");
  const append = fakeClaude(pinnedBad, "append");
  const y = host(t, { roles: { cto: { harness: "claude-code" } }, busClaude: append });
  fakeClaude(y.bin, "replace");
  blocked(precheck(y), /REFUSED claude adds to its own prompt/);
});

test("the session env file refuses when missing, open to others, a symlink, empty, or emptied by a later line", (t) => {
  blocked(precheck(host(t, { keyFile: null })), /REFUSED an instance's provider is zai and .*claude-session\.env is missing/);
  const open = host(t);
  chmodSync(join(open.secrets, "claude-session.env"), 0o644);
  blocked(precheck(open), /REFUSED .*claude-session\.env must be owned by .* at 0600 \(mode 644\)/);
  const link = host(t, { keyFile: null });
  const real = join(link.root, "real.env");
  writeFileSync(real, `ZAI_API_KEY=${FIXTURE_KEY}\n`);
  chmodSync(real, 0o600);
  symlinkSync(real, join(link.secrets, "claude-session.env"));
  blocked(precheck(link), /REFUSED .*claude-session\.env is a symlink/);
  blocked(precheck(host(t, { keyFile: "ZAI_API_KEY=\n" })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
  blocked(precheck(host(t, { keyFile: `ZAI_API_KEY=${FIXTURE_KEY}\nZAI_API_KEY=\n` })), /REFUSED .*sets no nonempty ZAI_API_KEY/);
  const quoted = precheck(host(t, { keyFile: `ZAI_API_KEY=\nZAI_API_KEY="${FIXTURE_KEY}"\n` }));
  assert.equal(quoted.code, 0, quoted.out);
});

// ---- the bind test (mos-1100) ----

// A session can't take the Console's port: inside the sandbox a real session gets, a bind below
// 1024 fails with EACCES, and the same bind on a free port of 1024 or above succeeds.
const sandboxSkip = sandboxProblem() ?? false;
const LOW_PORT = 980;
test("inside a session's sandbox a bind below 1024 fails with EACCES, and a bind on a free high port succeeds", { skip: sandboxSkip, timeout: 30000 }, async (t) => {
  const floor = Number(read("/proc/sys/net/ipv4/ip_unprivileged_port_start").trim());
  assert.ok(floor > LOW_PORT, `this host's net.ipv4.ip_unprivileged_port_start is ${floor}, at or below the test port ${LOW_PORT}, so the test can't show the sandbox refuses it`);
  const root = tmp(t, "c2b-");
  const runDir = join(root, "run");
  const workspace = join(root, "ws");
  mkdirSync(runDir);
  mkdirSync(workspace);
  mkdirSync(join(runDir, "bundle"));
  writeFileSync(join(runDir, "session.json"), "{}");
  const adapter = join(root, "adapter.sh");
  const brokerSocket = join(root, "broker.sock");
  writeFileSync(adapter, "");
  writeFileSync(brokerSocket, "");
  prepareRun(runDir);
  const list = binds({ runDir, workspace, adapter, harnessMounts: [], brokerSocket });
  const high = await new Promise((done) => {
    const s = createServer().listen(0, "127.0.0.1", () => { const { port } = s.address(); s.close(() => done(port)); });
  });
  const bindIn = (port) => {
    const code = `require("net").createServer().on("error", (e) => { console.log(e.code); process.exit(0); }).listen(${port}, "127.0.0.1", function () { console.log("bound"); this.close(); });`;
    const argv = sandboxArgv(list, runDir, [process.execPath, "-e", code], { business: "acme", role: "coder" });
    const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", timeout: 10000 });
    assert.equal(r.status, 0, r.stderr);
    return r.stdout.trim();
  };
  assert.equal(bindIn(LOW_PORT), "EACCES");
  assert.equal(bindIn(high), "bound");
});
