#!/usr/bin/env bash
# test-notif-gate.sh — proves every layer of the notification gate without emitting
# a single desktop notification: the real emitter is replaced by a recording stub and
# the registry lives in a throwaway XDG_STATE_HOME.
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GATE="$HERE/../notif_gate.py"
HOOKS="$HERE/../../../workstation/claude/hooks"
CLI="$HERE/../../../workstation/claude/bin/notif-approve"
export NOTIF_GATE_PY="$GATE"

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
export XDG_STATE_HOME="$WORK/state"
BIN="$WORK/bin"
mkdir -p "$BIN" "$XDG_STATE_HOME"

STUB="$WORK/notify-send.real"
SHOWN="$WORK/shown.log"
cat >"$STUB" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$SHOWN_LOG"
EOF
chmod +x "$STUB"
export NOTIF_GATE_REAL="$STUB" SHOWN_LOG="$SHOWN"
: >"$SHOWN"

ln -sf "$(readlink -f "$GATE")" "$BIN/notify-send"
export PATH="$BIN:$PATH"

PASS=0 FAIL=0
ok()   { PASS=$((PASS+1)); printf 'PASS  %s\n' "$1"; }
bad()  { FAIL=$((FAIL+1)); printf 'FAIL  %s — %s\n' "$1" "$2"; }
check(){ if [[ "$2" == "$3" ]]; then ok "$1"; else bad "$1" "expected [$3] got [$2]"; fi; }

pending_json() { python3 -c "
import json,sys
try: print(json.dumps(json.load(open('$XDG_STATE_HOME/notif-gate/pending.json'))['entries']))
except Exception: print('{}')"; }
pfield() { python3 -c "
import json,sys
e=json.loads(sys.stdin.read()).get('$1',{})
print(e.get('$2',''))" ; }

# --- a throwaway emitter, the kind an agent adds without asking -------------------
EMITTER="$WORK/rogue-guard"
cat >"$EMITTER" <<'EOF'
#!/usr/bin/env bash
notify-send -u critical "rogue-guard: exhaustion risk" "3 processes over the limit"
EOF
chmod +x "$EMITTER"
EMITTER_REAL="$(readlink -f "$EMITTER")"
ID="rogue-guard:rogue-guard-exhaustion-risk"

# 1 — unapproved attempt is swallowed and lands in pending
"$EMITTER"; rc=$?
check "unapproved emit exits 0 (caller contract)" "$rc" "0"
check "unapproved emit shows nothing" "$(wc -l <"$SHOWN")" "0"
check "unapproved emit is pending under a stable id" \
  "$(pending_json | python3 -c "import json,sys; print('$ID' in json.load(sys.stdin))")" "True"
check "pending records the emitting program" "$(pending_json | pfield "$ID" program)" "$EMITTER_REAL"
check "pending records what it would have said" \
  "$(pending_json | pfield "$ID" summary_sample)" "rogue-guard: exhaustion risk"
check "pending records the body" "$(pending_json | pfield "$ID" body_sample)" "3 processes over the limit"
check "pending counts one attempt" "$(pending_json | pfield "$ID" count)" "1"

# 2 — repeat attempts aggregate, still silent
"$EMITTER"; "$EMITTER"
check "repeat attempts aggregate" "$(pending_json | pfield "$ID" count)" "3"
check "repeat attempts still show nothing" "$(wc -l <"$SHOWN")" "0"

# 3 — approved source passes through with the exact argv
python3 "$GATE" approve "$ID" >/dev/null
"$EMITTER"
check "approved emit reaches the real emitter" "$(wc -l <"$SHOWN")" "1"
check "approved emit forwards argv verbatim" "$(cat "$SHOWN")" \
  "-u critical rogue-guard: exhaustion risk 3 processes over the limit"
check "approved id leaves the pending list" \
  "$(pending_json | python3 -c "import json,sys; print('$ID' in json.load(sys.stdin))")" "False"

# 4 — an approved id cannot be used to smuggle a different message
: >"$SHOWN"
SMUGGLER="$WORK/smuggler"
cat >"$SMUGGLER" <<'EOF'
#!/usr/bin/env bash
notify-send "rogue-guard: buy crypto now" "different message, same program"
EOF
chmod +x "$SMUGGLER"
"$SMUGGLER"
check "different summary is not covered by the approved id" "$(wc -l <"$SHOWN")" "0"

# 5 — digits are the only free variable inside an approved summary
: >"$SHOWN"
VARIANT="$WORK/rogue-guard-variant"
cat >"$VARIANT" <<'EOF'
#!/usr/bin/env bash
notify-send -u critical "rogue-guard: exhaustion risk" "now 9 processes"
EOF
chmod +x "$VARIANT"
"$VARIANT"
check "a different program with an approved summary is denied" "$(wc -l <"$SHOWN")" "0"

# 6 — --print-id keeps its contract while swallowed
out="$(cd "$WORK" && notify-send -p "unregistered probe" 2>/dev/null)"; rc=$?
check "-p on a swallowed notification exits 0" "$rc" "0"
check "-p on a swallowed notification still prints an id" "$out" "0"

# 7 — revoke puts a source back behind the gate
: >"$SHOWN"
python3 "$GATE" revoke "$ID" >/dev/null
"$EMITTER"
check "revoked source is silent again" "$(wc -l <"$SHOWN")" "0"

# --- the approval action is human-only -------------------------------------------
NOMARK=(env -u AGENT_BUILD_SCOPE_ACTIVE -u AI_AGENT -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT
        -u CLAUDE_CODE_SESSION_ID -u CLAUDE_CODE_EXECPATH -u CLAUDE_CODE_CHILD_SESSION
        -u TMPJAIL_ACTIVE -u CONFINE_ACTIVE)

out="$("${NOMARK[@]}" "$CLI" approve "$ID" 2>&1 </dev/null)"; rc=$?
check "CLI refuses a non-TTY approve with 77" "$rc" "77"
check "CLI names the no-tty reason" "$(grep -c 'no-tty' <<<"$out")" "1"

out="$("${NOMARK[@]}" CLAUDECODE=1 "$CLI" approve "$ID" 2>&1 </dev/null)"; rc=$?
check "CLI refuses an agent-marked approve with 77" "$rc" "77"
check "CLI names the marker" "$(grep -c 'agent-env-marker:CLAUDECODE' <<<"$out")" "1"

python3 "$GATE" register "$EMITTER_REAL" "forgettable probe" >/dev/null
FID="rogue-guard:forgettable-probe"
python3 "$GATE" forget "$FID" >/dev/null
check "forget drops a pending record" \
  "$(pending_json | python3 -c "import json,sys; print('$FID' in json.load(sys.stdin))")" "False"
: >"$SHOWN"
FORGOTTEN="$WORK/rogue-guard"
"$FORGOTTEN" >/dev/null
check "forget grants nothing" "$(wc -l <"$SHOWN")" "0"

out="$(echo | "$CLI" pending 2>&1)"; rc=$?
check "CLI still lists pending without a TTY" "$rc" "0"

# --- authoring hook ---------------------------------------------------------------
hook() { printf '%s' "$2" | node "$HOOKS/$1" 2>/dev/null; }
decision() { python3 -c "
import json,sys
raw=sys.stdin.read().strip()
print(json.loads(raw)['hookSpecificOutput']['permissionDecision'] if raw else 'allow')"; }

payload_edit='{"tool_name":"Edit","tool_input":{"file_path":"/home/user/Projects/x/watcher.py","new_string":"    subprocess.run([\"notify-send\", \"-u\", \"critical\", \"watcher: disk filling\", body])"}}'
check "authoring hook denies a new notify-send call" \
  "$(hook notif-authoring-gate.mjs "$payload_edit" | decision)" "deny"

payload_dbus='{"tool_name":"Write","tool_input":{"file_path":"/home/user/bin/nag.py","content":"bus.call(\"org.freedesktop.Notifications\", \"Notify\", \"nag: hello\")"}}'
check "authoring hook denies a direct D-Bus Notify call" \
  "$(hook notif-authoring-gate.mjs "$payload_dbus" | decision)" "deny"

payload_unit='{"tool_name":"Write","tool_input":{"file_path":"/home/user/.config/systemd/user/nag.service","content":"[Service]\nExecStart=/usr/bin/notify-send \"nag: hourly\" body"}}'
check "authoring hook denies a notifying unit" \
  "$(hook notif-authoring-gate.mjs "$payload_unit" | decision)" "deny"

payload_md='{"tool_name":"Write","tool_input":{"file_path":"/home/user/README.md","content":"run notify-send \"hi\" to test"}}'
check "authoring hook allows prose" \
  "$(hook notif-authoring-gate.mjs "$payload_md" | decision)" "allow"

python3 "$GATE" register "$EMITTER_REAL" "watcher: disk filling" >/dev/null
check "authoring hook allows a summary already registered as pending" \
  "$(hook notif-authoring-gate.mjs "$payload_edit" | decision)" "allow"

payload_cli='{"tool_name":"Bash","tool_input":{"command":"notif-approve forget unattributed:notify-send:systray-ai"}}'
check "authoring hook allows the gate's own CLI" \
  "$(hook notif-authoring-gate.mjs "$payload_cli" | decision)" "allow"

payload_cli_chain='{"tool_name":"Bash","tool_input":{"command":"notif-approve pending; notify-send \"sneak\" body"}}'
check "authoring hook still denies a chained emit" \
  "$(hook notif-authoring-gate.mjs "$payload_cli_chain" | decision)" "deny"

payload_bad='not json'
check "authoring hook fails closed on an unparseable payload" \
  "$(hook notif-authoring-gate.mjs "$payload_bad" | decision)" "deny"

# --- registry protection hook -----------------------------------------------------
REG="$HOME/.local/state/notif-gate/approved.json"
for cmd in "echo x > $REG" "tee $REG < /dev/null" "sed -i s/a/b/ $REG" \
           "cp /tmp/x $REG" "deck-sudo tee $REG"; do
  p=$(python3 -c "import json,sys; print(json.dumps({'tool_name':'Bash','tool_input':{'command':sys.argv[1]}}))" "$cmd")
  check "registry hook denies: ${cmd:0:28}" "$(hook notif-registry-gate.mjs "$p" | decision)" "deny"
done
p=$(python3 -c "import json,sys; print(json.dumps({'tool_name':'Bash','tool_input':{'command':'cat '+sys.argv[1]}}))" "$REG")
check "registry hook allows a plain read" "$(hook notif-registry-gate.mjs "$p" | decision)" "allow"
p=$(python3 -c "import json,sys; print(json.dumps({'tool_name':'Write','tool_input':{'file_path':sys.argv[1],'content':'{}'}}))" "$REG")
check "registry hook denies a Write to the registry" "$(hook notif-registry-gate.mjs "$p" | decision)" "deny"
check "registry hook fails closed on an unparseable payload" \
  "$(hook notif-registry-gate.mjs 'not json' | decision)" "deny"

# --- the installed /usr/bin/notify-send really is the gate --------------------------
INSTALLED=/usr/bin/notify-send
if [[ "$(readlink -f "$INSTALLED")" == /usr/local/lib/notif-gate/notif_gate.py ]]; then
  : >"$SHOWN"
  LIVE="$WORK/live-probe"
  cat >"$LIVE" <<EOF
#!/usr/bin/env bash
$INSTALLED "live-wiring probe" "should never be shown"
EOF
  chmod +x "$LIVE"
  "$LIVE"; rc=$?
  check "installed emitter swallows an unapproved notification" "$(wc -l <"$SHOWN")" "0"
  check "installed emitter keeps the caller contract" "$rc" "0"
  check "installed emitter records it as pending" \
    "$(pending_json | python3 -c "import json,sys; print('live-probe:live-wiring-probe' in json.load(sys.stdin))")" "True"
else
  printf 'SKIP  installed-emitter checks — %s is not the gate\n' "$INSTALLED"
fi

# --- systemd-launched emitters are identified by their unit -------------------------
UNIT_ID="$(python3 "$GATE" id "unit:nag.service" "nag: hourly")"
check "a unit-launched emitter gets a unit-scoped id" "$UNIT_ID" "unit:nag.service:nag-hourly"

# A unit whose ExecStart is notify-send itself has systemd as its parent, so the unit
# name is the only identity available. Run one for real; default-deny swallows it.
# The agent's /tmp is namespaced away from the user manager, so the probe lives under $HOME.
if [[ -n "${XDG_RUNTIME_DIR:-}" ]] && systemctl --user show -p Version >/dev/null 2>&1; then
  PDIR="$(mktemp -d "$HOME/.cache/notifgate-probe.XXXXXX")"
  mkdir -p "$PDIR/bin" "$PDIR/state"
  printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$*" >>"%s/shown.log"\n' "$PDIR" >"$PDIR/real"
  chmod +x "$PDIR/real"
  : >"$PDIR/shown.log"
  ln -sf "$(readlink -f "$GATE")" "$PDIR/bin/notify-send"
  PROBE="notifgate-unitprobe-$$"
  systemd-run --user --quiet --collect --wait --unit="$PROBE" \
    --setenv=XDG_STATE_HOME="$PDIR/state" --setenv=NOTIF_GATE_REAL="$PDIR/real" \
    "$PDIR/bin/notify-send" "unit probe" "should never be shown" >/dev/null 2>&1
  check "a real systemd-launched emitter shows nothing" "$(wc -l <"$PDIR/shown.log")" "0"
  check "a real systemd-launched emitter is recorded under its unit" \
    "$(python3 -c "
import json
d=json.load(open('$PDIR/state/notif-gate/pending.json'))['entries']
print(any(k.startswith('unit:$PROBE.service:') for k in d))" 2>/dev/null)" "True"
  rm -rf "$PDIR"
else
  printf 'SKIP  systemd unit-probe — no user manager\n'
fi

# --- every seeded source resolves to the program the gate will see at runtime --------
python3 - <<'PY'
import json, os, re, subprocess, sys

state = os.path.join(os.path.expanduser("~"), ".local/state/notif-gate/pending.json")
if not os.path.exists(state):
    print("SKIP  seeded-path agreement — no live registry")
    sys.exit(0)
seeded = {v["program"] for v in json.load(open(state))["entries"].values()
          if v.get("channel") == "notify-send" and not v["program"].startswith(("unattributed", "unit:"))}
units = subprocess.run(["systemctl", "--user", "list-unit-files", "--no-legend", "--plain"],
                       capture_output=True, text=True).stdout.split()
runtime = set()
for u in (x for x in units if x.endswith(".service")):
    argv = subprocess.run(["systemctl", "--user", "show", u, "-p", "ExecStart", "--value"],
                          capture_output=True, text=True).stdout
    m = re.search(r"argv\[\]=(.*?);", argv)
    if not m:
        continue
    for tok in m.group(1).split():
        base = os.path.basename(tok)
        if base.startswith(("python", "bash", "sh", "env", "-")) or base == "systemctl":
            continue
        runtime.add(os.path.realpath(tok))
        break
stale = sorted(p for p in seeded if p not in runtime and os.path.exists(p) is False)
drift = sorted(p for p in seeded if p in {os.path.realpath(p)} and p not in runtime and os.path.exists(p))
print(f"PASS  seeded notify-send programs all exist on disk" if not stale
      else f"FAIL  seeded programs missing on disk: {stale}")
print(f"INFO  seeded programs with no matching unit ExecStart (invoked by other means): {len(drift)}")
sys.exit(1 if stale else 0)
PY
[[ $? -eq 0 ]] && PASS=$((PASS+1)) || FAIL=$((FAIL+1))

# --- diagnosing the gate's own wiring stays possible ---------------------------------
for diag in "readlink -f /usr/bin/notify-send" "dpkg-divert --list /usr/bin/notify-send" \
            "grep -rn notify-send /home/user/.local/bin"; do
  p=$(python3 -c "import json,sys; print(json.dumps({'tool_name':'Bash','tool_input':{'command':sys.argv[1]}}))" "$diag")
  check "authoring hook allows read-only diagnosis: ${diag:0:22}" \
    "$(hook notif-authoring-gate.mjs "$p" | decision)" "allow"
done
p=$(python3 -c "import json; print(json.dumps({'tool_name':'Bash','tool_input':{'command':'grep -rn notify-send /etc 2>/dev/null'}}))")
check "authoring hook allows a diagnostic that discards stderr" \
  "$(hook notif-authoring-gate.mjs "$p" | decision)" "allow"
p=$(python3 -c "import json; print(json.dumps({'tool_name':'Bash','tool_input':{'command':'grep -rn notify-send /etc > /tmp/out'}}))")
check "authoring hook denies a diagnostic that redirects to a file" \
  "$(hook notif-authoring-gate.mjs "$p" | decision)" "deny"

for bad_diag in "readlink -f /usr/bin/notify-send && notify-send hi there" \
                "cp /bin/true /usr/bin/notify-send"; do
  p=$(python3 -c "import json,sys; print(json.dumps({'tool_name':'Bash','tool_input':{'command':sys.argv[1]}}))" "$bad_diag")
  check "authoring hook denies a non-read diagnostic: ${bad_diag:0:22}" \
    "$(hook notif-authoring-gate.mjs "$p" | decision)" "deny"
done

# --- a unit whose ExecStart IS notify-send is seeded before it ever fires ------------
SUM_ARGS="$(python3 -c "
import importlib.util as u
s=u.spec_from_file_location('seed','$HERE/../seed.py'); m=u.module_from_spec(s); s.loader.exec_module(m)
argv=['/usr/bin/notify-send','-u','critical','-i','dialog-error','guard DOWN','the body']
print(next(m._summary_args(argv), ''))")"
check "the summary is recovered past notify-send's flags" "$SUM_ARGS" "guard DOWN"

seed_units() { python3 -c "
import importlib.util as u
s=u.spec_from_file_location('seed','$HERE/../seed.py'); m=u.module_from_spec(s); s.loader.exec_module(m)
$1" 2>/dev/null; }
check "unit enumeration returns a mapping" "$(seed_units "print(type(m.notify_units()).__name__)")" "dict"
check "the guard-failure unit no longer puts a popup on screen" \
  "$(seed_units "print('unit:agent-guard-failure-notify.service' in m.notify_units())")" "False"

# --- the per-attempt log: every attempt is a record, whatever the gate decided -------
GLOG="$XDG_STATE_HOME/notif-gate/notif-log.jsonl"
lastrec() { python3 -c "
import json,sys
lines=[l for l in open('$GLOG') if l.strip()]
print(json.loads(lines[-1]).get('$1',''))" 2>/dev/null; }
logmatch() { python3 -c "
import json,sys
n=0
for l in open('$GLOG'):
    if not l.strip(): continue
    try: r=json.loads(l)
    except ValueError: continue
    if r.get('title')=='$1' and r.get('verdict')=='$2': n+=1
print(n)" 2>/dev/null; }

: >"$GLOG"
LOGGED="$WORK/log-probe"
cat >"$LOGGED" <<'EOF'
#!/usr/bin/env bash
notify-send -u critical "log-probe: disk filling" "root is at 96% — 1.2G left"
EOF
chmod +x "$LOGGED"
LOG_ID="log-probe:log-probe-disk-filling"

"$LOGGED"
check "a blocked attempt is logged" "$(logmatch "log-probe: disk filling" blocked)" "1"
check "the log carries its timestamp" "$(lastrec timestamp | grep -E '^[0-9]+(\.[0-9]+)?$' | wc -l)" "1"
check "the log carries the full body, not a template" \
  "$(lastrec body)" "root is at 96% — 1.2G left"
check "the log carries the source app" "$(lastrec source_app)" "$(readlink -f "$LOGGED")"
check "the log carries the source id" "$(lastrec id)" "$LOG_ID"
check "the log carries the channel" "$(lastrec channel)" "notify-send"

: >"$SHOWN"
python3 "$GATE" approve "$LOG_ID" >/dev/null
"$LOGGED"
check "a delivered attempt reaches the screen" "$(wc -l <"$SHOWN")" "1"
check "an allowed attempt is logged too" "$(logmatch "log-probe: disk filling" allowed)" "1"
check "the delivered record survives the execv handover" \
  "$(lastrec body)" "root is at 96% — 1.2G left"

# a log that cannot be written must not cost the owner a notification
: >"$SHOWN"
rm -f "$GLOG"
mkdir -p "$GLOG"
"$LOGGED"; rc=$?
check "an unwritable log does not break delivery" "$(wc -l <"$SHOWN")" "1"
check "an unwritable log keeps the caller contract" "$rc" "0"
rmdir "$GLOG"
python3 "$GATE" revoke "$LOG_ID" >/dev/null
"$LOGGED"; rc=$?
check "an unwritable log does not break suppression either" "$rc" "0"
rm -rf "$GLOG"

# retention is bounded: one rotation, never unbounded growth
python3 -c "
line=b'{}\n'
open('$GLOG','wb').write(line * (4*1024*1024//len(line) + 1))"
"$LOGGED"
check "the log rotates at its size cap" \
  "$([[ -f "$GLOG.1" ]] && echo yes || echo no)" "yes"
check "the rotated log starts fresh" "$(wc -l <"$GLOG")" "1"
rm -f "$GLOG" "$GLOG.1"

# --- the D-Bus path: emitters that keep their own bus call ask the gate first -------
GDBUS_LOG="$WORK/gdbus.log"
cat >"$BIN/gdbus" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$GDBUS_LOG"
printf '(uint32 91,)\n'
EOF
chmod +x "$BIN/gdbus"
export GDBUS_LOG
: >"$GDBUS_LOG"

DBUS_PROG="$WORK/bus-emitter"
DBUS_ID="bus-emitter:bus-emitter-listener-appeared"

python3 "$GATE" check "$DBUS_PROG" "bus-emitter: listener appeared" "tcp 0.0.0.0:9" dbus-gated
check "check denies an unapproved direct-bus source" "$?" "1"
check "a denied bus source lands in pending" \
  "$(pending_json | pfield "$DBUS_ID" program)" "$DBUS_PROG"
check "a gated bus source records its channel" \
  "$(pending_json | pfield "$DBUS_ID" channel)" "dbus-gated"
check "a gated bus source is not advertised as unswallowable" \
  "$(python3 "$GATE" pending | grep -c 'CANNOT swallow')" "0"

python3 "$GATE" register "$DBUS_PROG" "raw bus source" "" dbus-direct >/dev/null
check "an ungated bus source is still advertised as unswallowable" \
  "$(python3 "$GATE" pending | grep -c 'CANNOT swallow')" "1"

python3 "$GATE" approve "$DBUS_ID" >/dev/null
python3 "$GATE" check "$DBUS_PROG" "bus-emitter: listener appeared" "tcp 0.0.0.0:9" dbus-gated
check "check admits an approved direct-bus source" "$?" "0"
python3 "$GATE" revoke "$DBUS_ID" >/dev/null

python3 "$GATE" check "$WORK/relocated/bus-emitter" "bus-emitter: listener appeared" "" dbus-gated
check "a relocated source refreshes its recorded path instead of pinning a stale one" \
  "$(pending_json | pfield "$DBUS_ID" program)" "$WORK/relocated/bus-emitter"

check "a digit-shifted summary reuses the approved template id" \
  "$(python3 "$GATE" id "$DBUS_PROG" 'bus-emitter: listener appeared')" "$DBUS_ID"

# agent-guard notifier.py — same registry, same ids, no bus call while unapproved
AG="$HERE/../../../monitor/agent-guard/src/agent_guard/notifier.py"
ag_py() { PYTHONPATH="$(dirname "$AG")" python3 -c "
import importlib.util as u, sys
s=u.spec_from_file_location('n','$AG'); m=u.module_from_spec(s); s.loader.exec_module(m)
$1"; }
AG_ID="notifier.py:agent-guard-probe"

: >"$GDBUS_LOG"
check "agent-guard emits nothing while unapproved" \
  "$(ag_py "print(m.gdbus_notify('agent-guard probe','body',['Dismiss']))")" "0"
check "agent-guard made no bus call while unapproved" "$(wc -l <"$GDBUS_LOG")" "0"
check "agent-guard's denial is pending under its own source path" \
  "$(pending_json | pfield "$AG_ID" program)" "$(readlink -f "$AG")"

python3 "$GATE" approve "$AG_ID" >/dev/null
check "agent-guard reaches the bus once approved" \
  "$(ag_py "print(m.gdbus_notify('agent-guard probe','body',['Dismiss']))")" "91"
check "agent-guard's approved call went to the bus" \
  "$([[ $(wc -l <"$GDBUS_LOG") -ge 1 ]] && echo yes || echo no)" "yes"

check "agent-guard denies when the gate is missing rather than emitting" \
  "$(NOTIF_GATE_PY=$WORK/absent.py ag_py "print(m.gate_allows('agent-guard probe','body'))")" "False"

# reaper-notifier.py — the libnotify path
RN="$HERE/../../../workstation/claude/bin/reaper-notifier.py"
rn_py() { python3 -c "
import importlib.util as u
s=u.spec_from_file_location('r','$RN'); m=u.module_from_spec(s); s.loader.exec_module(m)
$1"; }
RN_ID="reaper-notifier.py:reaper-probe-pid-#"

check "reaper-notifier is denied while unapproved" \
  "$(rn_py "print(m.gate_allows('reaper probe PID 42','body'))")" "False"
check "reaper-notifier's denial is pending under its own source path" \
  "$(pending_json | pfield "$RN_ID" program)" "$(readlink -f "$RN")"
python3 "$GATE" approve "$RN_ID" >/dev/null
check "reaper-notifier is admitted once approved" \
  "$(rn_py "print(m.gate_allows('reaper probe PID 42','body'))")" "True"
check "reaper-notifier's approval covers a different pid in the same template" \
  "$(rn_py "print(m.gate_allows('reaper probe PID 7781','body'))")" "True"
check "reaper-notifier denies when the gate is missing rather than emitting" \
  "$(NOTIF_GATE_PY=$WORK/absent.py rn_py "print(m.gate_allows('reaper probe PID 42','body'))")" "False"

rn_raise() { rn_py "
class Fake:
    shown = False
    def set_urgency(self, u): pass
    def set_timeout(self, t): pass
    def add_action(self, *a): pass
    def show(self): Fake.shown = True
class Ns:
    class Notification:
        new = staticmethod(lambda s, b: Fake())
    class Urgency:
        CRITICAL = 2
m.GATE_PY = '$1'
n = m.Notifier.__new__(m.Notifier)
n.Notify = Ns
n.active = {}
n._raise('42', {'summary':'reaper probe PID 42','body':'body','actions':[]})
print(Fake.shown, '42' in n.active)"; }

check "a denied reaper notification is never shown but stays deduped" \
  "$(rn_raise "$WORK/absent.py")" "False True"
check "an approved reaper notification is shown" \
  "$(rn_raise "$(readlink -f "$GATE")")" "True True"

safety_of() { python3 -c "
import sys; sys.path.insert(0, '$(dirname "$GATE")')
import notif_gate
print(notif_gate._is_safety({'program': sys.argv[1]}))" "$1"; }

check "the installed agent-guard path is safety-relevant" \
  "$(safety_of /home/user/.local/share/system-monitor/lib/agent_guard/notifier.py)" "True"
check "the agent-guard source path is safety-relevant" \
  "$(safety_of /home/user/Projects/overdeck/modules/monitor/agent-guard/src/agent_guard/notifier.py)" "True"
check "an unrelated program is not safety-relevant" \
  "$(safety_of /usr/lib/discord/discord)" "False"

printf '\n%d passed, %d failed\n' "$PASS" "$FAIL"
[[ $FAIL -eq 0 ]]
