#!/usr/bin/env bash
# Contract tests for the remote seat launcher. Local only — no buildbox, no engine, no network
# beyond a DNS miss. Proves the WRAPPER-CONTRACT exit codes survive the remoting layer.
set -uo pipefail

SEAT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LAUNCHER="$SEAT_DIR/remote-seat.mjs"
PASS=0 FAIL=0
ok()   { PASS=$((PASS+1)); echo "PASS $1"; }
bad()  { FAIL=$((FAIL+1)); echo "FAIL $1"; }
check(){ [[ "$2" == "$3" ]] && ok "$1 ($2)" || bad "$1: expected $3, got $2"; }

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

# --- usage errors stay local and exit 2 (no ssh round trip) ---
node "$LAUNCHER" --adapter codex --wrapper codex.sh --workspace "$TMP" --task-slug s >/dev/null 2>&1
check "missing --trust is a usage error" "$?" 2
node "$LAUNCHER" --bogus x >/dev/null 2>&1
check "unknown flag is a usage error" "$?" 2

# --- adapter gating: blocked adapters and the kill switch never dispatch ---
for adapter in north pi grok canary; do
  node "$LAUNCHER" --check "$adapter" >/dev/null 2>&1
  check "adapter $adapter is not remoted" "$?" 1
done
node "$LAUNCHER" --check codex >/dev/null 2>&1
check "adapter codex is remoted" "$?" 0
node "$LAUNCHER" --check claude >/dev/null 2>&1
check "adapter claude is remoted" "$?" 0
HARNESS_SEAT_REMOTE=0 node "$LAUNCHER" --check codex >/dev/null 2>&1
check "HARNESS_SEAT_REMOTE=0 kill switch forces local" "$?" 1
HARNESS_SEAT_CONTAINER=1 node "$LAUNCHER" --check codex >/dev/null 2>&1
check "in-container re-entry forces local (no infinite remoting)" "$?" 1

# --- transport failure is exit 3: engine down, nothing dispatched ---
REPO="$TMP/repo"
mkdir -p "$REPO"
git -C "$REPO" init -q
printf '/seat-prompt-*.txt\n.harness-seat/\n' > "$REPO/.gitignore"
git -C "$REPO" add -A
git -C "$REPO" -c user.name=t -c user.email=t@t commit -q -m base
cat > "$TMP/build-remote.json" <<JSON
{"enabled":true,"hosts":["seat-transport-failure.invalid"],"port":2222,"ssh_user":"user",
 "remote_root":"/home/user/builds","identity_file":"~/.ssh/id_ed25519_buildbox",
 "local_fallback":false,"local_only":[],"max_remote_jobs":1}
JSON
BUILD_REMOTE_CONFIG="$TMP/build-remote.json" \
  node "$LAUNCHER" --adapter codex --wrapper codex.sh --workspace "$REPO" \
    --trust "noop" --task-slug transportfail --model gpt-5.6-terra-low >/dev/null 2>&1
check "unreachable host is exit 3 (engine down)" "$?" 3

# --- remoting switched off is rc 70: the wrapper runs locally, it does not fail ---
cat > "$TMP/build-remote-off.json" <<JSON
{"enabled":false,"hosts":["debian1"],"port":2222,"ssh_user":"user",
 "remote_root":"/home/user/builds","identity_file":"~/.ssh/id_ed25519_buildbox",
 "local_fallback":true,"local_only":[],"max_remote_jobs":1}
JSON
BUILD_REMOTE_CONFIG="$TMP/build-remote-off.json" \
  node "$LAUNCHER" --adapter codex --wrapper codex.sh --workspace "$REPO" \
    --trust "noop" --task-slug remoteoff --model gpt-5.6-terra-low >/dev/null 2>&1
check "remoting disabled by config is rc 70 (run locally)" "$?" 70

# --- the prompt rides the ignored-file overlay: any repo must be self-provisioned, not just ours ---
IGNORED="$TMP/ignored"
mkdir -p "$IGNORED"
git -C "$IGNORED" init -q
git -C "$IGNORED" -c user.name=t -c user.email=t@t commit -q --allow-empty -m base
dispatch_ignored() {
  BUILD_REMOTE_CONFIG="$TMP/build-remote.json" \
    node "$LAUNCHER" --adapter codex --wrapper codex.sh --workspace "$1" \
      --trust "noop" --task-slug promptignore --model gpt-5.6-terra-low >/dev/null 2>&1
}
dispatch_ignored "$IGNORED"
git -C "$IGNORED" check-ignore -q seat-prompt-probe.txt
check "a repo with no .gitignore is self-provisioned so the prompt ships" "$?" 0
check "the patterns land in info/exclude, not the tracked .gitignore" "$(ls -A "$IGNORED" | grep -c gitignore)" 0
check "the dispatch left no prompt behind" "$(ls "$IGNORED" | grep -c seat-prompt)" 0
dispatch_ignored "$IGNORED"
check "provisioning is idempotent across dispatches" "$(grep -c 'seat-prompt' "$IGNORED/.git/info/exclude")" 1

# a linked worktree shares the common dir: provisioning it once must cover the whole repo
git -C "$IGNORED" worktree add -q -b wtprobe "$TMP/ignored-wt" >/dev/null 2>&1
dispatch_ignored "$TMP/ignored-wt"
git -C "$TMP/ignored-wt" check-ignore -q seat-prompt-probe.txt
check "a linked worktree is covered by the same common-dir exclude" "$?" 0
check "the worktree dispatch did not append a second copy" "$(grep -c 'seat-prompt' "$IGNORED/.git/info/exclude")" 1

# an exclude file the launcher cannot use must still exit on a CONTRACT code, not an uncaught throw
UNWRITABLE="$TMP/unwritable"
mkdir -p "$UNWRITABLE"
git -C "$UNWRITABLE" init -q
git -C "$UNWRITABLE" -c user.name=t -c user.email=t@t commit -q --allow-empty -m base
rm -f "$UNWRITABLE/.git/info/exclude"; mkdir -p "$UNWRITABLE/.git/info/exclude"
dispatch_ignored "$UNWRITABLE"
check "an unusable info/exclude fails on the contract backstop (3)" "$?" 3

# --- classification: a dispatch of unknown outcome must NOT claim nothing ran ---
node --input-type=module -e "
import { classifyReason } from '$LAUNCHER';
const down = ['unreachable-or-full','push-failed','transport-grace-expired','start-failed'];
const unknown = ['start-ambiguous:RUNNING','unexpected-job-state:X','invalid-job-meta','pull-failed','epoch-mismatch'];
const local = ['disabled','local_only:x'];
const bad = [...down.filter((r) => classifyReason(r) !== 3), ...unknown.filter((r) => classifyReason(r) !== 124), ...local.filter((r) => classifyReason(r) !== 70)];
if (bad.length) { console.error('misclassified: ' + bad.join(',')); process.exit(1); }
"
check "reason->rc mapping (3 = nothing ran, 124 = outcome unknown)" "$?" 0

# --- commit replay: the transport's rb snapshot must not enter the caller's history ---
REPLAY="$TMP/replay"
mkdir -p "$REPLAY"
git -C "$REPLAY" init -q
git -C "$REPLAY" config user.name t; git -C "$REPLAY" config user.email t@t
echo one > "$REPLAY/a"; git -C "$REPLAY" add -A; git -C "$REPLAY" commit -q -m base
BASE="$(git -C "$REPLAY" rev-parse HEAD)"
echo dirty > "$REPLAY/b"; git -C "$REPLAY" add -A
SNAP="$(git -C "$REPLAY" -c user.name=rb -c user.email=rb@local commit-tree "$(git -C "$REPLAY" write-tree)" -p "$BASE" -m 'rb snapshot')"
echo agent > "$REPLAY/c"; git -C "$REPLAY" add -A
AGENT="$(git -C "$REPLAY" -c user.name=Seat -c user.email=seat@box commit-tree "$(git -C "$REPLAY" write-tree)" -p "$SNAP" -m 'seat work')"
git -C "$REPLAY" reset -q --mixed "$BASE"
node --input-type=module -e "
import { replayAgentCommits } from '$LAUNCHER';
replayAgentCommits('$REPLAY', '$BASE', '$AGENT', () => {});
"
SUBJECTS="$(git -C "$REPLAY" log --format=%s "$BASE"..HEAD | tr '\n' ',')"
check "seat commit replayed without the rb snapshot" "$SUBJECTS" "seat work,"
check "replayed commit keeps the seat author" "$(git -C "$REPLAY" log -1 --format=%ae)" "seat@box"

# --- a rebooted buildbox must not report a code the orchestrator can misread ---
node --input-type=module -e "
import { contractRc, classifyReason, SEAT_CONTRACT_RC } from '$LAUNCHER';
const eq = (a, b, what) => { if (a !== b) { console.error(what + ': expected ' + b + ', got ' + a); process.exit(1); } };
// the transport's own codes, none of which are wrapper codes
eq(contractRc({ status: 254, classification: 'remote-job-disappeared' }), 124, 'job vanished with the box');
eq(contractRc({ status: 0, classification: 'remote-job-disappeared' }), 124, 'a vanished job is never a success');
eq(contractRc({ status: 254 }), 124, 'bare 254 is not a contract code');
eq(contractRc({ status: 143 }), 124, 'a signal code is not a contract code');
// a real wrapper result still passes through untouched
for (const rc of SEAT_CONTRACT_RC) eq(contractRc({ status: rc, classification: 'remote-rc' }), rc, 'wrapper rc ' + rc + ' passes through');
// the two grace expiries mean opposite things to a caller deciding whether to re-dispatch
eq(classifyReason('transport-grace-expired'), 3, 'expiry BEFORE dispatch is nothing-ran');
eq(classifyReason('transport-grace-expired-after-start'), 124, 'expiry AFTER dispatch is outcome-unknown');
"
check "a rebooted box reports 124, never a raw transport code" "$?" 0

echo "---- seat-contract: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]
