From 9b811f6042dad68f57bb041a05f9da1f75d53218 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 11 Oct 2026 13:37:30 +0200 Subject: [PATCH] test(16.3): phase gate Add scripts/check-phase16.3.sh with fail-closed go/admin/docs/postgres stages, detector self-tests and NOT RUN postgres without Docker. Co-authored-by: Cursor --- scripts/check-phase16.3.sh | 378 +++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100755 scripts/check-phase16.3.sh diff --git a/scripts/check-phase16.3.sh b/scripts/check-phase16.3.sh new file mode 100755 index 0000000..bafe11a --- /dev/null +++ b/scripts/check-phase16.3.sh @@ -0,0 +1,378 @@ +#!/usr/bin/env bash +# Phase 16.3 fail-closed gate: personal API tokens with scopes (summercms.go +# bouncer/party/lagoon/cabana + admin SPA) and the Journal token matrix +# (sm-journal-plugin). +# +# Stages: +# go go vet in both Go repositories; the Docker-free framework and +# Journal token suites by required name (Journal root with -short). +# admin the admin SPA typecheck and full vitest suite with required +# account token case names. +# docs TestDocsTree, summer docs:build --check, check-admin-openapi.sh +# --check, check-admin-dist.sh. +# postgres Docker host only: lagoon migration, cabana account/CLI/guard and +# OpenAPI conformance, Journal token matrix. Without a reachable +# Docker daemon this stage fails. +# +# Every stage exits non-zero on a failing command; a go test -json run that +# fails, skips, matches zero tests, prints "no tests to run" or misses a named +# required test; a vitest JSON report with a failed, skipped or todo test, a +# failed file, no passing test or a missing required file or test. --self-test +# proves the detectors fail on planted output. +# +# --all runs self-test, go, admin and docs, then postgres when a Docker daemon +# answers. When none does, --all says so loudly (NOT RUN postgres) and the +# Docker suites stay a manual step for a Docker host: +# bash scripts/check-phase16.3.sh postgres +# Set PHASE163_REQUIRE_POSTGRES=1 to make --all fail instead. +# +# Environment overrides: PHASE163_ROOT (this repository), JOURNAL (default +# ../sm-journal-plugin). +set -euo pipefail + +unset FORCE_COLOR +export NO_COLOR=1 + +ROOT="${PHASE163_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +JOURNAL="${JOURNAL:-$ROOT/../sm-journal-plugin}" + +# Framework Docker-free tests that must pass by name. +FRAMEWORK_RUN='^(TestPrincipalScopeNotSerialized)$' +FRAMEWORK_REQUIRE=(TestPrincipalScopeNotSerialized) +CABANA_RUN='^(TestScopeMatch|TestAllowsScope|TestTokenScopeCompile|TestScopeOverlap|TestScopeCoversAll|TestAPITokenMint|TestParseTokenExpiry|TestValidateTokenRequest|TestBackendTokenGuard|TestTokenLastUsedDebounce|TestResolveBackend|TestTokenRouteGates|TestInteractiveOnly|TestTokenCommandsRegistered|TestTokenTracerSmoke|TestTokenGatesSmoke)$' +CABANA_REQUIRE=( + TestScopeMatch TestAllowsScope TestTokenScopeCompile TestScopeOverlap TestScopeCoversAll + TestAPITokenMint TestParseTokenExpiry TestValidateTokenRequest + TestBackendTokenGuard TestTokenLastUsedDebounce TestResolveBackend + TestTokenRouteGates TestInteractiveOnly TestTokenCommandsRegistered + TestTokenTracerSmoke TestTokenGatesSmoke +) +CMD_RUN='^(TestDocsTree)$' +CMD_REQUIRE=(TestDocsTree) +JOURNAL_SHORT_RUN='^(TestJournalTokenScopes)$' +JOURNAL_SHORT_REQUIRE=(TestJournalTokenScopes) + +# Docker-host suites. +PG_LAGOON_RUN='^(TestBackendAPITokensMigration|TestBackendAdminMetadataColumn|TestPhase09MigrationsFreshRollback)$' +PG_LAGOON_REQUIRE=(TestBackendAPITokensMigration TestBackendAdminMetadataColumn TestPhase09MigrationsFreshRollback) +PG_CABANA_RUN='^(TestAccountTokens|TestTokenCommands|TestBackendTokenPostgres|TestPhase10OpenAPIConformance)$' +PG_CABANA_REQUIRE=(TestAccountTokens TestTokenCommands TestBackendTokenPostgres TestPhase10OpenAPIConformance) +PG_JOURNAL_RUN='^(TestJournalTokens|TestJournalTokenTracer|TestJournalTokenExpansion)$' +PG_JOURNAL_REQUIRE=(TestJournalTokens TestJournalTokenTracer TestJournalTokenExpansion) + +ADMIN_FILES=( + tests/account/ApiTokensPanel.test.ts + tests/account/ApiTokenCreateDialog.test.ts +) +ADMIN_REQUIRE=( + "ApiTokensPanel tracer: list, create, show once, hide" + "ApiTokensPanel list states shows skeleton then the empty block" + "ApiTokensPanel list states shows the alert and hides the toggle when the load fails" + "ApiTokensPanel row anatomy renders name, hint aria-label, chips and meta line order" + "ApiTokensPanel revoke runs DELETE through the shared confirm and toasts success" + "ApiTokenCreateDialog plaintext lifetime clears plaintext from the DOM and toasts after Done (hide)" + "ApiTokenCreateDialog plaintext lifetime clears plaintext after Esc-confirm close and reopening starts at step 1" + "ApiTokenCreateDialog plaintext lifetime copy success announces; clipboard failure selects and alerts" + "ApiTokenCreateDialog plaintext lifetime Esc before copying asks and cancel keeps step 2" + "ApiTokenCreateDialog step 1 focuses the name input on open" + "ApiTokenCreateDialog step 1 star checked disables others; unchecking restores without picks; no preselection" + "ApiTokenCreateDialog step 1 busy disables fieldset, buttons, Esc and overlay and ignores a second submit" + "ApiTokenCreateDialog step 2 copy success flashes for two seconds and announces politely" + "ApiTokenCreateDialog step 2 focuses Copy token when entering step 2" +) + +usage() { + cat >&2 <<'EOF' +usage: + check-phase16.3.sh --self-test + check-phase16.3.sh go | admin | docs | postgres + check-phase16.3.sh --go | --admin | --docs | --postgres + check-phase16.3.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. +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" and "no tests to run" in text: + print(f"refuse: no tests to run in {pkg}", file=sys.stderr) + sys.exit(3) + 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_vitest reads a vitest --reporter=json report. Exit 1 a failed test +# or file or an unsuccessful run, 2 a skipped or todo test, 3 no passing +# test, 4 an unreadable report, 5 a missing required file or test. +detect_vitest() { + python3 - "$1" <<'PY' +import json, os, sys +path = sys.argv[1] +files = [n for n in os.environ.get("REQUIRE_FILES", "").split("\n") if n.strip()] +names = [n for n in os.environ.get("REQUIRE_NAMES", "").split("\n") if n.strip()] +try: + with open(path, encoding="utf-8") as fh: + report = json.load(fh) +except (OSError, ValueError) as err: + print(f"refuse: unreadable vitest report: {err}", file=sys.stderr) + sys.exit(4) +results = report.get("testResults") or [] +bad = [r.get("name", "?") for r in results if r.get("status") != "passed"] +if bad or report.get("numFailedTests", 0) or report.get("numFailedTestSuites", 0) or report.get("success") is not True: + print("refuse: failed " + ", ".join(bad or ["run"]), file=sys.stderr) + sys.exit(1) +if report.get("numPendingTests", 0) or report.get("numTodoTests", 0): + print(f"refuse: {report.get('numPendingTests', 0)} skipped, {report.get('numTodoTests', 0)} todo", file=sys.stderr) + sys.exit(2) +passed = set() +by_file = {} +for r in results: + for a in r.get("assertionResults") or []: + status = a.get("status") + if status != "passed": + print(f"refuse: {status} {a.get('fullName')}", file=sys.stderr) + sys.exit(2 if status in ("skipped", "pending", "todo") else 1) + passed.add(a.get("fullName", "")) + by_file[r.get("name", "")] = by_file.get(r.get("name", ""), 0) + 1 +if report.get("numPassedTests", 0) < 1 or not passed: + print("refuse: no passing tests", file=sys.stderr) + sys.exit(3) +missing = [f for f in files if not any(name.endswith("/" + f) and count > 0 for name, count in by_file.items())] +missing += [n for n in names if n not in passed] +if missing: + print("refuse: required files or tests did not pass: " + "; ".join(missing), file=sys.stderr) + sys.exit(5) +print(f"vitest: {report.get('numPassedTests')} passed in {len(results)} files") +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 + python3 -c 'import json,sys +for raw in open(sys.argv[1], errors="replace"): + try: ev = json.loads(raw) + except ValueError: continue + if ev.get("Action") == "output" and ("FAIL" in ev.get("Output", "") or "_test.go:" in ev.get("Output", "")): + sys.stderr.write(ev["Output"])' "$log" 2>&1 | tail -n 40 >&2 || true + rm -f "$log" "$err" + echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2 + return 1 + fi + echo "go: $(grep -c '"Action":"pass","Package":"[^"]*","Test":"[^"/]*","Elapsed"' "$log") top-level tests passed in $dir ($*)" + rm -f "$log" "$err" +} + +vitest_json() { + local dir="$1" report out rc=0 dc=0 + report="$(mktemp)" + out="$(mktemp)" + (cd "$dir" && ./node_modules/.bin/vitest run --reporter=json --outputFile="$report") >"$out" 2>&1 || rc=$? + detect_vitest "$report" || dc=$? + if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then + grep -E 'FAIL|Error|×' "$out" | tail -n 40 >&2 || true + rm -f "$report" "$out" + echo "refuse: vitest in $dir (test=$rc detect=$dc)" >&2 + return 1 + fi + rm -f "$report" "$out" +} + +expect_detector() { + local detector="$1" name="$2" want="$3" payload="$4" log dc=0 + log="$(mktemp)" + printf '%s\n' "$payload" >"$log" + "$detector" "$log" >/dev/null 2>&1 || dc=$? + rm -f "$log" + if [[ "$dc" -ne "$want" ]]; then + echo "refuse: self-test $detector $name: exit $dc, want $want" >&2 + return 1 + fi +} + +run_self_test() { + bash -n "${BASH_SOURCE[0]}" + + expect_detector detect_go pass 0 '{"Action":"pass","Package":"p","Test":"TestScopeMatch"}' + expect_detector detect_go fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} +{"Action":"fail","Package":"p","Test":"TestBackendTokenGuard"}' + expect_detector detect_go package-fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} +{"Action":"fail","Package":"p"}' + expect_detector detect_go build 1 '{"Action":"build-fail","ImportPath":"p"}' + expect_detector detect_go skip 2 '{"Action":"pass","Package":"p","Test":"TestA"} +{"Action":"skip","Package":"p","Test":"TestAccountTokens"}' + expect_detector detect_go zero 3 '{"Action":"pass","Package":"p"}' + expect_detector detect_go no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"} +{"Action":"pass","Package":"p"}' + expect_detector detect_go nonjson 4 '{"Action":"pass",' + REQUIRE_TESTS="TestScopeMatch TestJournalTokens" expect_detector detect_go missing-named 5 \ + '{"Action":"pass","Package":"p","Test":"TestScopeMatch"}' + REQUIRE_TESTS="TestScope" expect_detector detect_go prefix-is-not-the-test 5 \ + '{"Action":"pass","Package":"p","Test":"TestScopeMatch"}' + + local vok='{"success":true,"numPassedTests":2,"numFailedTests":0,"numFailedTestSuites":0,"numPendingTests":0,"numTodoTests":0,"testResults":[{"name":"/r/tests/account/ApiTokensPanel.test.ts","status":"passed","assertionResults":[{"fullName":"ApiTokensPanel tracer: list, create, show once, hide","status":"passed"},{"fullName":"ApiTokenCreateDialog plaintext lifetime clears plaintext from the DOM and toasts after Done (hide)","status":"passed"}]}]}' + expect_detector detect_vitest pass 0 "$vok" + REQUIRE_FILES='tests/account/ApiTokensPanel.test.ts' REQUIRE_NAMES='ApiTokensPanel tracer: list, create, show once, hide' expect_detector detect_vitest named 0 "$vok" + expect_detector detect_vitest failed-test 1 '{"success":false,"numPassedTests":1,"numFailedTests":1,"testResults":[{"name":"/r/a.test.ts","status":"failed","assertionResults":[{"fullName":"x","status":"failed"}]}]}' + expect_detector detect_vitest failed-file 1 '{"success":false,"numPassedTests":0,"numFailedTests":0,"numFailedTestSuites":1,"testResults":[{"name":"/r/a.test.ts","status":"failed","assertionResults":[]}]}' + expect_detector detect_vitest unsuccessful 1 '{"success":false,"numPassedTests":1,"numFailedTests":0,"testResults":[{"name":"/r/a.test.ts","status":"passed","assertionResults":[{"fullName":"x","status":"passed"}]}]}' + expect_detector detect_vitest skipped 2 '{"success":true,"numPassedTests":1,"numPendingTests":1,"testResults":[{"name":"/r/a.test.ts","status":"passed","assertionResults":[{"fullName":"x","status":"passed"},{"fullName":"y","status":"skipped"}]}]}' + expect_detector detect_vitest todo 2 '{"success":true,"numPassedTests":1,"numTodoTests":1,"testResults":[{"name":"/r/a.test.ts","status":"passed","assertionResults":[{"fullName":"x","status":"passed"}]}]}' + expect_detector detect_vitest zero 3 '{"success":true,"numPassedTests":0,"testResults":[]}' + expect_detector detect_vitest unreadable 4 'not json' + REQUIRE_FILES='tests/account/ApiTokenCreateDialog.test.ts' expect_detector detect_vitest missing-file 5 "$vok" + REQUIRE_NAMES='missing case' expect_detector detect_vitest missing-name 5 "$vok" + + local mode + for mode in self-test go admin docs postgres all; do + grep -q -- "^ --$mode" "${BASH_SOURCE[0]}" || { + echo "refuse: missing mode --$mode" >&2 + return 1 + } + done + echo "phase16.3 self-test passed" +} + +require_dir() { + [[ -d "$1" ]] || { + echo "refuse: $2 $1 not found" >&2 + return 1 + } +} + +run_go() { + require_dir "$JOURNAL" "Journal plugin" + (cd "$ROOT" && go vet ./modules/bouncer ./modules/party ./modules/lagoon ./modules/cabana ./cmd/summer) + REQUIRE_TESTS="${FRAMEWORK_REQUIRE[*]}" go_json "$ROOT" -count=1 -run "$FRAMEWORK_RUN" ./modules/bouncer + REQUIRE_TESTS= go_json "$ROOT" -count=1 ./modules/party + REQUIRE_TESTS="${CABANA_REQUIRE[*]}" go_json "$ROOT" -count=1 -run "$CABANA_RUN" ./modules/cabana + REQUIRE_TESTS="${CMD_REQUIRE[*]}" go_json "$ROOT" -count=1 -run "$CMD_RUN" ./cmd/summer + go -C "$JOURNAL" vet ./... + REQUIRE_TESTS="${JOURNAL_SHORT_REQUIRE[*]}" go_json "$JOURNAL" -short -count=1 -run "$JOURNAL_SHORT_RUN" . + echo "phase16.3 go passed" +} + +run_admin() { + (cd "$ROOT/admin" && ./node_modules/.bin/vue-tsc --noEmit) + REQUIRE_FILES="$(printf '%s\n' "${ADMIN_FILES[@]}")" REQUIRE_NAMES="$(printf '%s\n' "${ADMIN_REQUIRE[@]}")" vitest_json "$ROOT/admin" + echo "phase16.3 admin passed" +} + +run_docs() { + REQUIRE_TESTS="TestDocsTree" go_json "$ROOT" -count=1 -run '^TestDocsTree$' ./cmd/summer + (cd "$ROOT" && go run ./cmd/summer docs:build --check) + bash "$ROOT/scripts/check-admin-openapi.sh" --check + bash "$ROOT/scripts/check-admin-dist.sh" + echo "phase16.3 docs passed" +} + +docker_up() { + command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 +} + +run_postgres() { + require_dir "$JOURNAL" "Journal plugin" + if ! docker_up; then + echo "refuse: the postgres stage needs a reachable Docker daemon (testcontainers); run it on a Docker host" >&2 + return 1 + fi + REQUIRE_TESTS="${PG_LAGOON_REQUIRE[*]}" go_json "$ROOT" -count=1 -timeout 15m -run "$PG_LAGOON_RUN" ./modules/lagoon + REQUIRE_TESTS="${PG_CABANA_REQUIRE[*]}" go_json "$ROOT" -count=1 -timeout 15m -run "$PG_CABANA_RUN" ./modules/cabana + REQUIRE_TESTS="${PG_JOURNAL_REQUIRE[*]}" go_json "$JOURNAL" -count=1 -timeout 20m -run "$PG_JOURNAL_RUN" . + echo "phase16.3 postgres passed" +} + +run_all() { + local stage + for stage in self-test go admin docs; do + if bash "${BASH_SOURCE[0]}" "--$stage"; then + echo "PASS $stage" + else + echo "FAIL $stage" + exit 1 + fi + done + if docker_up; then + if bash "${BASH_SOURCE[0]}" --postgres; then + echo "PASS postgres" + else + echo "FAIL postgres" + exit 1 + fi + elif [[ "${PHASE163_REQUIRE_POSTGRES:-0}" == "1" ]]; then + echo "FAIL postgres: no reachable Docker daemon and PHASE163_REQUIRE_POSTGRES=1" + exit 1 + else + cat >&2 <<'EOF' +************************************************************************ +NOT RUN postgres: no reachable Docker daemon on this host. +The testcontainers suites (TestBackendAPITokensMigration, +TestAccountTokens, TestTokenCommands, TestBackendTokenPostgres, +TestPhase10OpenAPIConformance, TestJournalTokens, TestJournalTokenTracer, +TestJournalTokenExpansion) did NOT run. +Run on a Docker host before /msd-verify-work: + bash scripts/check-phase16.3.sh postgres +************************************************************************ +EOF + echo "NOT RUN postgres (Docker-host only; see the notice above)" + fi + echo "Phase 16.3 gate passed" +} + +case "${1:---all}" in + --self-test | self-test) run_self_test ;; + --go | go) run_go ;; + --admin | admin) run_admin ;; + --docs | docs) run_docs ;; + --postgres | postgres) run_postgres ;; + --all | all) run_all ;; + *) usage ;; +esac