#!/usr/bin/env bash
# Self-test for reap-census.sh's classification predicates.
#
# WHY THIS EXISTS, stated plainly because it is the lesson and not the feature: the bash half
# of the census/reap shipped to gate round 1 with three defects — `pgrep -E` (not a procps
# option, exit 2), comm's 15-char truncation silently excluding three family names, and
# `pgrep -c || echo 0` capturing "0\n0" — and every one of them was invisible without a Linux
# box, because a matcher that errors into empty output produces family_total=0, which reads
# EXACTLY like a clean box. Absence needs a sibling probe.
#
# The classification logic is pure string work, so it can be asserted ANYWHERE — including on
# the Windows dev box where the rest of this wave was written. That is the whole point: a rig
# that only runs on the machine which already has the problem is not a rig. The runtime half
# (the /proc walk, the kill pass) still needs a real Linux run; this covers the half that was
# actually wrong.
#
# Run: bash .github/ci/reap-census-selftest.sh
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Fixed roots so in_scope assertions are deterministic regardless of the caller's environment.
export GITHUB_WORKSPACE=/ci/workspace
export CARGO_TARGET_DIR=/ci/cargo-target
export RUNNER_TEMP=/ci/runner-temp
export HOME=/ci/home
export SPT_CI_REAP_LIB=1
# shellcheck source=./reap-census.sh
. "$HERE/reap-census.sh"

fails=0
ok() { printf '  ok   %s\n' "$1"; }
bad() { printf '  FAIL %s\n' "$1"; fails=$((fails + 1)); }

assert_true()  { if "$@"; then ok "$*"; else bad "$*"; fi; }
assert_false() { if "$@"; then bad "NOT $*"; else ok "NOT $*"; fi; }

echo "== membership is decided on the exe basename, not on comm =="
# D2's regression: these three exceed the kernel's 15-char comm limit (23, 17 and 16 chars),
# so a comm-keyed matcher could NEVER see them. The basename test has no such ceiling.
for n in translate_proof_fixture post_step_fixture dispatch_fixture spt owl notify-shell \
         mock-session mock-shell gh_fixture git_fixture; do
  assert_true is_family "$n"
done
# Whole-word only: the family set is space-delimited precisely so a substring cannot sneak in.
assert_false is_family "spt-daemon"
assert_false is_family "sp"
assert_false is_family "notify"
assert_false is_family ""

echo "== the deleted-exe suffix comes off before membership is tested =="
# D4's regression, and the one this rig should have caught before it ever reached a box. A
# rebuild unlinks a binary out from under a running daemon; the kernel reports the link as
# "<path> (deleted)". The first version stripped that ONLY on a fallback path, on the false
# premise that `readlink -f` fails for a deleted exe — it succeeds, and returns the suffix,
# so the strip never ran and the basename came out as "mock-session (deleted)", matching no
# family name. The process was absent from every census and survived the reap.
assert_eq() {
  local want="$1" got="$2" what="$3"
  if [ "$want" = "$got" ]; then ok "$what -> [$got]"; else bad "$what -> [$got], wanted [$want]"; fi
}
assert_eq "mock-session" "$(exe_family_name '/tmp/reaptest/target/debug/mock-session (deleted)')" \
  "exe_family_name strips the deleted suffix"
assert_eq "translate_proof_fixture" "$(exe_family_name '/ci/workspace/target/debug/translate_proof_fixture (deleted)')" \
  "exe_family_name strips it for the 23-char D2 name too"
assert_eq "spt" "$(exe_family_name '/ci/workspace/target/debug/spt')" \
  "exe_family_name is a no-op on a live path"
assert_eq "/ci/workspace/target/debug/spt" "$(strip_deleted '/ci/workspace/target/debug/spt (deleted)')" \
  "strip_deleted leaves a scope-testable path"
# The end-to-end consequence: a deleted binary under a scope root is still family AND still
# in scope, so it is still reaped. That is the whole point of the handling.
assert_true is_family "$(exe_family_name '/ci/workspace/target/debug/spt (deleted)')"
assert_true in_scope "$(strip_deleted '/ci/workspace/target/debug/spt (deleted)')"
# A path that merely CONTAINS the word is untouched — the strip is a suffix operation.
assert_eq "spt" "$(exe_family_name '/ci/workspace/target/deleted/spt')" \
  "exe_family_name does not maul a path containing 'deleted'"

echo "== comm fallback exists for VISIBILITY only, and is truncation-aware =="
# A process whose exe link is unreadable still has to appear in the census, or population B
# goes uncounted. comm is what is left, and the kernel has already truncated it to 15.
assert_true comm_is_family "translate_proof"   # translate_proof_fixture, cut at 15
assert_true comm_is_family "post_step_fixtu"   # post_step_fixture, cut at 15
assert_true comm_is_family "dispatch_fixtur"   # dispatch_fixture, cut at 15
assert_true comm_is_family "spt"
assert_false comm_is_family "translate_proof_fixture"  # untruncated never appears in comm
assert_false comm_is_family "bash"
assert_false comm_is_family ""

echo "== hard exclusions outrank the scope roots =="
# Checked AFTER the root test rather than instead of it, so live infra survives a bug in the
# root computation. Both of these sit UNDER a scope root and must still be refused.
assert_true is_excluded "/ci/workspace/target/debug/spt-core/bin/spt"
assert_true is_excluded "/ci/workspace/target/debug/owl"
assert_true is_excluded "/home/x/.local/spt-core/bin/spt"
# D5: the LINUX fleet layout. kitsubito's live binary is ~/.local/bin/spt, which the
# Windows-shaped spt-core/bin pattern never matched — a backstop that cannot match the thing
# it protects is decoration.
assert_true is_excluded "/home/reavus/.local/bin/spt"
assert_true is_excluded "/ci/workspace/.local/bin/spt"
assert_false is_excluded "/ci/workspace/target/debug/spt"

echo "== scope: only this run's own build roots are eligible =="
assert_true in_scope "/ci/workspace/target/debug/spt"
assert_true in_scope "/ci/cargo-target/debug/spt"
assert_true in_scope "/ci/workspace/.adapter-notify/target/debug/notify-shell"
assert_true in_scope "/ci/runner-temp/staged/spt"
assert_true in_scope "/ci/home/spt-n1-oldbroker/target/debug/spt"
# The live fleet's installed binary, an unrelated checkout, and an unreadable path.
assert_false in_scope "/home/decid/.local/spt-core/bin/spt"
assert_false in_scope "/some/other/checkout/target/debug/spt"
assert_false in_scope ""
# A prefix that merely SHARES A NAME PREFIX with a root is not under it.
assert_false in_scope "/ci/workspace-other/target/debug/spt"

# [unit->REQ-CI-CENSUS-TEST-ATTRIBUTION]
echo "== orphanable descendants retain per-test attribution =="
env NEXTEST_TEST_NAME='spt::activity_frames' sleep 5 &
attributed_pid=$!
# The async child must exec `env`/`sleep` before /proc exposes the new environment.
for attempt in $(seq 1 100); do
  [ "$(nextest_test_of "$attributed_pid")" = 'spt::activity_frames' ] && break
  sleep 0.01
done
assert_eq "spt::activity_frames" "$(nextest_test_of "$attributed_pid")" \
  "nextest identity is read from the descendant rather than inferred from timing"
kill "$attributed_pid" 2>/dev/null || true
wait "$attributed_pid" 2>/dev/null || true

echo "== unreadable family paths refuse a clean survivor verdict =="
assert_eq "0" "$(survivor_verdict 0 0)" \
  "a fully readable empty census may report zero survivors"
assert_eq "UNPROVEN" "$(survivor_verdict 0 1)" \
  "an unreadable family path prevents zero from masquerading as proof"
assert_eq "UNPROVEN" "$(survivor_verdict 2 1)" \
  "unreadability outranks even a nonzero observed count"

echo "== flood count is a single clean token =="
# D3's regression: `pgrep -c … || echo 0` prints "0" and THEN exits 1, so the fallback fires
# after the print and the capture becomes "0\n0" — a newline inside the one line the census
# exists to make greppable.
floods=$(flood_count)
case "$floods" in
  '')      bad "flood_count returned empty" ;;
  *$'\n'*) bad "flood_count returned an embedded newline: [$floods]" ;;
  *)       ok "flood_count is a single token: [$floods]" ;;
esac

echo
if [ "$fails" -eq 0 ]; then
  echo "reap-census selftest: OK"
  exit 0
fi
echo "reap-census selftest: $fails FAILED"
exit 1
