#!/bin/sh
# Validation for F-017 — the multi-subnet bringup gap, FIXED in spt-core v0.14.0 (the
# endpoint-creation-flow milestone, REQ-RUN-MULTISUBNET-HOME / ADRs 0026-0027). This started life as
# the W6 regression SEED that pinned the gap on <= 0.13.2; on v0.14.0 the expected outcomes FLIP from
# "gap reproduces" to "fix confirmed". Still multi-subnet-gated (single-subnet auto-homes + never sees
# any of this). Validates spt-core's REQ-RUN-MULTISUBNET-HOME (their registry, not ours) — kept
# untagged here, like the original finding-repro seed.
#
# THE FIX (v0.14.0). `spt endpoint run` now HOMES an endpoint to one subnet at creation:
#   - multi-subnet node, no --subnet, non-interactive  -> REFUSES clear (MULTI_SUBNET_HOME + the subnet
#     list) INSTANTLY — replacing the old silent ~25s ENDPOINT_RUN_ONLINE_TIMEOUT (the F-017 gap).
#   - --subnet <name>                                  -> homes there; the harness binds (UNBOUND ->
#     ONLINE), no HOME_REFUSED.
# The underlying home-assignment POLICY is unchanged + still correct: a NEW-endpoint `api bind` without
# a home still HOME_REFUSEs (Case 2) — that policy is WHY `endpoint run` had to grow `--subnet`.
#
# Cases 1 & 2 are fast (a refuse + a bare bind probe, no harness spawn) and run on any multi-subnet
# node. Case 3 is the full E2E of the fix: it spawns a REAL claude into a broker PTY (the daemon also
# hosts a Psyche) and asserts the endpoint HOMES + BINDS (UNBOUND -> online) — gated behind
# SPTC_ACCEPTANCE=1. All disposable, per-run-unique ids (REQ-HAZARD-PERCH-COLLISION — NEVER a live
# agent's id); everything torn down on exit. Idempotent.
# Run: sh ci/subnet/multi-subnet-bringup-int.sh                      (cases 1+2)
#      SPTC_ACCEPTANCE=1 sh ci/subnet/multi-subnet-bringup-int.sh    (+ case 3 E2E)   exit 0 = pass.
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
. "$ROOT/ci/lib/spt-probe.sh"
. "$ROOT/ci/lib/spt-bringup.sh"
A=claude-spt
OWLERY="${LOCALAPPDATA:-$HOME/AppData/Local}/spt-core/owlery"

command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# ── Bringup-shape gate — FAILS CLOSED. Never skip on an instrument that did not answer. ────────
# v0.14.0 is the carrying release (endpoint run gained --subnet + the MULTI_SUBNET_HOME refuse).
#
# HAZARD this guards (deployah, 2026-08-04, at the spt-core 0.54.0 tag). This gate used to read:
#     spt endpoint run --help 2>&1 | grep -q -- '--subnet' || { echo "SKIP: ... needs v0.14.0"; exit 0; }
# 0.54.0 RETIRES `endpoint run`, so the command REFUSES, the grep finds nothing, and the int exits 0
# announcing it skipped for being too OLD — the whole multi-subnet suite going green-by-skip on the
# exact version it exists to test, and announcing the opposite of the truth while doing it. A feature
# that is ABSENT and a feature that MOVED are byte-indistinguishable in a grep count. So the probe
# must first prove it read a REAL --help body (a positive control that must match) before a zero is
# allowed to mean "too old" — and when the shape cannot be established, this gate FAILS rather than
# skips. [impl->REQ-HAZARD-VERSION-GATE-FAIL-CLOSED]
# The positive-control'd shape probe lives in ci/lib/spt-bringup.sh so there is exactly ONE
# implementation of it across the ints, not a copy per caller drifting apart.
#
# THIS INT IS DELIBERATELY *NOT* PORTED to the create+start shape, unlike bind-int and
# wake-survival-int, which route their bringup through sptc_bringup. The difference is what is under
# test: there, bringup is a MEANS and any working spelling will do. Here the home-assignment REFUSAL
# SEMANTICS *are* the test — Case 1 asserts that a bringup without `--subnet` refuses with
# MULTI_SUBNET_HOME — and on 0.54.0 that decision MOVED to `create`, where `--subnet` is now
# create-only and the later verbs refuse it outright. Porting the cases means re-deriving which verb
# refuses what, and I cannot exercise that on a node running < 0.54.0. Authoring a green-looking
# assertion I cannot run is how the original defect shipped, so this fails loudly and names the port.
case "$(sptc_bringup_shape)" in
  run)
    # `endpoint run` genuinely exists on this build (positive control matched), so a missing
    # --subnet really is the pre-v0.14.0 gap and SKIP is the honest answer.
    if ! spt endpoint run --help 2>&1 | grep -q -- '--subnet'; then
      echo "SKIP: spt 'endpoint run' has no --subnet (needs v0.14.0 — the F-017 fix). On <= 0.13.2 the gap is unfixed."; exit 0
    fi
    ;;
  create-start)
    # PORTED 2026-08-04 against a real 0.54.0 node (both daemon images 0.54.0, verified — the
    # installed version alone is not the predicate). The home decision MOVED to `create`, where
    # `--subnet` now lives and the later verbs refuse it, "so a later verb must not appear to re-home"
    # something permanent. The REFUSAL TOKEN IS UNCHANGED: `MULTI_SUBNET_HOME:<id>: this node holds N
    # subnets (…) — pass --subnet <name>`, measured on this node.
    :
    ;;
  *)
    echo "FAIL: could not establish the bringup shape — 'spt endpoint run --help' did not answer and" >&2
    echo "      'spt endpoint create' is absent. Not skipping on an unreadable instrument." >&2
    exit 1
    ;;
esac

SHAPE=$(sptc_bringup_shape)

# Multi-subnet GATE — the fix (and the gap it replaced) is invisible on a single-subnet node.
#
# READ FROM `subnet status`, NOT `endpoint list`. The old detector grepped `endpoint list` for
# `^SUBNET ` and counted field 2; that shape is long gone (membership renders as
# "  Joined subnets: A, B, C"), so it returned ZERO on this genuinely 3-subnet node and the whole
# suite skipped announcing "node holds 0 subnet(s)" — a THIRD fail-open skip in this file, and one
# that had nothing to do with 0.54.0. Same defect class as the bringup gate above: a probe whose
# empty result described the probe rather than the node. So this parse asserts the row SHAPE
# (name + two integer columns), which cannot silently match nothing the way a vanished literal did,
# and a zero count is now treated as unreadable rather than as "single-subnet".
# [impl->REQ-HAZARD-VERSION-GATE-FAIL-CLOSED]
SUBNETS=$(spt subnet status 2>/dev/null | grep -E '^[A-Za-z][A-Za-z0-9_-]*[[:space:]]+[0-9]+[[:space:]]+[0-9]+' | awk '{print $1}')
NSUB=$(printf '%s\n' "$SUBNETS" | grep -c .)
if [ "${NSUB:-0}" -eq 0 ]; then
  echo "FAIL: could not read this node's subnet membership from 'spt subnet status'." >&2
  echo "      Not skipping on an unreadable instrument — a zero here is a claim about the PROBE." >&2
  exit 1
fi
if [ "$NSUB" -lt 2 ]; then
  echo "SKIP: node holds $NSUB subnet(s); F-017 only manifests on a multi-subnet node (>=2)"; exit 0
fi
HOME_SUB=$(printf '%s\n' "$SUBNETS" | head -1)
echo "multi-subnet node: [$(printf '%s' "$SUBNETS" | tr '\n' ' ')] — home = $HOME_SUB"

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

MADE=""
RUNPID=""
cleanup() {
  for id in $MADE; do spt endpoint purge "$id" --yes --force >/dev/null 2>&1 || true; done
  # Re-verify the remembered pid is still a claude process before a tree-force kill — a recycled
  # pid would otherwise take an unrelated tree (fleet kill-census, 2026-09-07). [impl->REQ-HAZARD-CI-KILL-SCOPING]
  [ -n "$RUNPID" ] && wmic process where "processid=$RUNPID" get name 2>/dev/null | grep -qiE 'claude' \
    && taskkill //PID "$RUNPID" //T //F >/dev/null 2>&1
  if [ -n "${C3_ID:-}" ]; then
    for p in $(wmic process where "name='claude-spt.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
      taskkill //PID "$p" //T //F >/dev/null 2>&1
    done
    spt endpoint shutdown "$C3_ID" >/dev/null 2>&1 || true
    spt endpoint stop "$C3_ID" >/dev/null 2>&1 || true
    # PURGE owns the whole record set (perch tree, context branches, registry + trust rows); a
    # follow-up `rm -rf` on the perch dir is redundant when purge worked and actively harmful when it
    # did not, since it destroys the evidence while the registry rows survive. (Operator, 2026-08-04.)
    spt endpoint purge "$C3_ID" --yes --force >/dev/null 2>&1 || true
  fi
}
trap cleanup EXIT INT TERM

# ── Case 1 — THE FIX: spt-hosted `endpoint run` without --subnet REFUSES clear + INSTANTLY ────
# (was the silent ~25s ENDPOINT_RUN_ONLINE_TIMEOUT). Non-interactive --start; expect no perch.
C1_ID=f017fix-run-$RUN
start=$(date +%s 2>/dev/null || echo 0)
# The home decision lives on whichever verb MINTS the endpoint: `endpoint run` pre-0.54.0,
# `endpoint create` from 0.54.0 on. NOTE the refusal exits rc=0 and reports on stdout (measured), so
# the TOKEN is the only sound read — an exit-status check would take the refusal for success.
if [ "$SHAPE" = "create-start" ]; then
  out=$(spt endpoint create "$C1_ID" --adapter "$A" --cwd "$PWD" 2>&1)
else
  out=$(spt endpoint run --adapter "$A" --id "$C1_ID" --start 2>&1)
fi
end=$(date +%s 2>/dev/null || echo 0)
case "$out" in
  MULTI_SUBNET_HOME:*) ok "Case 1: mint w/o --subnet -> MULTI_SUBNET_HOME refuse ([$out])" ;;
  *ENDPOINT_CREATED:*|*ENDPOINT_RUN_STARTED*|*ENDPOINT_RUN:*) MADE="$MADE $C1_ID"; bad "Case 1: mint w/o --subnet SUCCEEDED — should refuse on a multi-subnet node [$out]" ;;
  *) bad "Case 1: unexpected mint result (expected MULTI_SUBNET_HOME): [$out]" ;;
esac
# The refuse must be IMMEDIATE — the gap it replaced was a ~25s hang. Allow generous slack (< 10s).
if [ "$start" -gt 0 ] && [ "$end" -gt 0 ]; then
  el=$((end - start))
  [ "$el" -lt 10 ] && ok "Case 1: refuse was immediate (${el}s, not the old ~25s timeout)" \
    || bad "Case 1: refuse took ${el}s — suspiciously close to the old ONLINE_TIMEOUT hang"
fi
[ -f "$OWLERY/$C1_ID/info.json" ] && { MADE="$MADE $C1_ID"; bad "Case 1: a perch was created despite the refuse"; } || ok "Case 1: no perch created (clean refuse, nothing to reap)"

# ── Case 2 — underlying POLICY (unchanged): a NEW-endpoint bind needs a home ──────────────────
C2_ID=f017fix-bind-$RUN
out=$(spt api bind "$C2_ID" --set-session-id "sess-$RUN" 2>&1)
case "$out" in
  HOME_REFUSED:*) ok "Case 2a: bind w/o --subnet -> HOME_REFUSED (home-assignment policy intact) ([$out])" ;;
  BOUND:*)        MADE="$MADE $C2_ID"; bad "Case 2a: bind w/o --subnet BOUND unexpectedly [$out]" ;;
  *)              bad "Case 2a: unexpected bind result: [$out]" ;;
esac
out=$(spt api bind "$C2_ID" --set-session-id "sess-$RUN" --subnet "$HOME_SUB" 2>&1)
case "$out" in
  BOUND:*) MADE="$MADE $C2_ID"; ok "Case 2b: bind WITH --subnet $HOME_SUB -> BOUND ([$out])" ;;
  *)       bad "Case 2b: bind WITH --subnet did not BIND: [$out]" ;;
esac

# ── Case 3 (E2E, gated) — `endpoint run --subnet` HOMES + the harness BINDS (UNBOUND -> online) ─
if [ "${SPTC_ACCEPTANCE:-0}" = "1" ]; then
  if spt adapter list 2>/dev/null | grep -q "$A"; then
    C3_ID=f017fix-home-$RUN
    # --subnet is CREATE-ONLY from 0.54.0 (the later verbs refuse it — home is permanent), so it
    # rides the mint on both shapes. sptc_bringup handles the spelling; the started-token is
    # unchanged across the rename.
    runout=$(sptc_bringup "$C3_ID" "$A" "$PWD" "$HOME_SUB")
    case "$runout" in
      *ENDPOINT_RUN_STARTED*|*ENDPOINT_RUN:*) ok "Case 3: mint --subnet $HOME_SUB + start STARTED ([$runout])" ;;
      *) bad "Case 3: subnet-homed bringup did not start: [$runout]" ;;
    esac
    RUNPID=$(printf '%s' "$runout" | grep -oE 'pid=[0-9]+' | grep -oE '[0-9]+' | head -1)
    # Poll ~40s for the harness to self-bind (UNBOUND -> bound/online). The gap = no perch ever bound.
    #
    # READ LIVENESS FROM `daemon status`, NOT `endpoint list` — and assert the perch record too.
    # This polled `spt endpoint list` for `alive=true`, a token that view does not emit (it renders
    # `ONLINE + CONTROLLED`); `alive=true` belongs to `spt daemon status`. So the loop matched nothing
    # for 40s and reported "never bound / regressed to the no-perch gap" about a bringup that was
    # fine. MEASURED 2026-08-04: an identical disposable bringup on this node had info.json on disk
    # ~2s after start. Its sibling wake-survival-int already reads daemon status; only this one drifted.
    # FOURTH stale probe found in this file — same class as the ^SUBNET detector above and the
    # bringup-shape gate: an empty match describing the PROBE and being reported as a fact about the
    # node. [impl->REQ-HAZARD-VERSION-GATE-FAIL-CLOSED]
    bound=0
    i=0; while [ "$i" -lt 20 ]; do
      if [ -f "$OWLERY/$C3_ID/info.json" ] &&
         spt daemon status 2>/dev/null | grep "$C3_ID" | grep -q "alive=true"; then bound=1; break; fi
      sleep 2; i=$((i+1))
    done
    [ "$bound" -eq 1 ] && ok "Case 3: harness HOMED $HOME_SUB + BOUND (perch record on disk + alive, no HOME_REFUSED)" \
      || bad "Case 3: endpoint never bound within ~40s (regressed to the no-perch gap?)"
  else
    echo "Case 3: SKIP (claude-spt not registered)"
  fi
else
  echo "Case 3: SKIP (set SPTC_ACCEPTANCE=1 — spawns a real claude + Psyche, mutates perch state)"
fi

[ "$fail" -eq 0 ] && { echo "MULTI-SUBNET-BRINGUP-INT OK (F-017 fix confirmed; bringup shape: $SHAPE)"; exit 0; } || { echo "MULTI-SUBNET-BRINGUP-INT FAIL"; exit 1; }
