diff --git a/scripts/check-phase14.2.sh b/scripts/check-phase14.2.sh new file mode 100755 index 0000000..b2ef791 --- /dev/null +++ b/scripts/check-phase14.2.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +# Phase 14.2 fail-closed gate (the local fonoteka frontend on the local +# SummerCMS backend). Every stage exits non-zero on a failing command, a go +# test run that fails, skips, matches zero tests, prints "no tests to run", +# a named required test that did not pass or a data race. +# +# Modes: +# --self-test proves the go test detector fails closed on planted inputs +# --go the phase's named Go tests through the detector +# --all every stage in order; the last line is "Phase 14.2 gate passed" +# +# The fonoteka.go workspace is invoked with `go -C`. Postgres integration +# runs through testcontainers; an unavailable Docker daemon fails the gate, +# and -short is never final evidence. Go runs with TMPDIR on the +# workspace's device (tmp/dev-stack/go-tmp), because a tmpfs /tmp breaks +# the build cache and gocloud fileblob renames with a cross-device link. +set -euo pipefail + +unset FORCE_COLOR + +ROOT="${PHASE142_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +APP="${PHASE142_APP:-$ROOT/../fonoteka.go}" +NUXT="${PHASE142_NUXT:-/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app}" + +export TMPDIR="${PHASE142_TMPDIR:-$APP/tmp/dev-stack/go-tmp}" +mkdir -p "$TMPDIR" + +CONSOLE_REQUIRE=(TestDevSeedRefusesProduction) + +usage() { + cat >&2 <<'EOF' +usage: + check-phase14.2.sh --self-test + check-phase14.2.sh --go + check-phase14.2.sh --all +EOF + exit 2 +} + +# detect reads go test -json. Exit 1 fail/build, 2 skip, 3 zero/no-tests, +# 4 non-JSON, 5 missing required name, 6 data race. +detect() { + python3 - "$1" <<'PY' +import json, os, sys +path = sys.argv[1] +require = [n for n in os.environ.get("REQUIRE_TESTS", "").split() if n] +passed = set() +failed = [] +with open(path, encoding="utf-8", errors="replace") as fh: + for raw in fh: + line = raw.strip() + if not line.startswith("{"): + continue + try: + ev = json.loads(line) + except json.JSONDecodeError: + print("refuse: non-json test output", file=sys.stderr) + sys.exit(4) + action = ev.get("Action") + test = ev.get("Test") or "" + pkg = ev.get("Package") or ev.get("ImportPath") or "" + if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")): + print(f"refuse: build failed {pkg}", file=sys.stderr) + sys.exit(1) + text = ev.get("Output") or "" + if action == "output": + if "no tests to run" in text: + print(f"refuse: no tests to run in {pkg}", file=sys.stderr) + sys.exit(3) + if "WARNING: DATA RACE" in text: + print(f"refuse: data race in {pkg} {test}", file=sys.stderr) + sys.exit(6) + if action == "skip" and test: + print(f"refuse: skipped {pkg} {test}", file=sys.stderr) + sys.exit(2) + if action == "fail": + failed.append(f"{pkg} {test}".strip()) + if action == "pass" and test: + passed.add(test) +if failed: + print("refuse: failed " + ", ".join(failed), file=sys.stderr) + sys.exit(1) +if not passed: + print("refuse: zero tests", file=sys.stderr) + sys.exit(3) +top = {name for name in passed if "/" not in name} +missing = [n for n in require if n not in top and not any(p.startswith(n + "/") or p == n for p in passed)] +if missing: + print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr) + sys.exit(5) +PY +} + +go_json() { + local dir="$1" + shift + local log err rc=0 dc=0 + log="$(mktemp)" + err="$(mktemp)" + (cd "$dir" && go test -json "$@") >"$log" 2>"$err" || rc=$? + detect "$log" || dc=$? + if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then + cat "$err" >&2 || true + grep -v '^{' "$log" | tail -n 40 >&2 || true + rm -f "$log" "$err" + echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2 + return 1 + fi + rm -f "$log" "$err" +} + +expect_detect() { + local name="$1" want="$2" payload="$3" log dc=0 + log="$(mktemp)" + printf '%s\n' "$payload" >"$log" + detect "$log" 2>/dev/null || dc=$? + rm -f "$log" + if [[ "$dc" -ne "$want" ]]; then + echo "refuse: self-test $name: detector exit $dc, want $want" >&2 + return 1 + fi +} + +# run_regex builds an anchored -run alternation from test names. +run_regex() { + local IFS='|' + printf '^(%s)$' "$*" +} + +run_self_test() { + bash -n "${BASH_SOURCE[0]}" + expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestDevSeedRefusesProduction"}' + expect_detect fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} +{"Action":"fail","Package":"p","Test":"TestDevSeedShape"}' + expect_detect package-fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} +{"Action":"fail","Package":"p"}' + expect_detect build 1 '{"Action":"build-fail","ImportPath":"p"}' + expect_detect skip 2 '{"Action":"skip","Package":"p","Test":"TestUploadsMount"}' + expect_detect zero 3 '{"Action":"pass","Package":"p"}' + expect_detect no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"} +{"Action":"pass","Package":"p"}' + expect_detect nonjson 4 '{"Action":"pass",' + expect_detect race 6 '{"Action":"output","Package":"p","Test":"TestA","Output":"WARNING: DATA RACE\n"} +{"Action":"pass","Package":"p","Test":"TestA"}' + REQUIRE_TESTS="TestDevSeedRefusesProduction TestDevMarkerRoute" expect_detect missing-named 5 \ + '{"Action":"pass","Package":"p","Test":"TestDevSeedRefusesProduction"}' + local flag + for flag in --self-test --go --all; do + grep -q -- "^ $flag)" "${BASH_SOURCE[0]}" || { + echo "refuse: missing mode $flag" >&2 + return 1 + } + done + echo "phase14.2 self-test passed" +} + +run_go() { + [[ -d "$APP" ]] || { + echo "refuse: fonoteka.go workspace $APP not found" >&2 + return 1 + } + go -C "$APP" vet ./plugins/golem15/fonoteka/... + REQUIRE_TESTS="${CONSOLE_REQUIRE[*]}" go_json "$APP" ./plugins/golem15/fonoteka/console \ + -count=1 -timeout 20m -run "$(run_regex "${CONSOLE_REQUIRE[@]}")" + echo "phase14.2 go passed" +} + +run_all() { + local stage + for stage in self-test go; do + if bash "${BASH_SOURCE[0]}" "--$stage"; then + echo "PASS $stage" + else + echo "FAIL $stage" + exit 1 + fi + done + echo "Phase 14.2 gate passed" +} + +case "${1:---all}" in + --self-test) run_self_test ;; + --go) run_go ;; + --all) run_all ;; + *) usage ;; +esac