#!/usr/bin/env bash
# test-validate-repo-json.sh — hostile-input suite for the T51 declaration validator.
# (Vendored with validate-repo-json.sh from mosaic-brain @ 515bcbab — see the
# validator header for provenance.)
#
# Hermetic: all fixtures in a tracked mktemp sandbox removed by an EXIT trap
# (pass and fail paths both — zero residue). No network, no real repos, no host
# state mutated. MOSAIC_HOST_ROOT is set/unset per arm via env only.
#
# T51P2RW1: arms extended per review T51P2R1 (F1-F5): root gate for ordinary
# v2 declarations (unset AND explicitly empty; display warns), git-grammar
# branch arms (double slash, dot component, control byte), contract-escape
# arms (list enums, invalid UTF-8, NaN — one VALIDATION_ERROR line, never a
# traceback), remote normalization (.git/ ordering, port preservation), and
# mirror-component fullmatch arms (trailing newline, control bytes).

set -u

HERE=$(cd "$(dirname "$0")" && pwd)
V="$HERE/validate-repo-json.sh"

PASS=0; FAIL=0; FAILED=""
ok()  { PASS=$((PASS+1)); }
bad() { FAIL=$((FAIL+1)); FAILED="$FAILED $1"; printf 'FAIL: %s\n' "$1" >&2; }

SB=$(mktemp -d "${TMPDIR:-/tmp}/vrj-test.XXXXXX")
trap 'rm -rf "$SB"' EXIT

fx() { printf '%s' "$2" > "$SB/$1"; }

run() { # [env KV=V ...] -- args...
  local envs=()
  while [ "$1" != "--" ]; do envs+=("$1"); shift; done; shift
  OUT=$(env "${envs[@]:-_=_}" bash "$V" "$@" 2>&1 < /dev/null; echo "__RC__$?")
  RC=${OUT##*__RC__}; OUT=${OUT%__RC__*}; OUT=${OUT%$'\n'}
}
expect_ok()   { local d="$1"; shift; run "$@"; if [ "$RC" = 0 ] && printf '%s' "$OUT" | grep -q '^OK'; then ok; else bad "$d (rc=$RC out=$(printf '%s' "$OUT" | head -1))"; fi; }
expect_err() { # desc expected-substring [env... -- args...]
  local d="$1" sub="$2"; shift 2
  run "$@"
  if [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "VALIDATION_ERROR.*$sub"; then ok
  else bad "$d (rc=$RC, wanted error ~$sub, got: ${OUT%%$'\n'*})"; fi
}
expect_err_notrace() { # like expect_err, plus no traceback anywhere in output
  local d="$1" sub="$2"; shift 2
  run "$@"
  if [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "VALIDATION_ERROR.*$sub" && ! printf '%s' "$OUT" | grep -q "Traceback"; then ok
  else bad "$d (rc=$RC, wanted clean error ~$sub, got: ${OUT%%$'\n'*})"; fi
}

STACK='{"schema_version":2,"integration_trunk":"next","release_branch":"main","flow":"trunk-release","canonical_remote":"https://git.mosaicstack.dev/mosaicstack/stack","canonical_clone":"host:/src/mosaic-stack","worktree_root":"host:/src/mosaic-stack-worktrees","worktree_policy":"orchestrator-precreated","notes":"x"}'
BRAIN='{"schema_version":2,"integration_trunk":"main","release_branch":"main","flow":"direct","canonical_remote":"https://git.example.invalid/acme/brain","canonical_clone":"host:/.mosaic","worktree_root":"host:/.mosaic-worktrees","worktree_policy":"orchestrator-precreated"}'
ROOT="$SB/hostroot"; mkdir -p "$ROOT"

echo "== (0) syntax + version =="
bash -n "$V" && ok || bad "bash -n"
run -- --version; [ "$RC" = 0 ] && case "$OUT" in validate-repo-json\ *) ok ;; *) bad "version output" ;; esac || bad "version rc"

echo "== (1) spec examples: stack + brain OK (root set) =="
fx stack.json "$STACK"; fx brain.json "$BRAIN"
expect_ok a1 MOSAIC_HOST_ROOT=$ROOT -- "$SB/stack.json"
expect_ok a2 MOSAIC_HOST_ROOT=$ROOT -- "$SB/brain.json"

echo "== (2) malformed JSON (stable contract, no traceback) =="
fx bad.json '{"schema_version": 2, '
expect_err_notrace b1 "json:" -- "$SB/bad.json"
fx arr.json '[1,2]'
expect_err_notrace b2 "top level" -- "$SB/arr.json"
printf '\xff\xfe{"schema_version":2}' > "$SB/utf8.json"
expect_err_notrace b3 "UTF-8" MOSAIC_HOST_ROOT=$ROOT -- "$SB/utf8.json"
fx nan.json '{"schema_version":NaN}'
expect_err_notrace b4 "malformed JSON" MOSAIC_HOST_ROOT=$ROOT -- "$SB/nan.json"

echo "== (3) unknown schema_version = ABSENT-loud =="
fx v3.json "${STACK/schema_version\":2/schema_version\":3}"
expect_err c1 "schema_version" MOSAIC_HOST_ROOT=$ROOT -- "$SB/v3.json"

echo "== (4) v1 mode + authoring rule (v1 consumes no paths: no root needed) =="
fx v1.json '{"integration_trunk":"next","release_branch":"main"}'
expect_ok  d1 -- "$SB/v1.json"
expect_err d2 "schema_version" -- --require-v2 "$SB/v1.json"
fx v1x.json '{"integration_trunk":"next","release_branch":"main","notes":"no"}'
expect_err d3 "x_extensions" -- "$SB/v1x.json"

echo "== (5) unknown top-level key rejected; x_extensions home OK =="
fx unk.json "${STACK%\}*},\"typo_key\":1}"
expect_err e1 "typo_key" MOSAIC_HOST_ROOT=$ROOT -- "$SB/unk.json"
fx ext.json "${STACK%\}*},\"x_extensions\":{\"future\":true}}"
expect_ok e2 MOSAIC_HOST_ROOT=$ROOT -- "$SB/ext.json"

echo "== (6) flow: required (no defaulting) + cross-field =="
fx noflow.json "$(printf '%s' "$STACK" | python3 -c 'import json,sys; d=json.load(sys.stdin); del d["flow"]; print(json.dumps(d))')"
expect_err f1 "flow" MOSAIC_HOST_ROOT=$ROOT -- "$SB/noflow.json"
fx xdirect.json "${STACK/\"trunk-release\"/\"direct\"}"
expect_err f2 "direct" MOSAIC_HOST_ROOT=$ROOT -- "$SB/xdirect.json"
fx xtr.json "${BRAIN/\"direct\"/\"trunk-release\"}"
expect_err f3 "trunk-release" MOSAIC_HOST_ROOT=$ROOT -- "$SB/xtr.json"

echo "== (7) dot-segment / empty-segment / tilde escapes =="
fx dots.json "${STACK/host:\/src\/mosaic-stack\"/host:/src/../secrets\"}"
expect_err g1 "dot segment" MOSAIC_HOST_ROOT=$ROOT -- "$SB/dots.json"
fx dot1.json "${STACK/host:\/src\/mosaic-stack\"/host:/src/./mosaic-stack\"}"
expect_err g2 "dot segment" MOSAIC_HOST_ROOT=$ROOT -- "$SB/dot1.json"
fx empty.json "${STACK/host:\/src\/mosaic-stack\"/host://src/mosaic-stack\"}"
expect_err g3 "empty segment" MOSAIC_HOST_ROOT=$ROOT -- "$SB/empty.json"
fx tild.json "${STACK/host:\/src\/mosaic-stack\"/~jw/src/mosaic-stack\"}"
expect_err g4 "tilde" MOSAIC_HOST_ROOT=$ROOT -- "$SB/tild.json"
fx tailslash.json "${STACK/host:\/src\/mosaic-stack\"/host:/src/mosaic-stack/\"}"
expect_err g5 "empty segment" MOSAIC_HOST_ROOT=$ROOT -- "$SB/tailslash.json"
fx noanchor.json "${STACK/host:\/src\/mosaic-stack\"//src/mosaic-stack\"}"
expect_err g6 "host:/" MOSAIC_HOST_ROOT=$ROOT -- "$SB/noanchor.json"

echo "== (8) branch-name grammar (delegated to git check-ref-format, F2) =="
fx badbr.json "${STACK/\"next\"/\"bad..name\"}"
expect_err h1 "branch name" MOSAIC_HOST_ROOT=$ROOT -- "$SB/badbr.json"
fx sp.json "${STACK/\"next\"/\"fea ture\"}"
expect_err h2 "branch name" MOSAIC_HOST_ROOT=$ROOT -- "$SB/sp.json"
fx lock.json "${STACK/\"next\"/\"feature/x.lock\"}"
expect_err h3 "branch name" MOSAIC_HOST_ROOT=$ROOT -- "$SB/lock.json"
fx slash.json "${STACK/\"next\"/\"feature/x\"}"
expect_ok h4 MOSAIC_HOST_ROOT=$ROOT -- "$SB/slash.json"
fx dslash.json "${STACK/\"next\"/\"feature//x\"}"
expect_err h5 "branch name" MOSAIC_HOST_ROOT=$ROOT -- "$SB/dslash.json"
fx hidden.json "${STACK/\"next\"/\"feature/.hidden\"}"
expect_err h6 "branch name" MOSAIC_HOST_ROOT=$ROOT -- "$SB/hidden.json"
fx ctrl.json "$(printf '%s' "$STACK" | python3 -c 'import json,sys; d=json.load(sys.stdin); d["integration_trunk"]="feature/\x01x"; print(json.dumps(d))')"
expect_err h7 "branch name" MOSAIC_HOST_ROOT=$ROOT -- "$SB/ctrl.json"

echo "== (8b) reflog shorthand rejected independent of ambient checkout history (B1) =="
# Hermetic repo WITH checkout history: proves '@{-1}' (which git would expand to
# 'main' from THIS repo's reflog) is still refused by the pre-delegation gate.
HISTREPO="$SB/histrepo"; mkdir -p "$HISTREPO"
(cd "$HISTREPO" && git init -q -b main . \
  && git -c user.name=t -c user.email=t@t commit -q --allow-empty -m m \
  && git checkout -q -b feature/x \
  && git checkout -q main \
  && git check-ref-format --branch "@{-1}" >/dev/null 2>&1 && echo "ambient-expandable" || echo "not-expandable") \
  | grep -q ambient-expandable && ok || bad "fixture repo failed to make @{-1} expandable"
fx atminus1.json "$(printf '%s' "$STACK" | python3 -c 'import json,sys; d=json.load(sys.stdin); d["integration_trunk"]="@{-1}"; print(json.dumps(d))')"
# Run the validator from INSIDE the history repo via command substitution so the
# assertion runs in the PARENT shell (T51P2R3 B1: the previous ( subshell ) form
# mutated ok/bad counters only in a dead subshell — FAIL printed, suite rc 0).
OUTX=$(cd "$HISTREPO" && MOSAIC_HOST_ROOT=$ROOT bash "$V" "$SB/atminus1.json" 2>&1 </dev/null; echo "__RC__$?")
RCX=${OUTX##*__RC__}
if [ "$RCX" = 1 ] && printf '%s' "$OUTX" | grep -q "VALIDATION_ERROR.*@{"; then ok
else bad "@{-1} must be rejected inside a repo with checkout history (got rc=$RCX)"; fi
fx atbrace.json "$(printf '%s' "$STACK" | python3 -c 'import json,sys; d=json.load(sys.stdin); d["integration_trunk"]="@{u}"; print(json.dumps(d))')"
expect_err h9 "@{" MOSAIC_HOST_ROOT=$ROOT -- "$SB/atbrace.json"

echo "== (9) canonical_remote: userinfo, list-type, normalization (F3/F4) =="
fx user.json "${STACK/https:\/\/git.mosaicstack.dev/https:\/\/bot:s3cret@git.mosaicstack.dev}"
expect_err_notrace i1 "userinfo" MOSAIC_HOST_ROOT=$ROOT -- "$SB/user.json"
fx listflow.json "${STACK/\"trunk-release\"/[\"trunk-release\"]}"
expect_err_notrace i2 "flow" MOSAIC_HOST_ROOT=$ROOT -- "$SB/listflow.json"
fx listpol.json "${STACK/\"orchestrator-precreated\"/[\"tool-managed\"]}"
expect_err_notrace i3 "worktree_policy" MOSAIC_HOST_ROOT=$ROOT -- "$SB/listpol.json"
run -- --normalize-remote "HTTPS://Git.Example.Invalid/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://git.example.invalid/o/r" ] && ok || bad "norm .git/case ($OUT)"
run -- --normalize-remote "https://git.mosaicstack.dev/mosaicstack/stack/"
[ "$RC" = 0 ] && [ "$OUT" = "https://git.mosaicstack.dev/mosaicstack/stack" ] && ok || bad "norm trailing slash ($OUT)"
run -- --normalize-remote "git.mosaicstack.dev/mosaicstack/stack"
[ "$RC" = 1 ] && ok || bad "schemeless must fail"
run -- --normalize-remote "HTTPS://Git.Example.Invalid/o/r.git/"
[ "$RC" = 0 ] && [ "$OUT" = "https://git.example.invalid/o/r" ] && ok || bad "norm .git-then-slash ($OUT)"
run -- --normalize-remote "https://Git.Example.Invalid:8443/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://git.example.invalid:8443/o/r" ] && ok || bad "port must be preserved ($OUT)"
run -- --normalize-remote "https://[2001:db8::1]:8443/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://[2001:db8::1]:8443/o/r" ] && ok || bad "IPv6 must stay bracketed with port ($OUT)"
run -- --normalize-remote "https://[2001:db8::1]/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://[2001:db8::1]/o/r" ] && ok || bad "IPv6 must stay bracketed ($OUT)"
run -- --normalize-remote "https://Git.Example.Invalid:0/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://git.example.invalid:0/o/r" ] && ok || bad "explicit port 0 must be preserved ($OUT)"
run -- --normalize-remote "https://[::1].evil.example/o/r.git"
[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "canonical_remote" && ok || bad "suffix after ] must be rejected (.evil.example)"
run -- --normalize-remote "https://[::1]x:8443/o/r.git"
[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "canonical_remote" && ok || bad "suffix after ] must be rejected (x:8443)"
run -- --normalize-remote "https://[::1]x/o/r.git"
[ "$RC" = 1 ] && ok || bad "suffix after ] must be rejected (x)"
echo "== (9b) IPvFuture bracketed authorities (R4-B1: guard keys off raw netloc) =="
run -- --normalize-remote "https://[v1.fe80]/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://[v1.fe80]/o/r" ] && ok || bad "valid IPvFuture must keep brackets ($OUT)"
run -- --normalize-remote "https://[vF.foo]:8443/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://[vf.foo]:8443/o/r" ] && ok || bad "valid IPvFuture+port must keep brackets ($OUT)"
run -- --normalize-remote "https://[v1.fe80]evil/o/r.git"
[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "canonical_remote" && ok || bad "IPvFuture suffix must be rejected (evil)"
run -- --normalize-remote "https://[v1.fe80].evil.example/o/r.git"
[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "canonical_remote" && ok || bad "IPvFuture suffix must be rejected (.evil.example)"
run -- --normalize-remote "https://[vF.foo]x:8443/o/r.git"
[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "canonical_remote" && ok || bad "IPvFuture suffix must be rejected (x:8443)"
echo "== (9d) non-bracketed authority grammar (R6-B1) =="
for U in "https://:8443/o/r.git" "https://bad host/o/r.git" "https://bad^host/o/r.git" "https://bad\\host/o/r.git" "https://bad%zz/o/r.git" "https://bad%2/o/r.git" "https://bad%/o/r.git"; do
  run -- --normalize-remote "$U"
  if [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "VALIDATION_ERROR canonical_remote"; then ok
  else bad "non-bracketed authority must be rejected: $U (rc=$RC out=$OUT)"; fi
done
run -- --normalize-remote "https://git.mosaicstack.dev:9000/mosaicstack/stack"
[ "$RC" = 0 ] && [ "$OUT" = "https://git.mosaicstack.dev:9000/mosaicstack/stack" ] && ok || bad "valid host:port unchanged ($OUT)"
run -- --normalize-remote "https://192.168.1.10:8443/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://192.168.1.10:8443/o/r" ] && ok || bad "IPv4 reg-name stays valid ($OUT)"
run -- --normalize-remote "https://bad%2Fx/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://bad%2fx/o/r" ] && ok || bad "complete %HH must stay legal, case-normalized ($OUT)"
echo "== (9e) ASCII-only authority bytes (R7-B1) =="
# isolated port arm (R8): VALID ASCII host + full-width-digit port ONLY —
# unconfounded, so restoring Unicode-aware isdigit() goes red right here.
run -- --normalize-remote "https://git.example.invalid:４４３/o/r.git"
if [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "ASCII digits only"; then ok
else bad "full-width-digit port on a VALID host must be rejected with the port reason (rc=$RC out=$OUT)"; fi
for U in "https://éxample.invalid/o/r.git" "https://例え.テスト/o/r.git" "https://full－width.invalid/o/r.git" "https://mosaic－stack.dev:４４３/o/r.git"; do
  run -- --normalize-remote "$U"
  if [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "VALIDATION_ERROR canonical_remote"; then ok
  else bad "non-ASCII authority must be rejected: $U (rc=$RC out=$OUT)"; fi
done
run -- --normalize-remote "https://xn--xample-9ua.invalid/o/r.git"
[ "$RC" = 0 ] && [ "$OUT" = "https://xn--xample-9ua.invalid/o/r" ] && ok || bad "punycode xn-- host must stay legal ($OUT)"
echo "== (9c) bracket-payload grammar + raw control bytes (R5-B1) =="
for P in "v1. " "v1.a b" "v1.a^b" "v1.a\\b" "v1.%20" "not-an-ip" "::gg::1"; do
  run -- --normalize-remote "https://[$P]/o/r.git"
  if [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "VALIDATION_ERROR canonical_remote"; then ok
  else bad "bracket payload [$P] must be rejected (rc=$RC out=$OUT)"; fi
done
for CB in $'\t' $'\n' $'\r'; do
  run -- --normalize-remote "https://[v1.a${CB}b]/o/r.git"
  if [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "control byte"; then ok
  else bad "raw control byte must be rejected before urlsplit (rc=$RC out=$OUT)"; fi
done
run -- --normalize-remote "https://[v1.fe80%zone]/o/r.git"
[ "$RC" = 1 ] && ok || bad "percent (not in RFC host grammar) must be rejected ($OUT)"
run -- --normalize-remote "https://[fe80::1%eth0]/o/r.git"
[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "canonical_remote" && ok || bad "IPv6 zone-id (not RFC host grammar) must be rejected ($OUT)"

echo "== (10) root gate: every v2 managed validation fails closed (F1) =="
# NOTE (spec §4.5): host:/ paths resolve UNDER MOSAIC_HOST_ROOT by construction,
# so tool-managed is not declarable today — the cross-check fails for every
# host:/ root until the anchor scheme grows an outside-root form (J3 era).
expect_err j1 "MOSAIC_HOST_ROOT" MOSAIC_HOST_ROOT= -- "$SB/stack.json"
expect_err j2 "MOSAIC_HOST_ROOT" MOSAIC_HOST_ROOT= -- "$SB/brain.json"
expect_err j3 "MOSAIC_HOST_ROOT" MOSAIC_HOST_ROOT= -- "$SB/stack.json"
# rider (T51P2R2): genuine ABSENCE, not just explicitly empty — captured via
# command substitution, asserted in the parent shell (no subshell-counter shape).
OUTU=$(cd "$SB" && env -u MOSAIC_HOST_ROOT bash "$V" "$SB/stack.json" 2>&1 </dev/null; echo "__RC__$?")
RCU=${OUTU##*__RC__}
if [ "$RCU" = 1 ] && printf '%s' "$OUTU" | grep -q "VALIDATION_ERROR.*MOSAIC_HOST_ROOT"; then ok
else bad "unset-by-absence root must fail closed in managed mode (rc=$RCU)"; fi
run MOSAIC_HOST_ROOT= -- --mode display "$SB/stack.json"
if [ "$RC" = 0 ] && printf '%s' "$OUT" | grep -q '^OK' && printf '%s' "$OUT" | grep -q "host root unset"; then ok
else bad "display-mode unset must pass with the specified warning (rc=$RC)"; fi
run MOSAIC_HOST_ROOT= -- --mode display "$SB/brain.json"
if [ "$RC" = 0 ] && printf '%s' "$OUT" | grep -q "host root unset"; then ok
else bad "display-mode warn missing for brain fixture"; fi
TM='{"schema_version":2,"integration_trunk":"next","release_branch":"main","flow":"trunk-release","canonical_remote":"https://git.mosaicstack.dev/mosaicstack/stack","canonical_clone":"host:/src/mosaic-stack","worktree_root":"host:/src/mosaic-stack-worktrees","worktree_policy":"tool-managed"}'
fx tm.json "$TM"; mkdir -p "$ROOT/src"
expect_err j4 "inside MOSAIC_HOST_ROOT" MOSAIC_HOST_ROOT=$ROOT -- "$SB/tm.json"
fx tm_noroot.json "$(printf '%s' "$TM" | python3 -c 'import json,sys; d=json.load(sys.stdin); del d["worktree_root"]; print(json.dumps(d))')"
expect_err j5 "worktree_root" MOSAIC_HOST_ROOT=$ROOT -- "$SB/tm_noroot.json"
echo "== (10b) symlink escape cannot fake outside-ness (B3 fix: lexical containment) =="
OUTSIDE="$SB/outside-target"; mkdir -p "$OUTSIDE"
ln -s "$OUTSIDE" "$ROOT/escape"
TM_ESC="${TM/host:\/src\/mosaic-stack-worktrees/host:/escape/worktrees}"
fx tmsym.json "$TM_ESC"
expect_err j6 "inside MOSAIC_HOST_ROOT" MOSAIC_HOST_ROOT=$ROOT -- "$SB/tmsym.json"

echo "== (11) mirror-path components: collision/delimiter/control fixtures (F5) =="
run -- --mirror-path git.mosaicstack.dev mosaicstack stack
[ "$RC" = 0 ] && [ "$OUT" = "projects/git.mosaicstack.dev/mosaicstack/stack/repo.json" ] && ok || bad "mirror path ok ($OUT)"
expect_err k1 "mirror-component" -- --mirror-path "git.mosaicstack.dev" "a__b" "c"
expect_err k2 "mirror-component" -- --mirror-path "git.mosaicstack.dev" "a" "b__c"
expect_err k3 "mirror-component" -- --mirror-path "git.mosaicstack.dev/x" "a" "b"
expect_err k4 "mirror-component" -- --mirror-path "git.mosaicstack.dev" "A" "B"
expect_err k5 "mirror-component" -- --mirror-path "git.mosaicstack.dev" "" "stack"
run -- --mirror-path git.mosaicstack.dev a b.c
[ "$RC" = 0 ] && [ "$OUT" = "projects/git.mosaicstack.dev/a/b.c/repo.json" ] && ok || bad "distinct path ($OUT)"
run -- --mirror-path $'git.example.invalid\n' owner repo
[ "$RC" = 1 ] && ok || bad "trailing-newline host must be rejected (fullmatch)"
run -- --mirror-path $'git.\texample' owner repo
[ "$RC" = 1 ] && ok || bad "control-byte host must be rejected"

echo "== (12) missing required keys =="
for key in release_branch canonical_clone; do
  fx miss.json "$(printf '%s' "$STACK" | python3 -c "import json,sys; d=json.load(sys.stdin); del d['$key']; print(json.dumps(d))")"
  expect_err "l-$key" "$key" MOSAIC_HOST_ROOT=$ROOT -- "$SB/miss.json"
done

echo "== (13) absent file =="
expect_err m1 "file" -- "$SB/nonexistent.json"

echo
echo "pass=$PASS fail=$FAIL"
if [ "$FAIL" -gt 0 ]; then echo "FAILED:$FAILED"; exit 1; fi
echo "ALL GREEN"
