#!/usr/bin/env bash # Red-first acceptance checks for #1050. This file is committed before the # installer implementation. Do not weaken these properties to make it green. # pass_case always returns zero and fail_case records the aggregate failure; # the compact A&&pass||fail assertions are intentional. # shellcheck disable=SC2015 set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-install-state-test.XXXXXX")" trap 'rm -rf "$TMP"' EXIT failures=0 COMPAT_BIN="$TMP/compat-bin" mkdir -p "$COMPAT_BIN" cat > "$COMPAT_BIN/realpath" <<'REALPATH' #!/usr/bin/env python3 import os import sys args = sys.argv[1:] mode = args.pop(0) if args and args[0] in ("-e", "-m") else "-m" if args and args[0] == "--": args.pop(0) if len(args) != 1 or (mode == "-e" and not os.path.exists(args[0])): raise SystemExit(1) print(os.path.realpath(args[0])) REALPATH chmod 0755 "$COMPAT_BIN/realpath" fail_case() { printf '[test] FAIL: %s\n' "$*" >&2; failures=$((failures + 1)); } pass_case() { printf '[test] PASS: %s\n' "$*"; } fingerprint() { local dir="$1" if [[ ! -d "$dir" ]]; then printf 'ABSENT\n'; return; fi python3 - "$dir" <<'PY' import hashlib import os import stat import sys root = os.path.abspath(sys.argv[1]) rows = [] for current, dirs, files in os.walk(root, topdown=True, followlinks=False): for name in dirs + files: path = os.path.join(current, name) rel = os.path.relpath(path, root) meta = os.lstat(path) target = os.readlink(path) if stat.S_ISLNK(meta.st_mode) else "" digest = "" if stat.S_ISREG(meta.st_mode): with open(path, "rb") as handle: digest = hashlib.sha256(handle.read()).hexdigest() rows.append((rel, stat.S_IFMT(meta.st_mode), stat.S_IMODE(meta.st_mode), meta.st_uid, meta.st_gid, target, digest)) payload = "\n".join("|".join(map(str, row)) for row in sorted(rows)).encode() print(hashlib.sha256(payload).hexdigest()) PY } make_fake_npm() { local bin="$1" mkdir -p "$bin" cat > "$bin/npm" <<'FAKE' #!/bin/bash set -euo pipefail if [[ "${1:-}" == "--version" ]]; then echo '10.6.2'; exit 0; fi case "${1:-} ${2:-} ${3:-}" in 'view @mosaicstack/mosaic@next version') echo '0.0.50-next.999' ;; 'view @mosaicstack/gateway@next version') echo '0.0.7-next.999' ;; 'view @mosaicstack/mosaic version') echo '0.0.49' ;; 'ls -g --depth=0'|'ls -g --json') echo '{"dependencies":{"@mosaicstack/mosaic":{"version":"0.0.50-next.999"},"@mosaicstack/gateway":{"version":"0.0.7-next.999"}}}' ;; ls*) echo '{"dependencies":{"@mosaicstack/mosaic":{"version":"0.0.50-next.999"},"@mosaicstack/gateway":{"version":"0.0.7-next.999"}}}' ;; *) echo "unexpected fake npm command: $*" >&2; exit 1 ;; esac FAKE chmod 0755 "$bin/npm" } printf '[test] case: --check enumerates exactly P0-P8, discriminates, and mutates nothing\n' check_home="$TMP/check-home" check_bin="$TMP/check-bin" mkdir -p "$check_home/.config/mosaic/skills/alpha" "$check_home/.npm-global/bin" "$check_bin" printf '# framework\n' > "$check_home/.config/mosaic/AGENTS.md" printf '# skill\n' > "$check_home/.config/mosaic/skills/alpha/SKILL.md" cat > "$check_home/.npm-global/bin/mosaic" <<'CLI' #!/usr/bin/env bash printf '0.0.50-next.999\n' CLI chmod 0755 "$check_home/.npm-global/bin/mosaic" make_fake_npm "$check_bin" before="$(fingerprint "$check_home")" set +e HOME="$check_home" MOSAIC_HOME="$check_home/.config/mosaic" MOSAIC_PREFIX="$check_home/.npm-global" \ MOSAIC_NO_COLOR=1 PATH="$check_bin:/usr/local/bin:/usr/bin:/bin" \ bash "$ROOT/tools/install.sh" --check --next >"$TMP/check.log" 2>&1 check_status=$? set -e after="$(fingerprint "$check_home")" [[ "$before" == "$after" ]] && pass_case '--check left the complete HOME fingerprint unchanged' \ || fail_case "--check mutated HOME (before=$before after=$after)" [[ "$check_status" -ne 0 ]] && pass_case '--check exited non-zero for failed P4/P5/P8 predicates' \ || fail_case '--check returned zero on the deliberately broken host' phase_rows=0 for phase in P0 P1 P2 P3 P4 P5 P6 P7 P8; do count="$(grep -Ec "^\[$phase\] (PASS|FAIL):" "$TMP/check.log" || true)" [[ "$count" -eq 1 ]] || fail_case "$phase expected exactly one PASS/FAIL row, got $count" phase_rows=$((phase_rows + count)) done [[ "$phase_rows" -eq 9 ]] && pass_case '--check emitted exactly nine P0-P8 result rows' \ || fail_case "--check emitted $phase_rows canonical rows instead of 9" grep -q '^\[P3\] PASS:.*0\.0\.50-next\.999' "$TMP/check.log" \ && pass_case 'P3 preserves the absolute-path exact-version discriminator' \ || fail_case 'P3 did not PASS with the exact resolved next-lane version' grep -q '^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:' "$TMP/check.log" \ && pass_case 'P4 refuses fabricated precision when no shipped-set declaration exists' \ || fail_case 'P4 did not report the declared-set population as NOT-MEASURED / UNDECLARED' for phase in P5 P8; do grep -q "^\[$phase\] FAIL:" "$TMP/check.log" \ && pass_case "$phase remains an attributable expected RED" \ || fail_case "$phase did not report its own expected failure" done printf '[test] case: --check discriminates a constructed good host without mutation\n' good_home="$TMP/good-home" good_bin="$TMP/good-bin" good_prefix="$good_home/.npm-global" good_mosaic="$good_home/.config/mosaic" mkdir -p "$good_bin" "$good_prefix/bin" "$good_mosaic/skills/declared-skill" make_fake_npm "$good_bin" cp "$COMPAT_BIN/realpath" "$good_bin/realpath" cat > "$good_bin/id" <<'ID' #!/bin/bash case "${1:-}" in -u) echo 1001 ;; -g) echo 1001 ;; -un) echo fixture-user ;; *) exec /bin/id "$@" ;; esac ID cat > "$good_bin/stat" <<'STAT' #!/bin/bash if [[ "${1:-} ${2:-}" == '-c %u' ]]; then echo 1001; exit 0; fi exec /bin/stat "$@" STAT cat > "$good_bin/curl" <<'CURL' #!/bin/bash exit 0 CURL cat > "$good_bin/ldd" <<'LDD' #!/bin/bash echo 'ldd (GNU libc) 2.36' LDD chmod 0755 "$good_bin/id" "$good_bin/stat" "$good_bin/curl" "$good_bin/ldd" cat > "$good_prefix/bin/mosaic" <<'CLI' #!/usr/bin/env bash printf '0.0.50-next.999\n' CLI chmod 0755 "$good_prefix/bin/mosaic" cat > "$good_bin/getent" < "$good_bin/bash" < "$good_mosaic/SOUL.md" printf '# User\n\nConfigured.\n' > "$good_mosaic/USER.md" chmod 0600 "$good_mosaic/SOUL.md" "$good_mosaic/USER.md" cat > "$good_mosaic/skills/declared-skill/SKILL.md" <<'SKILL' --- name: declared-skill description: Constructed loadable acceptance skill. --- # Declared skill SKILL printf '{"lane":"next","version":"0.0.50-next.999","skills":["declared-skill"]}\n' > "$good_mosaic/.install-shipped-skills.json" printf '{\n "lane": "next",\n "cliVersion": "0.0.50-next.999"\n}\n' > "$good_mosaic/.install-manifest.json" before="$(fingerprint "$good_home")" set +e HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" \ MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \ bash "$ROOT/tools/install.sh" --check --next >"$TMP/good-check.log" 2>&1 status=$? set -e after="$(fingerprint "$good_home")" [[ "$status" -eq 0 ]] && pass_case 'good-host --check exited zero' || fail_case "good-host --check exited $status" [[ "$before" == "$after" ]] && pass_case 'good-host --check left HOME unchanged' || fail_case 'good-host --check mutated HOME' good_rows="$(grep -Ec '^\[P[0-8]\] PASS:' "$TMP/good-check.log" || true)" [[ "$good_rows" -eq 9 ]] && pass_case 'good-host --check emitted nine PASS rows' \ || { cat "$TMP/good-check.log" >&2; fail_case "good-host --check emitted $good_rows PASS rows"; } printf '[test] case: persisted required-action failures remain blocking\n' for blocked_phase in P4 P6; do node -e ' const fs=require("fs"); const p=process.argv[1]; const phase=process.argv[2]; const m=JSON.parse(fs.readFileSync(p,"utf8")); m.phaseOutcomes={P4:"committed",P6:"committed"}; m.phaseOutcomes[phase]="failed"; fs.writeFileSync(p,JSON.stringify(m)+"\n"); ' "$good_mosaic/.install-manifest.json" "$blocked_phase" set +e HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" \ MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \ bash "$ROOT/tools/install.sh" --check --next >"$TMP/action-$blocked_phase.log" 2>&1 status=$? set -e [[ "$status" -ne 0 ]] || fail_case "$blocked_phase action failure returned zero" grep -q "^\[$blocked_phase\] FAIL:.*action reported a required $blocked_phase failure" "$TMP/action-$blocked_phase.log" \ && pass_case "$blocked_phase action failure remained blocking in a later --check" \ || fail_case "$blocked_phase persisted action failure was not attributed" done printf '{\n "lane": "next",\n "cliVersion": "0.0.50-next.999",\n "phaseOutcomes": {"P4":"committed","P6":"committed"}\n}\n' > "$good_mosaic/.install-manifest.json" printf '[test] case: per-phase P2-P8 fault injection restores representative host mutations\n' for phase in P2 P3 P4 P5 P6 P7 P8; do home="$TMP/fault-$phase/home" state="$TMP/fault-$phase/state" mkdir -p "$home/.config/mosaic" "$home/.npm-global/bin" "$home/.claude" "$state" printf 'operator-framework-sentinel\n' > "$home/.config/mosaic/operator.txt" printf '@scope:registry=https://pre.example.invalid/\n' > "$home/.npmrc" printf 'old-cli\n' > "$home/.npm-global/bin/mosaic" printf '{"hooks":{"safe":true}}\n' > "$home/.claude/settings.json" before="$(fingerprint "$home")" set +e HOME="$home" MOSAIC_HOME="$home/.config/mosaic" MOSAIC_PREFIX="$home/.npm-global" \ MOSAIC_INSTALL_STATE_DIR="$state" MOSAIC_INSTALL_FAULT_AFTER="$phase" \ MOSAIC_NO_COLOR=1 PATH="$COMPAT_BIN:$PATH" bash "$ROOT/tools/install.sh" --state-machine-self-test \ >"$TMP/fault-$phase.log" 2>&1 status=$? set -e after="$(fingerprint "$home")" [[ "$status" -ne 0 ]] || fail_case "$phase injected fault returned zero" grep -q "phase=$phase" "$TMP/fault-$phase.log" \ || fail_case "$phase fault transcript did not name the injected phase" [[ "$before" == "$after" ]] \ && pass_case "$phase rollback restored framework/npmrc/prefix/runtime representative state" \ || fail_case "$phase rollback mismatch (before=$before after=$after)" if find "$state" -type f -exec grep -l '"status"[[:space:]]*:[[:space:]]*"in-progress"' {} + 2>/dev/null | grep -q .; then fail_case "$phase left a journal in-progress" else pass_case "$phase left no journal falsely in-progress" fi done printf '[test] case: unsafe and overlapping rollback roots fail before mutation\n' unsafe_home="$TMP/unsafe-home" mkdir -p "$unsafe_home" for case_name in root-target home-target overlap-target; do case "$case_name" in root-target) unsafe_mosaic=/; unsafe_prefix="$unsafe_home/.npm-global" ;; home-target) unsafe_mosaic="$unsafe_home"; unsafe_prefix="$unsafe_home/.npm-global" ;; overlap-target) unsafe_mosaic="$unsafe_home/.config"; unsafe_prefix="$unsafe_home/.config/mosaic/prefix" ;; esac before="$(fingerprint "$unsafe_home")" set +e HOME="$unsafe_home" MOSAIC_HOME="$unsafe_mosaic" MOSAIC_PREFIX="$unsafe_prefix" \ MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \ bash "$ROOT/tools/install.sh" --check --next >"$TMP/$case_name.log" 2>&1 status=$? set -e after="$(fingerprint "$unsafe_home")" [[ "$status" -ne 0 ]] || fail_case "$case_name unsafe path returned zero" grep -q '^\[P0\] FAIL:.*unsafe context' "$TMP/$case_name.log" \ && pass_case "$case_name was rejected by P0" || fail_case "$case_name lacked an attributable P0 failure" [[ "$before" == "$after" ]] || fail_case "$case_name mutated HOME" done symlink_home="$TMP/symlink-home" symlink_outside="$TMP/symlink-outside" mkdir -p "$symlink_home" "$symlink_outside" ln -s "$symlink_outside" "$symlink_home/.config" set +e HOME="$symlink_home" MOSAIC_HOME="$symlink_home/.config/mosaic" MOSAIC_PREFIX="$symlink_home/.npm-global" \ MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \ bash "$ROOT/tools/install.sh" --check --next >"$TMP/symlink-target.log" 2>&1 status=$? set -e [[ "$status" -ne 0 ]] || fail_case 'symlink-parent unsafe path returned zero' grep -q '^\[P0\] FAIL:.*unsafe context' "$TMP/symlink-target.log" \ && pass_case 'symlinked rollback parent was rejected by P0' \ || fail_case 'symlinked rollback parent lacked an attributable P0 failure' [[ -z "$(find "$symlink_outside" -mindepth 1 -print -quit)" ]] || fail_case 'symlink target was mutated' printf '[test] case: stale in-progress projection does not impersonate a live OS lock\n' stale_home="$TMP/stale/home" stale_state="$TMP/stale/state" mkdir -p "$stale_home/.config/mosaic" "$stale_state" printf '{"status":"in-progress","journal":"%s"}\n' "$stale_state/dead-run/journal.ndjson" > "$stale_state/active.json" set +e HOME="$stale_home" MOSAIC_HOME="$stale_home/.config/mosaic" MOSAIC_PREFIX="$stale_home/.npm-global" \ MOSAIC_INSTALL_STATE_DIR="$stale_state" MOSAIC_INSTALL_FAULT_AFTER=P2 MOSAIC_NO_COLOR=1 \ PATH="$COMPAT_BIN:$PATH" bash "$ROOT/tools/install.sh" --state-machine-self-test >"$TMP/stale.log" 2>&1 status=$? set -e [[ "$status" -eq 97 ]] || fail_case "stale projection recovery expected injected status 97, got $status" if find "$stale_state" -name prior-active.json -type f -print -quit | grep -q .; then pass_case 'stale projection was preserved and superseded after the free OS lock was acquired' else fail_case 'stale projection was not preserved for recovery evidence' fi [[ "$(node -p "require('$stale_state/active.json').status")" == "rolled-back" ]] \ || fail_case 'stale retry did not reach an honest rolled-back terminal state' printf '[test] case: journal initialization failure is fatal before mutation\n' journal_home="$TMP/journal-failure/home" mkdir -p "$journal_home/.config/mosaic" printf 'journal-sentinel\n' > "$journal_home/.config/mosaic/operator.txt" before="$(fingerprint "$journal_home")" set +e HOME="$journal_home" MOSAIC_HOME="$journal_home/.config/mosaic" MOSAIC_PREFIX="$journal_home/.npm-global" \ MOSAIC_INSTALL_STATE_DIR="/proc/mosaic-journal-denied-$$" MOSAIC_INSTALL_FAULT_AFTER=P2 \ MOSAIC_NO_COLOR=1 bash "$ROOT/tools/install.sh" --state-machine-self-test \ >"$TMP/journal-failure.log" 2>&1 status=$? set -e after="$(fingerprint "$journal_home")" [[ "$status" -ne 0 ]] && pass_case 'unwritable journal directory failed non-zero' \ || fail_case 'unwritable journal directory returned zero' grep -q 'cannot create private journal directory' "$TMP/journal-failure.log" \ && pass_case 'journal initialization failure was named' \ || fail_case 'journal initialization failure lacked a named diagnostic' [[ "$before" == "$after" ]] && pass_case 'journal failure occurred before target mutation' \ || fail_case "journal failure mutated target HOME (before=$before after=$after)" if [[ "$failures" -ne 0 ]]; then printf '[test] install state-machine acceptance RED: %d failed assertion(s)\n' "$failures" >&2 printf '[test] --check transcript: %s\n' "$TMP/check.log" >&2 exit 1 fi printf '[test] installer state-machine acceptance passed\n'