#!/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 &1