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 <cursoragent@cursor.com>
This commit is contained in:
2026-10-11 13:37:30 +02:00
parent 55999b2984
commit 9b811f6042

378
scripts/check-phase16.3.sh Executable file
View File

@@ -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