#!/usr/bin/env bash
# Tests for lib/pids-cap-guard.mjs and hooks/pids-cap-guard.mjs.
#
# Nothing here writes a real cgroup file: every command is evaluated as a string.
#
# Run: bash pids-cap-guard.test.sh (exit 0 = all pass).
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
GUARD="$ROOT/lib/pids-cap-guard.mjs"
HOOK="$ROOT/hooks/pids-cap-guard.mjs"
PASS=0; FAIL=0
ok()  { PASS=$((PASS+1)); printf 'PASS %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf 'FAIL %s\n     %s\n' "$1" "$2"; }

UID_NUM="$(id -u)"
SCOPE="/sys/fs/cgroup/user.slice/user-${UID_NUM}.slice/user@${UID_NUM}.service/agent.slice/confine-agent-1-2.scope"

denies() { # $1=label $2=command  → expects exit 1
  local out rc
  out=$(node "$GUARD" "$2" 2>&1); rc=$?
  if (( rc == 1 )) && [[ "$out" == *Blocked* ]]; then ok "$1"; else bad "$1" "rc=$rc out=${out:0:200}"; fi
}
allows() { # $1=label $2=command  → expects exit 0
  local out rc
  out=$(node "$GUARD" "$2" 2>&1); rc=$?
  if (( rc == 0 )); then ok "$1"; else bad "$1" "rc=$rc out=${out:0:200}"; fi
}

# The exact escalation from the 2026-08-07 incident.
denies "echo into pids.max"            "echo 12288 > $SCOPE/pids.max"
denies "append into pids.max"          "echo 13500 >> $SCOPE/pids.max"
denies "tee into pids.max"             "echo 14200 | tee $SCOPE/pids.max"
denies "tee -a into pids.max"          "echo 14200 | tee -a $SCOPE/pids.max"
denies "dd into pids.max"              "dd of=$SCOPE/pids.max <<< 14200"
denies "cp over pids.max"              "cp /tmp/n $SCOPE/pids.max"
denies "truncate pids.max"             "truncate -s 0 $SCOPE/pids.max"
denies "systemctl set-property"        "systemctl --user set-property agent.slice TasksMax=16384"
denies "systemd-run -p TasksMax"       "systemd-run --user --scope -p TasksMax=16384 -- bash x.sh"
denies "drop-in write"                 "printf '[Slice]\nTasksMax=16384\n' > ~/.config/systemd/user/agent.slice.d/99.conf"
denies "drop-in via tee"               "echo TasksMax=16384 | tee ~/.config/systemd/user/agent.slice.d/99.conf"
# Lowering is the same write shape; the gate does not try to read intent from a number.
denies "lowering the cap by hand"      "echo 512 > $SCOPE/pids.max"

# Forensics must stay available — this is what an incident actually needs.
allows "cat pids.max"                  "cat $SCOPE/pids.max"
allows "grep pids.max"                 "grep -H . /sys/fs/cgroup/**/pids.max"
allows "systemctl show TasksMax"       "systemctl --user show agent.slice -p TasksMax --value"
allows "read pids.current"             "cat $SCOPE/pids.current"
allows "unrelated command"             "ls -l /tmp"
# The launchers and installers own the ceilings.
allows "confine.sh sets its own cap"   "AGENT_TASKS_MAX=1024 ~/.claude/lib/confine.sh agent bash -c true"
allows "monitor install.sh"            "deck-sudo bash modules/monitor/install.sh"
# The killer, not the cap, is the fix.
allows "stopping a runaway scope"      "systemctl --user stop confine-agent-1-2.scope"

# The hook denies with a PreToolUse verdict, and has no escape hatch.
hook_verdict() { printf '{"tool_input":{"command":%s}}' "$1" | node "$HOOK" 2>&1; }
out=$(hook_verdict "\"echo 16384 > $SCOPE/pids.max\"")
if [[ "$out" == *'"permissionDecision":"deny"'* ]]; then ok "hook denies a cap write"; else bad "hook denies a cap write" "$out"; fi
out=$(hook_verdict "\"echo 16384 > $SCOPE/pids.max  # raw-ok\"")
if [[ "$out" == *'"permissionDecision":"deny"'* ]]; then ok "hook ignores a # raw-ok escape"; else bad "hook ignores a # raw-ok escape" "$out"; fi
out=$(hook_verdict "\"cat $SCOPE/pids.max\"")
if [[ -z "$out" ]]; then ok "hook stays silent on a read"; else bad "hook stays silent on a read" "$out"; fi

printf 'pids-cap-guard: pass=%d fail=%d\n' "$PASS" "$FAIL"
(( FAIL == 0 ))
