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