#!/usr/bin/env bash # Phase 16 fail-closed gate (vue-journal-layer, the grzybyfunkcjonalne.pl site # app and its SummerCMS host). Every stage exits non-zero on a failing # command; a node --test run that reports "not ok", a failed, skipped, todo or # cancelled test, no passing test or a missing named test; a go test run that # fails, skips, matches zero tests, prints "no tests to run", misses a named # required test or reports a data race; or an SSR smoke that does not end ok. # --self-test proves both detectors fail closed on planted inputs. --all runs # every stage and must end with "Phase 16 gate passed". # # The grzyby command tests need Postgres: GRZYBY_TEST_ADMIN_DSN (a # maintenance-database DSN whose role has CREATEDB) or Docker for # testcontainers. Without either they fail; -short is not final evidence. # # Environment overrides: PHASE16_LAYER (default ../vue-journal-layer), # PHASE16_HOST (default ../sm-grzybyfunkcjonalne-app), PHASE16_APP (default # $PHASE16_HOST/vue-grzybyfunkcjonalne-app). set -euo pipefail unset FORCE_COLOR ROOT="${PHASE16_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" LAYER="${PHASE16_LAYER:-$ROOT/../vue-journal-layer}" HOST="${PHASE16_HOST:-$ROOT/../sm-grzybyfunkcjonalne-app}" APP="${PHASE16_APP:-$HOST/vue-grzybyfunkcjonalne-app}" PLUGIN="$HOST/plugins/grzyby" # One named test per layer test file. LAYER_REQUIRE=( "an override replaces one key and keeps the others" "a post path without :slug throws naming journal.routes.post" "listCategories always sends include_empty=1" "getPost returns null for status 404 and statusCode 404" "anything else is a 404 (null)" "Go case vbscript is unsafe" "Go case data in upper case is unsafe" "a custom root category route gives /cordyceps" "without an excerpt the description is the first 160 characters of the body text" "pl and en have identical leaf key sets" ) APP_REQUIRE=( "D-15: every PHP URL form answers the expected status" "static pages win over the root category route (RESEARCH A2)" "D-16: Polish only, without a prefix" "T-16-01: an unsafe post body renders the blocked notice only" "Pitfall 5: SSR fetches forward the visitor X-Forwarded-For" "D-23: NUXT_PUBLIC_GA_ID loads gtag for its id" "D-23: a malformed NUXT_PUBLIC_GA_ID loads no script (T-16-07)" "malformed ids give no scripts" "the FAQ has the 16 live entries with question and answer" "every t('…') key used in app/**/*.vue exists in pl.json or the layer's journal strings" ) HOST_REQUIRE=( TestBootUserTranslateJournal TestCORS TestBackendURIGfadmin TestGrzybyCommandsRegistered ) PLUGIN_REQUIRE=( TestReadFeaturedImageMap TestValidDiskName TestFeaturedImageSourceEscape TestImportFeaturedImages TestImportFeaturedImagesDryRun TestReadCategoryMap TestImportCategories ) usage() { cat >&2 <<'EOF' usage: check-phase16.sh --self-test check-phase16.sh --layer check-phase16.sh --layer-smoke check-phase16.sh --app check-phase16.sh --host check-phase16.sh --plugin check-phase16.sh --deploy check-phase16.sh --all EOF exit 2 } # detect_go 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_go() { 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) missing = [n for n in require if n not in passed] if missing: print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr) sys.exit(5) PY } # detect_tap reads node --test --test-reporter=tap output. Exit 1 "not ok" or # a fail/cancelled count, 2 skip/todo, 3 no pass count or zero passes, 5 # missing required name. REQUIRE_NAMES holds one test name per line. detect_tap() { python3 - "$1" <<'PY' import os, re, sys path = sys.argv[1] require = [n for n in os.environ.get("REQUIRE_NAMES", "").split("\n") if n.strip()] passed = set() counts = {} with open(path, encoding="utf-8", errors="replace") as fh: for raw in fh: line = raw.rstrip("\n") body = line.lstrip() if body.startswith("not ok"): print(f"refuse: {body}", file=sys.stderr) sys.exit(1) m = re.match(r"ok \d+ - (.*?)(\s+#\s*(SKIP|TODO)\b.*)?$", body, re.I) if m: if m.group(2): print(f"refuse: {body}", file=sys.stderr) sys.exit(2) if line == body: passed.add(m.group(1)) continue m = re.match(r"# (tests|suites|pass|fail|cancelled|skipped|todo) (\d+)$", line) if m: counts[m.group(1)] = int(m.group(2)) for key in ("fail", "cancelled"): if counts.get(key, 0) > 0: print(f"refuse: {counts[key]} {key}", file=sys.stderr) sys.exit(1) for key in ("skipped", "todo"): if counts.get(key, 0) > 0: print(f"refuse: {counts[key]} {key}", file=sys.stderr) sys.exit(2) if counts.get("pass", 0) < 1 or not passed: print("refuse: no passing tests", file=sys.stderr) sys.exit(3) missing = [n for n in require if n not 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_go "$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" } # node_tap runs `pnpm -C