339 lines
15 KiB
Bash
Executable File
339 lines
15 KiB
Bash
Executable File
#!/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" <<GETENT
|
|
#!/bin/bash
|
|
printf '%s:x:%s:%s::%s:%s\\n' '$(id -un)' '$(id -u)' '$(id -g)' '$good_home' '$good_bin/bash'
|
|
GETENT
|
|
cat > "$good_bin/bash" <<SHELL
|
|
#!/bin/bash
|
|
if [[ "\${*: -1}" == 'command -v mosaic' ]]; then
|
|
printf '%s\\n' '$good_prefix/bin/mosaic'
|
|
exit 0
|
|
fi
|
|
exec /bin/bash "\$@"
|
|
SHELL
|
|
chmod 0755 "$good_bin/getent" "$good_bin/bash"
|
|
printf '# Soul\n\nConfigured.\n' > "$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'
|