#!/usr/bin/env bash
# selftest — prove this checkout works, without touching anything real.
#
# Everything runs against a THROWAWAY amux home and a throwaway bin directory, so
# it is safe to run on a machine that already has amux configured: it never reads,
# writes, or deletes your real ~/.amux, never contacts a real machine, and never
# starts or stops a session.
#
# What it proves:
#   1. every script parses (bash -n / py_compile)
#   2. every lib function a script calls is actually defined
#   3. install.sh installs, and the symlinked commands still find lib/
#   4. amux-config's whole lifecycle: add, show, test, rename, remove
#   5. security properties hold — secret-safe HTTP, validated names, exact tmux targets
#      and mode-600 config; remote exec/provider and numeric rename resolution work
#   6. the renderer produces the same output for local and remote given the same data
#   7. patches apply, are idempotent, and revert cleanly (on a COPY, never your server)
#   8. the audit catches a planted secret
#
# Usage:
#   tools/selftest              run everything
#   tools/selftest -v           show each command's output
#
# Exit codes: 0 = all passed · 1 = something failed
set -uo pipefail

cd "$(dirname "$0")/.." || exit 1
REPO=$(pwd)

case "${1:-}" in
  -h|--help) awk 'NR==1&&/^#!/{next} /^[[:space:]]*#/{sub(/^[[:space:]]*#[[:space:]]?/,"");print;next} {exit}' "$0"; exit 0 ;;
esac
VERBOSE=0; [ "${1:-}" = "-v" ] && VERBOSE=1

RED=''; GREEN=''; DIM=''; BOLD=''; RESET=''
if [ -t 1 ]; then
  RED=$'\033[31m'; GREEN=$'\033[32m'; DIM=$'\033[2m'; BOLD=$'\033[1m'; RESET=$'\033[0m'
fi

PASS=0; FAIL=0
ok()   { PASS=$((PASS+1)); printf '  %s✓%s %s\n' "$GREEN" "$RESET" "$1"; }
bad()  { FAIL=$((FAIL+1)); printf '  %s✗%s %s\n' "$RED" "$RESET" "$1"; [ -n "${2:-}" ] && printf '      %s\n' "$2"; }
sect() { printf '\n%s%s%s\n' "$BOLD" "$1" "$RESET"; }

# Isolated sandbox. Everything the test writes lives here and is removed at exit.
SANDBOX=$(mktemp -d "${TMPDIR:-/tmp}/amux-selftest.XXXXXX")
cleanup() { rm -rf "$SANDBOX"; }
trap cleanup EXIT
export CC_HOME="$SANDBOX/amux"
export BINDIR="$SANDBOX/bin"
mkdir -p "$CC_HOME" "$BINDIR"

run() { if [ "$VERBOSE" = 1 ]; then "$@"; else "$@" >/dev/null 2>&1; fi; }

# Capture a command's output, then match against the captured STRING.
#
# Never `cmd | grep -q pat` in this file: grep -q exits at the first match, which
# SIGPIPEs the producer, and with `set -o pipefail` the pipeline then reports
# failure even though the match SUCCEEDED. That turns a passing check into a
# flaky failure whose flakiness depends on how much output the producer had
# already written — which is exactly how this suite first "failed" six real,
# working behaviors.
out_of()  { "$@" 2>&1; }
has()     { case "$2" in *"$1"*) return 0 ;; *) return 1 ;; esac; }
matches() { printf '%s' "$2" | grep -qE "$1"; }

# ── 1. syntax ────────────────────────────────────────────────────────────────
sect "1. Everything parses"
n=0; bad_files=""
for f in bin/* setup/* tools/audit-clean tools/selftest install.sh; do
  [ -f "$f" ] || continue
  head -1 "$f" | grep -q 'python' && continue
  if bash -n "$f" 2>/dev/null; then n=$((n+1)); else bad_files="$bad_files $f"; fi
done
[ -z "$bad_files" ] && ok "$n shell scripts parse" || bad "shell syntax errors:$bad_files"

pyok=1
for f in lib/*.py patches/apply; do
  [ -f "$f" ] || continue
  python3 -c "import ast,sys; ast.parse(open(sys.argv[1],encoding='utf-8').read())" "$f" 2>/dev/null \
    || { bad "python syntax error in $f"; pyok=0; }
done
[ "$pyok" = 1 ] && ok "python files parse"

# ── 2. no undefined lib functions ────────────────────────────────────────────
sect "2. Every lib function used is defined"
defined=$(grep -ohE '^amux_[a-z_]+\(\)' lib/amux-common.sh | tr -d '()' | sort -u)
missing=""
for u in $(grep -rhoE '\bamux_[a-z_]+\b' bin/ setup/ 2>/dev/null | sort -u); do
  printf '%s\n' "$defined" | grep -qx "$u" || missing="$missing $u"
done
[ -z "$missing" ] && ok "no undefined helpers" || bad "undefined lib functions:$missing"

# ── 3. install + symlink resolution ──────────────────────────────────────────
sect "3. install.sh works and symlinks resolve lib/"
if run ./install.sh; then
  ok "install.sh completed"
else
  bad "install.sh failed"
fi
count=$(ls "$BINDIR" 2>/dev/null | wc -l | tr -d ' ')
[ "$count" -ge 10 ] && ok "$count commands installed" || bad "only $count commands installed"

# The real risk with symlinks: a command that cannot find lib/ from its link.
export PATH="$BINDIR:$PATH"
if out=$(amux-config list 2>&1); then
  ok "symlinked command resolves lib/ (amux-config runs)"
else
  bad "symlinked command cannot find lib/" "$out"
fi

# Network-facing commands are mocked for the rest of the suite. The first pair
# simply fails fast for unreachable-path tests; section 5 replaces them with
# deterministic API/SSH fakes. No selftest operation contacts a real machine.
MOCK_HOME="$SANDBOX/mockhome"
MOCKBIN="$MOCK_HOME/.local/bin"
mkdir -p "$MOCKBIN"
cat > "$MOCKBIN/curl" <<'SH'
#!/usr/bin/env bash
printf '000'
exit 7
SH
cat > "$MOCKBIN/ssh" <<'SH'
#!/usr/bin/env bash
exit 255
SH
chmod +x "$MOCKBIN/curl" "$MOCKBIN/ssh"
export PATH="$MOCKBIN:$PATH"

# ── 4. amux-config lifecycle ─────────────────────────────────────────────────
sect "4. amux-config lifecycle"
FAKE_TOKEN="selftest-not-a-real-credential-0123456789"

printf '%s' "$FAKE_TOKEN" | run amux-config add testbox --host 192.0.2.77 --token-stdin
o=$(out_of amux-config list)
if has testbox "$o"; then
  ok "add + list"
else
  bad "add did not produce a listed box"
fi

envfile="$CC_HOME/remotes/testbox.env"
mode=$(ls -l "$envfile" 2>/dev/null | cut -c1-10)
[ "$mode" = "-rw-------" ] && ok "config file is mode 600" || bad "config file mode is $mode, expected -rw-------"

o=$(out_of amux-config show testbox)
if has 192.0.2.77 "$o"; then
  ok "show displays the URL"
else
  bad "show did not display the URL"
fi

# An unreachable box must read as UNREACHABLE, not as an unrecognized status.
o=$(out_of amux-config test testbox)
if matches "[Uu][Nn][Rr][Ee][Aa][Cc][Hh][Aa][Bb][Ll][Ee]" "$o"; then
  ok "unreachable box reported correctly"
else
  bad "unreachable box not reported as unreachable"
fi

run amux-config rename testbox renamedbox
o=$(out_of amux-config list)
if has renamedbox "$o"; then
  ok "rename"
else
  bad "rename failed"
fi

printf 'y\n' | run amux-config remove renamedbox
o=$(out_of amux-config list)
if ! has renamedbox "$o"; then
  ok "remove"
else
  bad "remove failed"
fi

# ── 4b. amux-up word order ───────────────────────────────────────────────────
# amux-up names the machine FIRST, mirroring `amux-remote <machine> <cmd>`. Both
# that and the older colon form must reach the remote lane, and a FLAG after the
# session name must never be mistaken for a machine name.
sect "4b. amux-up argument forms"
printf '%s' "$FAKE_TOKEN" | run amux-config add upbox --host 192.0.2.99 --token-stdin
# Bound each call: the remote lane ends in a real ssh to an unreachable
# documentation address, and we are only testing how arguments were parsed.
bounded() { perl -e 'alarm 6; exec @ARGV' "$@" 2>&1; }

o=$(bounded amux-up upbox somesession)
if has "on box 'upbox'" "$o"; then
  ok "amux-up <machine> <name> reaches the remote lane"
else
  bad "amux-up <machine> <name> did not reach the remote lane" "$o"
fi

o=$(bounded amux-up upbox:somesession)
if has "on box 'upbox'" "$o"; then
  ok "legacy amux-up <machine>:<name> still works"
else
  bad "legacy colon form broke" "$o"
fi

o=$(bounded amux-up notasession --continue)
if has "no box was named" "$o"; then
  ok "a flag after the name is not mistaken for a machine"
else
  bad "a flag was misread as a machine name" "$o"
fi
printf 'y\n' | run amux-config remove upbox

# ── 5. security properties ───────────────────────────────────────────────────
sect "5. Security properties"

# --token must be refused outright: argv is world-readable via ps.
o=$(out_of amux-config add x --host h --token "$FAKE_TOKEN")
if has "not supported on purpose" "$o"; then
  ok "--token is refused (keeps credentials off argv)"
else
  bad "--token was NOT refused"
fi

# No command may print a credential in full.
printf '%s' "$FAKE_TOKEN" | run amux-config add leaktest --host 192.0.2.78 --token-stdin
leaked=0
for cmd in "amux-config show leaktest" "amux-config list" "amux-config status"; do
  o=$($cmd 2>&1); if has "$FAKE_TOKEN" "$o"; then leaked=1; fi
done
[ "$leaked" = 0 ] && ok "no command prints a credential in full" || bad "a command printed the credential in full"

o=$(out_of amux-config show leaktest)
if matches "\\(41 chars\\)" "$o"; then
  ok "credential summarized as prefix + length"
else
  bad "credential summary missing or wrong"
fi
printf 'y\n' | run amux-config remove leaktest

# ── 5b. remote security + companion regressions ──────────────────────────────
sect "5b. Remote hardening and companion regressions"

# A deterministic curl stand-in records only non-secret argv, verifies that the
# authorization header arrived on stdin, and returns small API-shaped responses.
# If a token ever moves back onto argv, it records a marker instead of the value.
CURL_LOG="$SANDBOX/curl.argv"
SSH_LOG="$SANDBOX/ssh.argv"
TMUX_LOG="$SANDBOX/tmux.argv"
export AMUX_TEST_CURL_LOG="$CURL_LOG"
export AMUX_TEST_SSH_LOG="$SSH_LOG"
export AMUX_TEST_TMUX_LOG="$TMUX_LOG"
export AMUX_TEST_TOKEN="$FAKE_TOKEN"

cat > "$MOCKBIN/curl" <<'SH'
#!/usr/bin/env bash
set -u
method=GET
url=""
previous=""
header_stdin=0
for arg in "$@"; do
  case "$arg" in
    *"${AMUX_TEST_TOKEN:?}"*)
      printf 'TOKEN_ON_ARGV\n' >> "$AMUX_TEST_CURL_LOG"
      exit 97 ;;
  esac
  printf '%s\n' "$arg" >> "$AMUX_TEST_CURL_LOG"
  [ "$previous" = -X ] && method="$arg"
  [ "$arg" = @- ] && header_stdin=1
  case "$arg" in http://*|https://*) url="$arg" ;; esac
  previous="$arg"
done
if [ "$header_stdin" = 1 ]; then
  IFS= read -r header || true
  case "$header" in
    *"$AMUX_TEST_TOKEN"*) ;;
    *) printf 'HEADER_MISSING\n' >> "$AMUX_TEST_CURL_LOG"; exit 98 ;;
  esac
fi
case "$url" in
  https://api.anthropic.com/*) printf '200' ;;
  */api/sessions/*/peek*)      printf '{"output":"peek-ok"}' ;;
  */api/sessions/*/send*)      printf '{"ok":true}' ;;
  */api/sessions/*/info*)      printf '{"provider":"codex"}' ;;
  */api/sessions/*/config)     printf '{"ok":true,"message":"configuration updated"}' ;;
  */api/sessions/*/start)      printf '{"ok":true,"message":"started"}' ;;
  */api/sessions)
    if [ "$method" = POST ]; then printf '{"ok":true,"message":"created"}'
    else printf '[]'
    fi ;;
  *) printf '{}' ;;
esac
SH
cat > "$MOCKBIN/ssh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$@" > "${AMUX_TEST_SSH_LOG:?}"
exit 0
SH
cat > "$MOCKBIN/tmux" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$@" >> "${AMUX_TEST_TMUX_LOG:?}"
case "${1:-}" in has-session) exit 1 ;; esac
exit 0
SH
cat > "$MOCKBIN/amux" <<'SH'
#!/usr/bin/env bash
exit 0
SH
cat > "$MOCKBIN/claude" <<'SH'
#!/usr/bin/env bash
exit 0
SH
cat > "$MOCKBIN/security" <<'SH'
#!/usr/bin/env bash
exit 1
SH
cat > "$MOCKBIN/pgrep" <<'SH'
#!/usr/bin/env bash
printf '4242\n'
SH
cat > "$MOCKBIN/lsof" <<'SH'
#!/usr/bin/env bash
printf 'n%s\n' "${AMUX_TEST_CC_DIR:?}"
SH
cat > "$MOCKBIN/ps" <<'SH'
#!/usr/bin/env bash
printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "${AMUX_TEST_REAUTH_TOKEN:?}"
SH
chmod +x "$MOCKBIN"/*

printf '%s' "$FAKE_TOKEN" \
  | run amux-config add remotebox --host 192.0.2.79 --ssh-host example-host --token-stdin

: > "$CURL_LOG"
o=$(out_of amux-remote remotebox peek safe.name 7)
curl_args=$(out_of sed -n '1,200p' "$CURL_LOG")
if has "peek-ok" "$o" && has "/api/sessions/safe.name/peek?lines=7" "$curl_args" \
   && ! has "TOKEN_ON_ARGV" "$curl_args"; then
  ok "remote REST path is encoded and the token stays off curl argv"
else
  bad "remote peek security regression" "$curl_args"
fi

: > "$CURL_LOG"
o=$(out_of amux-remote remotebox peek 'bad/name' 7)
curl_args=$(out_of sed -n '1,20p' "$CURL_LOG")
if has "invalid session name" "$o" && [ -z "$curl_args" ]; then
  ok "invalid session paths are rejected before curl"
else
  bad "invalid REST session name reached curl"
fi

rm -f "$SSH_LOG"
o=$(out_of amux-remote remotebox attach "bad'name" --plain)
if has "invalid session name" "$o" && [ ! -e "$SSH_LOG" ]; then
  ok "attach rejects shell metacharacters before ssh"
else
  bad "attach name validation failed"
fi

run amux-remote remotebox attach safe.name --plain
ssh_args=$(out_of sed -n '1,20p' "$SSH_LOG")
if has "-t '=amux-safe.name'" "$ssh_args"; then
  ok "attach uses an exact tmux target"
else
  bad "attach did not use an exact tmux target" "$ssh_args"
fi

o=$(out_of amux-remote remotebox exec new-session --dir '~/project' --model model-test --provider codex)
curl_args=$(out_of sed -n '1,200p' "$CURL_LOG")
if has "created new-session" "$o" && has '"provider": "codex"' "$curl_args" \
   && has '"model": "model-test"' "$curl_args" \
   && has "/api/sessions/new-session/start" "$curl_args"; then
  ok "remote exec creates, configures provider/model, and starts"
else
  bad "remote exec/provider flow failed" "$curl_args"
fi

o=$(out_of amux-remote remotebox provider safe.name)
if has "codex" "$o"; then
  ok "remote provider query works"
else
  bad "remote provider query failed" "$o"
fi

# start must forward extra args as one-shot flags in the JSON body — including
# values with spaces — and print the attach next-step hint (start != attach).
: > "$CURL_LOG"
o=$(out_of amux-remote remotebox start safe.name --append-system-prompt 'say hi')
curl_args=$(out_of sed -n '1,200p' "$CURL_LOG")
if has "/api/sessions/safe.name/start" "$curl_args" \
   && has "--append-system-prompt 'say hi'" "$curl_args" \
   && has "attach: amux-up remotebox safe.name" "$o"; then
  ok "remote start forwards one-shot flags (spaces survive) and hints the attach"
else
  bad "remote start flag forwarding failed" "$curl_args"
fi

# Zero-flag start must stay byte-identical: no JSON body at all.
: > "$CURL_LOG"
o=$(out_of amux-remote remotebox start safe.name)
curl_args=$(out_of sed -n '1,200p' "$CURL_LOG")
if has "/api/sessions/safe.name/start" "$curl_args" && ! has '"flags"' "$curl_args"; then
  ok "zero-flag start sends no body"
else
  bad "zero-flag start grew a body" "$curl_args"
fi

# The hint is suppressed for wrappers that attach right after (amux-up).
o=$(out_of env AMUX_NO_HINT=1 amux-remote remotebox start safe.name)
if ! has "attach: amux-up" "$o"; then
  ok "AMUX_NO_HINT suppresses the attach hint"
else
  bad "attach hint printed despite AMUX_NO_HINT" "$o"
fi

# stop takes no flags — unexpected extras must die loudly BEFORE any request,
# not be silently ignored.
: > "$CURL_LOG"
o=$(out_of amux-remote remotebox stop safe.name extra-arg)
curl_args=$(out_of sed -n '1,20p' "$CURL_LOG")
if has "unexpected extra arguments" "$o" && [ -z "$curl_args" ]; then
  ok "stop rejects unexpected extra args before any request"
else
  bad "stop swallowed extra args" "$o"
fi

# amux-up two-word form: the machine interpretation wins, launch flags ride the
# start request (not the attach), and --plain routes to attach only.
rm -f "$SSH_LOG"; : > "$CURL_LOG"
o=$(out_of amux-up remotebox safe.name --continue --plain)
curl_args=$(out_of sed -n '1,200p' "$CURL_LOG")
ssh_args=$(out_of sed -n '1,20p' "$SSH_LOG")
if has '"flags": "--continue"' "$curl_args" \
   && has "-t '=amux-safe.name'" "$ssh_args" \
   && ! has "attach: amux-up" "$o" \
   && ! has "continue" "$ssh_args"; then
  ok "amux-up <machine> <name> starts with flags then attaches (flags routed)"
else
  bad "amux-up flag routing failed" "curl: $curl_args | ssh: $ssh_args"
fi

# Globs put 2fa first and alpha second. Query "2" must resolve row 2 (alpha),
# not the unique prefix 2fa.
mkdir -p "$CC_HOME/sessions"
: > "$CC_HOME/sessions/2fa.env"
: > "$CC_HOME/sessions/alpha.env"
: > "$CC_HOME/sessions/beta.env"
o=$(out_of amux-rename -n 2 renamed)
if has "alpha" "$o" && ! has "2fa" "$o"; then
  ok "numeric rename lookup wins over prefix matching"
else
  bad "numeric rename lookup selected the wrong session" "$o"
fi

# Exercise reauth without touching a real credential store or process table.
REAUTH_ENV="$SANDBOX/reauth.env"
AMUX_TEST_CC_DIR="$SANDBOX/project"
AMUX_TEST_REAUTH_TOKEN="sk-ant-oat$(printf '%s' 'selftest-only-value')"
export AMUX_TEST_CC_DIR AMUX_TEST_REAUTH_TOKEN
printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "$AMUX_TEST_REAUTH_TOKEN" > "$REAUTH_ENV"
printf 'CC_DIR="%s"\n' "$AMUX_TEST_CC_DIR" > "$CC_HOME/sessions/api.env"
: > "$TMUX_LOG"
o=$(AMUX_REAUTH_HOME="$MOCK_HOME" AMUX_REAUTH_ENVFILE="$REAUTH_ENV" AMUX_REAUTH_CLAUDE=claude \
  amux-reauth -d api 2>&1)
tmux_args=$(out_of sed -n '1,80p' "$TMUX_LOG")
if has "=amux-api" "$tmux_args" && has "durable token verified" "$o"; then
  ok "reauth waits on an exact tmux target"
else
  bad "reauth exact-target regression" "$tmux_args"
fi

printf 'y\n' | run amux-config remove remotebox

# ── 6. renderer parity ───────────────────────────────────────────────────────
sect "6. Local and remote views come from one renderer"
FIX='[{"name":"alpha","running":true,"active_model":"claude-opus-5","dir":"/home/u/a","archived":false},
     {"name":"beta","running":false,"active_model":"claude-sonnet-5","dir":"/home/u/b","archived":false}]'
loc=$(printf '%s' "$FIX" | AMUX_LS_LABEL=host AMUX_LS_KIND=local  AMUX_LS_COMPACT=1 AMUX_LS_BARE=1 python3 lib/amux-render.py 2>&1)
rem=$(printf '%s' "$FIX" | AMUX_LS_LABEL=host AMUX_LS_KIND=remote AMUX_LS_COMPACT=1 AMUX_LS_BARE=1 python3 lib/amux-render.py 2>&1)
if [ "$loc" = "$rem" ] && [ -n "$loc" ]; then
  ok "identical data renders identically local vs remote"
else
  bad "local and remote renders differ" "local=[$loc] remote=[$rem]"
fi

o=$(printf '%s' "$FIX" | AMUX_LS_BARE=1 AMUX_LS_COMPACT=1 AMUX_LS_ASCII=1 python3 lib/amux-render.py 2>&1)
if has "*" "$o"; then
  ok "ASCII mode renders ASCII marks (Windows consoles)"
else
  bad "ASCII mode did not produce ASCII marks"
fi

# Archived sessions hidden by default, shown on request.
ARCH='[{"name":"old","running":false,"archived":true},{"name":"new","running":false,"archived":false}]'
vis=$(printf '%s' "$ARCH" | AMUX_LS_BARE=1 AMUX_LS_COMPACT=1 python3 lib/amux-render.py 2>/dev/null)
if has new "$vis" && ! has old "$vis"; then
  ok "archived sessions hidden by default"
else
  bad "archived filtering wrong"
fi

# ── 7. patches, on a copy ────────────────────────────────────────────────────
sect "7. Patches (against a copy, never a live server)"
SRV="$SANDBOX/amux-server.py"
# A minimal stand-in carrying both anchors, so this test needs no network and no
# 3 MB download. Exercises the anchor matching, not upstream itself.
cat > "$SRV" <<'FAKE'
import re, shlex
def _at_shell_prompt(s): return False
def _detect_session_status(n, r): return ""
def _find_latest_session_id(w): return ""
def list_sessions():
    for f in []:
        name = f
        running = tmux_name(name) in tmux_info
        raw = ""
        if raw:
            strip_ansi = lambda t: t
            lines = [l for l in raw.splitlines() if l.strip()]
            preview = strip_ansi(lines[-1][:120]) if lines else ""
            if running:
                status = _detect_session_status(name, raw)
def start_session(name, extra_flags="", _skip_conv_id=False):
    with open("x") as _f:
        if not _skip_conv_id and provider == "claude":
            if cc_session_name:
                session_flag = "a"
            else:
                # First-ever start
                session_flag = f'--name {shlex.quote(name)}'
FAKE
before=$(md5 -q "$SRV" 2>/dev/null || md5sum "$SRV" | cut -d' ' -f1)

if ./patches/apply --check --server "$SRV" >/dev/null 2>&1; then
  bad "--check reported patched on an unpatched file"
else
  ok "--check detects an unpatched file"
fi

if run ./patches/apply --server "$SRV"; then ok "patches applied"; else bad "patches failed to apply"; fi
python3 -c "import ast,sys; ast.parse(open(sys.argv[1],encoding='utf-8').read())" "$SRV" 2>/dev/null \
  && ok "patched file still compiles" || bad "patched file does NOT compile"
./patches/apply --check --server "$SRV" >/dev/null 2>&1 && ok "--check now reports patched" || bad "--check still reports unpatched"

out=$(./patches/apply --server "$SRV" 2>&1)
has "already applied" "$out" && ok "re-applying is a no-op (idempotent)" || bad "not idempotent"

run ./patches/apply --revert --server "$SRV"
after=$(md5 -q "$SRV" 2>/dev/null || md5sum "$SRV" | cut -d' ' -f1)
[ "$before" = "$after" ] && ok "--revert restores the original byte-for-byte" || bad "--revert did not restore the original"

# Refuses on drift rather than corrupting.
printf 'print("nothing to anchor on")\n' > "$SANDBOX/drifted.py"
o=$(./patches/apply --server "$SANDBOX/drifted.py" 2>&1)
if has "ANCHOR NOT FOUND" "$o"; then
  ok "refuses cleanly when upstream has drifted"
else
  bad "did not refuse on a drifted file"
fi

# ── 8. the audit actually catches things ─────────────────────────────────────
sect "8. Audit catches planted secrets"
planted="$REPO/.selftest-planted.md"
# Build the bait at RUNTIME from parts, so these literals never appear in this
# file. Writing them out plainly would make the audit flag its own test suite —
# and the tempting fix, exempting tools/selftest by path, would carve a permanent
# blind spot into the check. A test for a scanner must not weaken the scanner.
_o1=100; _o2=$((64 + 44)); _o3=219; _o4=90
printf 'host %s.%s.%s.%s\n' "$_o1" "$_o2" "$_o3" "$_o4" > "$planted"
printf 'nui-%s-01\n' "bl${_o4:0:0}ade" >> "$planted"
o=$(./tools/audit-clean 2>&1)
if has "finding" "$o"; then
  ok "audit catches a planted address and machine name"
else
  bad "audit MISSED a planted secret"
fi
rm -f "$planted"
if ./tools/audit-clean >/dev/null 2>&1; then
  ok "audit passes on the clean tree"
else
  bad "audit fails on the clean tree"
fi

# ── verdict ──────────────────────────────────────────────────────────────────
printf '\n%s%d passed, %d failed%s\n' "$BOLD" "$PASS" "$FAIL" "$RESET"
printf '%s(sandbox %s removed; your real ~/.amux was never touched)%s\n' "$DIM" "$SANDBOX" "$RESET"
[ "$FAIL" = 0 ] || exit 1
