#!/bin/sh
# Integration proof for the SessionStart BIND SEAM against a REAL spt-core: the identity brief's
# perch claim is emitted only after a registration that SUCCEEDED, and a refused registration emits
# the no-perch shape carrying the daemon's own refusal bytes.
# [int->REQ-PORTER-PERCH-CLAIM-GATED] [int->REQ-PORTER-BIND-REFUSAL-SURFACED]
#
# Why this exists: at v0.58.0 ER acceptance a REFUSED bind still emitted the full
# <sptc-active-perch> brief — the agent told, in its own opening context, that it held a perch it
# did not hold — while the daemon's refusal reached only the adapter log (releases#204). The
# published harness contract now states the rule this script drives: treat a session brief that
# asserts perch ownership as conditional on the bind's exit, never on having attempted it; on a
# refused bind, emit the no-perch shape and carry the refusal into it.
#
# Mutates node-local perch state (a throwaway perch, torn down on exit), so it is gated behind
# SPTC_ACCEPTANCE=1. Idempotent.
# Run: SPTC_ACCEPTANCE=1 sh ci/hooks/bind-seam-int.sh   (exit 0 = pass).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
ADAPTER=claude-spt
# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION).
BID=sptc-bindseam-int

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates perch state)"; exit 0; fi
command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# $SPTC_HOOKBIN aims this proof at a specific build. Its purpose is the POSITIVE CONTROL: point it
# at a pre-fix binary (<= 0.26.2) and every arm-1/arm-3 assertion must FAIL. An all-green run that
# has never been shown failing is an instrument nobody has proven can still see the defect.
HOOKBIN="${SPTC_HOOKBIN:-$ROOT/tools/claude-spt/target/release/claude-spt.exe}"
[ -x "$HOOKBIN" ] || HOOKBIN="$ROOT/tools/claude-spt/target/release/claude-spt"
[ -x "$HOOKBIN" ] || { echo "SKIP: claude-spt binary not built (cargo build --release)"; exit 0; }

fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }

# A sandboxed HOME so the SessionStart brief's spill files land in the throwaway tree, not in the
# runner's own ~/.claude. The .claude dir must EXIST — without it the spill write fails and the
# adapter degrades to the essentials prefix, which would weaken every assertion below.
SANDHOME=$(mktemp -d "${TMPDIR:-/tmp}/sptc-bindseam.XXXXXX") || exit 1
mkdir -p "$SANDHOME/.claude"
trap 'spt endpoint shutdown "$BID" >/dev/null 2>&1 || true; spt endpoint stop "$BID" >/dev/null 2>&1 || true; rm -rf "$SANDHOME"' EXIT INT TERM

# Run the SHIPPED hook entry point with the ambient perch env scrubbed but $SPT_ENDPOINT_ID set to
# the id under test — that env var is exactly the "request" the old code mistook for a registration.
# Whoever runs this is very often an spt-hosted agent whose shell exports its OWN perch id; left
# inherited, every assertion below would point at the operator's live endpoint.
startrun() { # startrun <endpoint-id> <session-id> -> the hook's stdout
  printf '%s' "{\"hook_event_name\":\"SessionStart\",\"source\":\"startup\",\"session_id\":\"$2\",\"transcript_path\":\"$SANDHOME/no-such-transcript.jsonl\",\"cwd\":\"$SANDHOME\"}" \
    | env -u OWL_SESSION_ID -u SPT_AGENT_ID -u SPT_SESSION_NAME -u SPT_HOST_PID -u SPT_ADAPTER \
      HOME="$SANDHOME" SPT_ENDPOINT_ID="$1" "$HOOKBIN" hook SessionStart 2>"$SANDHOME/err.txt"
}

# -- Arm 1 - a REFUSED registration claims no perch and names the refusal. ----------------------
# The refusal cause is deliberately one that does NOT depend on the node's shape: an id carrying a
# disallowed character is refused by every spt-core on every node, so this arm runs everywhere.
# (The field case that opened #204 - ANCHOR_REFUSED on a multi-subnet node - is arm 3, which can
# only run where the node actually holds more than one subnet.)
out=$(startrun "sptc bad id" "bindseam-$$-refused")
case "$out" in
  *"<sptc-active-perch"*) bad "arm1: a REFUSED bind still emitted the perch claim (this is the #204 defect)" ;;
  *) ok "arm1: refused bind emits NO perch claim" ;;
esac
case "$out" in
  *"<sptc_perch_refused"*) ok "arm1: no-perch shape emitted" ;;
  *) bad "arm1: refused bind emitted no <sptc_perch_refused> block; out=[$out]" ;;
esac
# The refusal must be the DAEMON's own bytes, not a label we invented for it.
case "$out" in
  *INVALID_ID*) ok "arm1: the daemon's refusal rides the block verbatim" ;;
  *) bad "arm1: the block does not carry the daemon's refusal text; out=[$out]" ;;
esac
# And the bringup idle assert must be skipped - after a refusal the perch holds its PRIOR sid, so
# a state-idle call scoped to THIS session id could only ever AUTH_REFUSE.
if grep -q "bringup idle SKIPPED" "$SANDHOME/err.txt"; then
  ok "arm1: bringup idle assert skipped on a refused bind"
else
  bad "arm1: bringup idle was attempted after a refused bind"
fi

# -- Arm 2 - a SUCCESSFUL registration still claims its perch (the fix must not over-gate). -----
# Anchor the throwaway perch first (a node holding more than one subnet will not guess a NEW
# endpoint's home), then let the hook itself perform the bind.
subnets=$(spt subnet --json 2>/dev/null | sed -n 's/.*"name": *"\([^"]*\)".*/\1/p')
nsub=$(printf '%s\n' "$subnets" | grep -c .)
subnet="${SPTC_INT_SUBNET:-}"
if [ -z "$subnet" ] && [ "$nsub" -gt 1 ]; then subnet=$(printf '%s\n' "$subnets" | head -1); fi
if [ -n "$subnet" ]; then
  spt api --adapter "$ADAPTER" bind "$BID" --subnet "$subnet" --set-session-id "bindseam-$$-anchor" --session-id "bindseam-$$-anchor" >/dev/null 2>&1
else
  spt api --adapter "$ADAPTER" bind "$BID" --set-session-id "bindseam-$$-anchor" --session-id "bindseam-$$-anchor" >/dev/null 2>&1
fi
out=$(startrun "$BID" "bindseam-$$-ok")
# The hook's stdout is a JSON envelope, so the brief's own quotes arrive backslash-escaped. Compare
# on an unescaped copy: the assertion is about WHICH id the brief claims, not how JSON spelled it.
unesc=$(printf '%s\n' "$out" | sed 's/\\"/"/g')
case "$unesc" in
  *"<sptc-active-perch id=\"$BID\""*) ok "arm2: a successful bind still claims its perch" ;;
  *) bad "arm2: a SUCCESSFUL registration emitted no perch claim - the gate is over-tight; out=[$out]" ;;
esac
case "$out" in
  *"<sptc_perch_refused"*) bad "arm2: a successful bind emitted the no-perch shape" ;;
  *) ok "arm2: no refusal block on a successful bind" ;;
esac

# -- Arm 3 - the FIELD case that opened #204, where the node can carry it. ----------------------
# ANCHOR_REFUSED fires when a bind must anchor a home and the node holds more than one subnet. A
# single-subnet node cannot produce it; say so out loud rather than letting a silent skip read as a
# pass for an arm that never ran.
if [ "$nsub" -gt 1 ]; then
  out=$(startrun "sptc-bindseam-nonexistent-$$" "bindseam-$$-anchorrefused")
  case "$out" in
    *"<sptc-active-perch"*) bad "arm3: ANCHOR_REFUSED still emitted the perch claim (the measured #204 defect)" ;;
    *) ok "arm3: ANCHOR_REFUSED claims no perch" ;;
  esac
  case "$out" in
    *ANCHOR_REFUSED*) ok "arm3: the ANCHOR_REFUSED text reaches the agent's context, not just the log" ;;
    *) bad "arm3: the refusal block does not carry ANCHOR_REFUSED; out=[$out]" ;;
  esac
else
  echo "note arm3 NOT RUN: this node holds $nsub subnet(s); ANCHOR_REFUSED needs more than one."
fi

[ "$fail" -eq 0 ] && echo "BIND-SEAM-INT PASS" || echo "BIND-SEAM-INT FAIL"
exit "$fail"
