#!/bin/sh
# Brand-value regression guard for the `spt endpoint run` shortcut: the manifest MUST declare
# adapter.shortcut_basename = "cc" (→ cc-<id>, the M12 cc launcher). A drift to "sptc"/"spt"/default
# is caught here, distinct from the structural manifest-schema check. (The launcher brand is
# decoupled from the plugin name `sptc`; the s/sptc/spt/ succession renames the plugin, not this.)
# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
fail=0

# The active assignment line (not a comment): a TOML key at column 0. grep -E, exclude `#`-leading.
line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
# [unit->REQ-DIST-SHORTCUT-BASENAME]
if [ -z "$line" ]; then echo "FAIL no shortcut_basename assignment in manifest"; exit 1; fi

case "$line" in
  *'"cc"'*) echo "ok   shortcut_basename = \"cc\" (cc-<id> brand intact)" ;;
  *) echo "FAIL shortcut_basename is not \"cc\": $line"; fail=1 ;;
esac

# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

# Non-interactive bringup: ALL FOUR CC spawn commands — [session.self] + [session.resume], base +
# ccs profile — route through the launch shim (0.10.3: `{adapter_dir}/claude-spt launch …`), which
# appends --dangerously-skip-permissions ITSELF (launch.rs cli_argv, unit-tested in the crate). The
# broker spawns CC into a PTY with no operator, so an interactive permission prompt would deadlock
# the launch (docs/KNOWN-HAZARDS.md §2.2). Manifest side: exactly 6 launch spawn lines — self +
# resume for base, `:ccs`, and `:alt` (the account-root profile, ADR-0010). Shim side:
# the flag literal must exist in launch.rs (cross-file guard — a shim edit must not silently drop
# the deadlock-breaker). And NO bare claude/ccs spawn line may linger (it would bypass the shim).
spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"\{adapter_dir\}/claude-spt launch ' "$MANIFEST")
nspawn=$(printf '%s' "$spawn" | grep -c .)
# [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
if [ "$nspawn" -eq 6 ]; then echo "ok   6 CC spawn commands route through claude-spt launch (self/resume × base/ccs/alt)"; else echo "FAIL expected 6 claude-spt launch spawn commands, found $nspawn"; fail=1; fi
if grep -q -- '--dangerously-skip-permissions' "$ROOT/tools/claude-spt/src/launch.rs"; then echo "ok   launch shim carries --dangerously-skip-permissions (deadlock-breaker)"; else echo "FAIL launch.rs lost --dangerously-skip-permissions"; fail=1; fi
if grep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST"; then echo "FAIL a bare claude/ccs spawn command bypasses the launch shim"; fail=1; else echo "ok   no bare claude/ccs spawn command (all via the shim)"; fi

# [session.resume] declares the native-resume launch — the shim's `--resume {session_id}` maps to
# CC's `-r <session_id>` (reload the real transcript, else a resume re-runs [session.self] → blank;
# the -r mapping is unit-tested in launch.rs resume_argv_leads_with_native_resume_verb). ALL THREE
# resume roles (base + ccs + alt) must thread {session_id} AND {id}.
resume=$(printf '%s\n' "$spawn" | grep -- '--resume')
nresume=$(printf '%s' "$resume" | grep -c .)
# [unit->REQ-DIST-SESSION-RESUME]
if [ "$nresume" -ne 3 ]; then echo "FAIL expected 3 [session.resume] launch commands (base+ccs+alt), found $nresume"; fail=1; else
  echo "ok   3 [session.resume] launch commands (base + ccs + alt profiles)"
  if [ "$(printf '%s\n' "$resume" | grep -c -- '--resume {session_id}')" -eq 3 ]; then echo "ok   [session.resume] reloads by {session_id} (--resume → claude -r)"; else echo "FAIL a [session.resume] command misses --resume {session_id}: $resume"; fail=1; fi
  if [ "$(printf '%s\n' "$resume" | grep -c -- '--id {id}')" -eq 3 ]; then echo "ok   [session.resume] threads the endpoint {id} into the shim"; else echo "FAIL a [session.resume] command misses --id {id}: $resume"; fail=1; fi
fi

# [unit->REQ-DIST-NAME-UNIFY]
# U3: the repo was renamed spt-claude-code -> claude-spt, then RELOCATED to the private home
# (ADR-0008, v0.22.0). [update].repo MUST read the private home exactly (else `spt adapter update`
# pulls from the old public repo, which gets no new releases), and NO owner-qualified old slug may
# linger in the manifest (regression guard for both the ref-flip and the relocation).
# [unit->REQ-DIST-PRIVATE-HOME]
if grep -Eq '^[[:space:]]*repo[[:space:]]*=[[:space:]]*"BigscreenVR/claude-spt-bs"' "$MANIFEST"; then echo 'ok   [update].repo = "BigscreenVR/claude-spt-bs" (private home)'; else echo "FAIL [update].repo is not BigscreenVR/claude-spt-bs"; fail=1; fi
if grep -Eq '^[[:space:]]*repo[[:space:]]*=[[:space:]]*"SaberMage/claude-spt"' "$MANIFEST"; then echo "FAIL [update].repo still points at the old public repo"; fail=1; else echo "ok   [update].repo no longer points at the old public repo"; fi
if grep -q 'SaberMage/spt-claude-code' "$MANIFEST"; then echo "FAIL stale SaberMage/spt-claude-code ref lingers in manifest"; fail=1; else echo "ok   no stale spt-claude-code repo ref in manifest"; fi

# [unit->REQ-DIST-UPDATE-MESSAGE]
# U1: [update] carries a `message` field (markdown spt-core prints on a real apply). It MUST mention
# the unavoidable /reload-plugins manual residual — that is the field's whole reason to exist. Match
# the active `message =` assignment and the reload-plugins notice anywhere in the manifest body.
if grep -Eq '^[[:space:]]*message[[:space:]]*=' "$MANIFEST"; then echo "ok   [update].message present"; else echo "FAIL [update] has no message field"; fail=1; fi
if grep -q 'reload-plugins' "$MANIFEST"; then echo "ok   [update].message points at /reload-plugins"; else echo "FAIL [update].message lacks the /reload-plugins notice"; fail=1; fi
# D2: [update.post] declares the delegated plugin-reconcile = {adapter_dir}/claude-spt post-update.
if grep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*"\{adapter_dir\}/claude-spt post-update"' "$MANIFEST"; then echo "ok   [update.post].command = \"{adapter_dir}/claude-spt post-update\""; else echo "FAIL [update.post].command missing/wrong"; fail=1; fi
if grep -Eq '^\[update\.post\]' "$MANIFEST"; then echo "ok   [update.post] table present"; else echo "FAIL no [update.post] table"; fail=1; fi

# [unit->REQ-DIST-RC-STARTUP]
# U6 + the 0.10.3 node-named upgrade (doyle ask #4 stretch) + the project-named enrichment: every
# spawn path threads {id} into the launch shim, which computes the node name + project folder ON-NODE
# and sets the DISPLAY name (-n "<id> @ <node> (<project>/)") and the REMOTE-CONTROL channel
# (--remote-control <id>--<node>--<project-token>) — shapes unit-tested in launch.rs
# (names_carry_id_at_node_project_and_separator_rc + rc_project_token_sanitizes_incompatible_chars +
# the argv tests; unknown project drops its suffix, unknown node degrades to bare <id>).
# Manifest side: all 6 launch lines carry --id {id}, and the three fresh ([session.self]) lines carry
# NO --resume. A drift that drops --id anywhere breaks the display/RC identity on that path.
if [ "$(printf '%s\n' "$spawn" | grep -c -- '--id {id}')" -eq 6 ]; then echo "ok   all 6 launch commands thread --id {id} (display + RC identity)"; else echo "FAIL a launch command misses --id {id}: $spawn"; fail=1; fi
nself=$(printf '%s\n' "$spawn" | grep -v -- '--resume' | grep -c .)
if [ "$nself" -eq 3 ]; then echo "ok   3 fresh [session.self] launch commands (base + ccs + alt profiles)"; else echo "FAIL expected 3 fresh launch commands (no --resume), found $nself"; fail=1; fi
# The shim must emit BOTH name flags (cross-file guard, mirrors the skip-perms guard above).
if grep -q -- '"--remote-control"' "$ROOT/tools/claude-spt/src/launch.rs" && grep -q -- '"-n"' "$ROOT/tools/claude-spt/src/launch.rs"; then echo "ok   launch shim emits -n + --remote-control"; else echo "FAIL launch.rs lost the -n/--remote-control name flags"; fail=1; fi
# 0.11.0 {node} adoption (spt-core v0.20.0): all 6 launch lines pass the daemon's advertised node
# label via --node {node}, and every [session.*] keys list declares "node" so the fill substitutes.
# The shim guards a blank/unsubstituted fill (launch.rs unsubstituted_or_blank_node_collapses_to_fallback).
if [ "$(printf '%s\n' "$spawn" | grep -c -- '--node {node}')" -eq 6 ]; then echo "ok   all 6 launch commands thread --node {node} (advertised label)"; else echo "FAIL a launch command misses --node {node}: $spawn"; fail=1; fi
nnodekeys=$(grep -cE '^[[:space:]]*keys[[:space:]]*=.*"node"' "$MANIFEST")
if [ "$nnodekeys" -eq 6 ]; then echo "ok   all 6 [session.*] keys lists declare \"node\""; else echo "FAIL expected 6 keys lists with \"node\", found $nnodekeys"; fail=1; fi

# The `:alt` account-root profile (ADR-0010) overlays self + resume with --account, and BOTH keep the
# full shim shape (the v0.9.1 wholesale-leaf-replace lesson applies to every overlay, not just ccs).
# The role-coverage half — that the psyche + echo-commune roles follow the account too — lives in
# tests/alt-account-roots.sh, which owns this feature's suite.
alt_spawn=$(printf '%s\n' "$spawn" | grep -- '--account ')
nalt=$(printf '%s' "$alt_spawn" | grep -c .)
# [unit->REQ-ALT-ACCOUNT-ROOTS]
if [ "$nalt" -eq 2 ]; then echo "ok   2 alt-profile launch commands carry --account (self + resume)"; else echo "FAIL expected 2 alt launch commands (--account), found $nalt"; fail=1; fi
case "$alt_spawn" in
  *'--resume {session_id}'*) echo "ok   alt [session.resume] keeps native-resume (--resume {session_id})";;
  *) echo "FAIL alt [session.resume] misses --resume {session_id}: $alt_spawn"; fail=1;;
esac

# v0.9.1 FIX (bug #6, still binding): the ccs profile leaf-replaces session.self/.resume `command`
# wholesale, so its overrides MUST mirror base's full shim shape — else a ccs endpoint loses its
# name + RC channel. 0.10.3: the ccs overlay = the same launch shim + `--cli ccs`.
ccs_spawn=$(printf '%s\n' "$spawn" | grep -- '--cli ccs')
nccs=$(printf '%s' "$ccs_spawn" | grep -c .)
# [unit->REQ-CCS-PROFILES]
if [ "$nccs" -eq 2 ]; then echo "ok   2 ccs-profile launch commands carry --cli ccs (self + resume)"; else echo "FAIL expected 2 ccs launch commands (--cli ccs), found $nccs"; fail=1; fi
case "$ccs_spawn" in
  *'--resume {session_id}'*) echo "ok   ccs [session.resume] keeps native-resume (--resume {session_id})";;
  *) echo "FAIL ccs [session.resume] misses --resume {session_id}: $ccs_spawn"; fail=1;;
esac

# [message-idle-translation-binary] declares the idle-delivery filter via `command` (spt-core v0.16.0
# seam; `path` deprecated) = the `translate` subcommand of the consolidated binary, resolved from the
# install dir via {adapter_dir} (D3 fold). MUST be `command` (not the deprecated `path`) and name the
# claude-spt translate subcommand.
idle=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"\{adapter_dir\}/claude-spt translate"' "$MANIFEST")
# [unit->REQ-DIST-IDLE-TRANSLATE]
if [ -n "$idle" ]; then echo "ok   [message-idle-translation-binary] command = \"{adapter_dir}/claude-spt translate\""; else echo "FAIL [message-idle-translation-binary] command != \"{adapter_dir}/claude-spt translate\""; fail=1; fi
# Regression guard: the deprecated bare `path = "cc-spt-idle-translate"` must be GONE (exactly one of path/command).
if grep -Eq '^[[:space:]]*path[[:space:]]*=[[:space:]]*"cc-spt-idle-translate"' "$MANIFEST"; then echo "FAIL deprecated [message-idle-translation-binary].path still present (both-set is refused)"; fail=1; else echo "ok   no deprecated idle-translate path (command-only)"; fi

# [unit->REQ-HAZARD-PSYCHE-IDENTITY-ENV]
# F-028 C2 (doyle brief 2026-07-03): a spawned Psyche must NOT inherit the parent session's identity
# env (OWL_SESSION_ID / SPT_AGENT_ID / SPT_ENDPOINT_ID) — inherited identity makes the psyche's hooks
# resolve "self" to the parent/a foreign perch and rebind THAT perch (KNOWN-HAZARDS §2.5); an
# inherited SPT_ENDPOINT_ID bind-rotates that endpoint's perch to the psyche sid EVERY pulse (the
# 2026-07-09 doyle seat theft — daemon-leaked env). The scrub is the role-level env_remove, honored by
# the spt-core runtime, and rides ALL THREE detached-child roles (psyche_init gate + psyche_resume
# spawn + echo_commune summarizer). Section-scoped check per role header.
# field_of <section-header> <field-regex>: the assignment between the section header and the next.
field_of() { awk -v h="^\\\\[$1\\\\]" -v f="$2" '$0 ~ h {s=1;next} /^\[/{s=0} s && $0 ~ f' "$MANIFEST"; }
for role in session.psyche_init session.psyche_resume session.echo_commune; do
  scrub=$(field_of "$role" '^[[:space:]]*env_remove[[:space:]]*=')
  if [ -z "$scrub" ]; then echo "FAIL [$role] has no env_remove (identity-env scrub missing)"; fail=1; else
    case "$scrub" in *'"OWL_SESSION_ID"'*) : ;; *) echo "FAIL $role env_remove misses OWL_SESSION_ID: $scrub"; fail=1 ;; esac
    case "$scrub" in *'"SPT_AGENT_ID"'*) : ;; *) echo "FAIL $role env_remove misses SPT_AGENT_ID: $scrub"; fail=1 ;; esac
    case "$scrub" in
      *'"SPT_ENDPOINT_ID"'*) echo "ok   [$role] env_remove scrubs OWL_SESSION_ID + SPT_AGENT_ID + SPT_ENDPOINT_ID" ;;
      *) echo "FAIL $role env_remove misses SPT_ENDPOINT_ID (the 2026-07-09 seat-theft miss): $scrub"; fail=1 ;;
    esac
  fi
done

# [unit->REQ-PSYCHE-EPHEMERAL-SHIM]
# v0.14.0 ephemeral shim manifest shape (W4 file form): [session.psyche_resume] is the ONE spawned role
# — captured (detach=false), references {psyche_context_file} on argv (a path the shim reads), lists it
# in keys, and the event rides stdin (no {psyche_prompt} key anywhere). psyche_init survives gate-only.
if awk '/^\[session\.psyche_resume\]/{found=1} END{exit !found}' "$MANIFEST"; then echo "ok   [session.psyche_resume] declared (the spawned ephemeral role)"; else echo "FAIL [session.psyche_resume] missing (ephemeral shim's spawn role)"; fail=1; fi
rz_detach=$(field_of session.psyche_resume '^[[:space:]]*detach[[:space:]]*=')
case "$rz_detach" in *false*) echo "ok   psyche_resume is captured (detach=false)" ;; *) echo "FAIL psyche_resume must be detach=false (stdout-captured), got: [$rz_detach]"; fail=1 ;; esac
rz_cmd=$(field_of session.psyche_resume '^[[:space:]]*command[[:space:]]*=')
case "$rz_cmd" in *'--psyche-context-file {psyche_context_file}'*) echo "ok   psyche_resume command carries --psyche-context-file {psyche_context_file}" ;; *) echo "FAIL psyche_resume command missing --psyche-context-file: [$rz_cmd]"; fail=1 ;; esac
rz_keys=$(field_of session.psyche_resume '^[[:space:]]*keys[[:space:]]*=')
case "$rz_keys" in *'"psyche_context_file"'*) echo "ok   psyche_resume keys include psyche_context_file" ;; *) echo "FAIL psyche_resume keys missing psyche_context_file: [$rz_keys]"; fail=1 ;; esac
# The retired resident key must be GONE from both psyche roles (the event moved to stdin).
if field_of session.psyche_init  '^[[:space:]]*command[[:space:]]*=' | grep -q 'psyche_prompt' \
 || field_of session.psyche_resume '^[[:space:]]*command[[:space:]]*=' | grep -q 'psyche_prompt'; then
  echo "FAIL a psyche role still references the retired {psyche_prompt} (event rides stdin now)"; fail=1
else echo "ok   no {psyche_prompt} in either psyche role (event on stdin)"; fi

# [unit->REQ-INJECT-VERIFY-ECHO]
# W6 LIFECYCLE-TRUTH item 3 (doyle W6-DISPATCH-RULING): the CC-hosted Layer-2 echo-verify opt-in is
# adapter DATA — the manifest declares [env.SPT_INJECT_VERIFY_ECHO] direction="inject" value="1", so
# the broker arms echo-verify at spawn (self-activates once counter-49 deploys; inert + forward-safe
# on older cores, so NO floor bump). Section-scoped, mirroring the identity-env scrub check.
if awk '/^\[env\.SPT_INJECT_VERIFY_ECHO\]/{found=1} END{exit !found}' "$MANIFEST"; then echo "ok   [env.SPT_INJECT_VERIFY_ECHO] declared (Layer-2 echo-verify opt-in)"; else echo "FAIL [env.SPT_INJECT_VERIFY_ECHO] missing (W6 echo-verify opt-in)"; fail=1; fi
ve_dir=$(field_of env.SPT_INJECT_VERIFY_ECHO '^[[:space:]]*direction[[:space:]]*=')
case "$ve_dir" in *'"inject"'*) echo "ok   SPT_INJECT_VERIFY_ECHO direction=inject" ;; *) echo "FAIL SPT_INJECT_VERIFY_ECHO direction must be inject, got: [$ve_dir]"; fail=1 ;; esac
ve_val=$(field_of env.SPT_INJECT_VERIFY_ECHO '^[[:space:]]*value[[:space:]]*=')
case "$ve_val" in *'"1"'*) echo "ok   SPT_INJECT_VERIFY_ECHO value=1 (opt-in ON)" ;; *) echo "FAIL SPT_INJECT_VERIFY_ECHO value must be \"1\", got: [$ve_val]"; fail=1 ;; esac

[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }
