#!/bin/sh
# tests/ci-kill-scoping.sh — a CI cleanup must never force-kill by the claude-spt NAME without
# scoping the kill to its own run, and must never tree-force a REMEMBERED pid without re-verifying
# that pid's identity at kill time. Both are the same hazard class doyle's fleet kill-census
# (2026-09-07) went looking for: a shared runner hosts every agent's live `claude-spt.exe`, so an
# unscoped `taskkill` in one test's cleanup wall-a's every peer's adapter.
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
fail=0
bad() { echo "FAIL $1"; fail=1; }
ok()  { echo "ok   $1"; }

# The banned shape: a process enumeration by the claude-spt NAME piped into a KILL, with no
# command-line scoping. The historical offender was `tasklist | grep claude-spt | ... taskkill`.
# Presence/count probes over the same `tasklist | grep` are fine — they run NO kill — so we test
# for the enumeration-feeding-a-kill shape with python, per file, not a blind grep.
# [unit->REQ-HAZARD-CI-KILL-SCOPING]
t_no_unscoped_name_kill() {
python - "$ROOT" <<'PY'
import sys, os, re, glob
root = sys.argv[1]
bad = 0
KILL = re.compile(r'taskkill\s+//(?:PID|F|T)|Stop-Process')
files = []
for d in ("ci", "tests"):
    files += glob.glob(os.path.join(root, d, "**", "*.sh"), recursive=True)
for f in files:
    lines = open(f, encoding="utf-8", errors="replace").read().split("\n")
    rel = os.path.relpath(f, root).replace("\\", "/")
    for i, ln in enumerate(lines, 1):
        if not KILL.search(ln):
            continue
        # a kill line (or a `for p in $(...enum...); do taskkill`): find the enumeration that feeds
        # it — the same line for a one-liner, else scan upward a few lines to the nearest `$(...`.
        window = ln
        if "$(" not in window:
            window = "\n".join(lines[max(0, i-4):i])
        # A NAME enumeration selects processes by image name — the dangerous, potentially fleet-wide
        # form. A query by processid=/PID-eq is inherently pid-scoped (an identity recheck) and is
        # NOT a name enumeration, so it is handled by the remembered-pid branch instead.
        name_query = (
            bool(re.search(r'tasklist', window) and "claude" in window.lower())
            or bool(re.search(r'wmic\s+process\s+where\s+"?name', window, re.I))
            or bool(re.search(r'Get-CimInstance', window) and re.search(r'Name\s*=', window, re.I))
        )
        remembered_pid = re.search(r'taskkill\s+//PID\s+"?\$\{?(\w+)', ln)
        if name_query:
            scoped = ("commandline like '%" in window) or ("CommandLine -match" in window) or ("commandline like \"%" in window)
            if not scoped:
                print("  %s:%d unscoped name-enumeration feeds a kill" % (rel, i)); bad += 1
        elif remembered_pid:
            var = remembered_pid.group(1)
            # the pid must be identity-rechecked on this or a preceding line of the same statement
            ctx = "\n".join(lines[max(0, i-3):i])
            rechecked = re.search(r'processid=\$%s"?\s+get\s+name' % re.escape(var), ctx) or ("get name" in ctx and "grep -qiE 'claude'" in ctx)
            if not rechecked:
                print("  %s:%d tree/force kill of remembered $%s with no kill-time identity recheck" % (rel, i, var)); bad += 1
sys.exit(1 if bad else 0)
PY
[ $? -eq 0 ] && ok "no unscoped name-kill and no unrechecked remembered-pid kill in ci/ or tests/" || bad "a kill site is unscoped (see lines above)"
}
t_no_unscoped_name_kill

# Positive anchors: the three known-good scoped sites keep their run-id command-line filter, so a
# later edit that drops the scoping fails here as well as in the scan above.
grep -q "commandline like '%\$ID%'" "$ROOT/ci/launcher/bind-int.sh" || bad "bind-int.sh lost its \$ID command-line scoping"
grep -q "commandline like '%\$C3_ID%'" "$ROOT/ci/subnet/multi-subnet-bringup-int.sh" || bad "multi-subnet lost its \$C3_ID command-line scoping"
grep -q "CommandLine -match '\$PROBE'" "$ROOT/ci/idle-translate/wake-survival-int.sh" || bad "wake-survival lost its \$PROBE command-line scoping"
grep -q "commandline like '%\$ID%'" "$ROOT/ci/psyche/live-relay-int.sh" || bad "live-relay lost its \$ID command-line scoping"

[ "$fail" -eq 0 ] && echo "PASS ci-kill-scoping" || echo "FAIL ci-kill-scoping"
exit "$fail"
