#!/usr/bin/env bash # Phase 17 fail-closed gate: blog companions. The framework pieces in this # repository (pact menu item type contracts, signpost, seaglass, the party # hook, cabana repeater, dependent dropdown, richeditor, reorder and # singleton, the admin SPA), the four plugin repositories (Journal menu item # types, sitemap, menu, FAQ), the grzybyfunkcjonalne host and the two Nuxt # layers (menu, FAQ). # # Stages: # go go vet ./... here; the Docker-free framework suites by required # name (-short). # plugins go vet ./... in Journal, sitemap, menu, FAQ and the host; their # Docker-free suites by required name (-short). # admin the admin SPA typecheck and the vitest JSON report with required # spec files and test names. # layers pnpm test (node --test, TAP) in both layers with required test # files and names. # docs TestDocsTree, summer docs:build --check, check-admin-openapi.sh # --check, check-admin-dist.sh. # postgres Docker host only: the testcontainers suites by required name in # every Go repository. 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; a TAP run # with a "not ok", a fail, cancelled, skipped or todo count, zero passing tests # or a missing required test; or a missing required layer test file. # --self-test proves the detectors fail on planted output. # # --all runs self-test, go, plugins, admin, layers 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-phase17.sh postgres # Set PHASE17_REQUIRE_POSTGRES=1 to make --all fail instead. # # Environment overrides (default: the sibling checkouts next to this # repository): PHASE17_ROOT (this repository), JOURNAL (sm-journal-plugin), # SITEMAP (sm-sitemap-plugin), MENU (sm-menu-plugin), FAQ (sm-faq-plugin), # HOST (sm-grzybyfunkcjonalne-app), MENU_LAYER (vue-menu-layer), FAQ_LAYER # (vue-faq-layer). set -euo pipefail unset FORCE_COLOR export NO_COLOR=1 ROOT="${PHASE17_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" JOURNAL="${JOURNAL:-$ROOT/../sm-journal-plugin}" SITEMAP="${SITEMAP:-$ROOT/../sm-sitemap-plugin}" MENU="${MENU:-$ROOT/../sm-menu-plugin}" FAQ="${FAQ:-$ROOT/../sm-faq-plugin}" HOST="${HOST:-$ROOT/../sm-grzybyfunkcjonalne-app}" MENU_LAYER="${MENU_LAYER:-$ROOT/../vue-menu-layer}" FAQ_LAYER="${FAQ_LAYER:-$ROOT/../vue-faq-layer}" # --------------------------------------------------------------------------- # Required test names. Each list is run with -run '^(a|b|...)$' and every name # must pass; a name that skips or does not run fails the stage. # go stage: Docker-free framework suites (-short). SIGNPOST_TESTS=(TestSignpostSmoke TestRegistryDuplicateKey) SEAGLASS_TESTS=(TestSeaglassSmoke TestSanitizeByteStable) # plugins stage: Docker-free plugin and host suites (-short). SITEMAP_SHORT_TESTS=(TestSitemapGolden TestSitemapTiersConfig) # postgres stage: testcontainers suites. PG_CABANA_TESTS=(TestCompanionsDependentSmoke TestCompanionsRichEditorSmoke TestCompanionsRepeaterSmoke TestCompanionsReorderSmoke TestCompanionsSingletonSmoke TestPhase10OpenAPIConformance) PG_JOURNAL_TESTS=(TestMenuItemTypesSmoke) PG_SITEMAP_TESTS=(TestSitemapTracer TestSitemapImportSmoke TestSitemapAdminSmoke) PG_MENU_TESTS=(TestMenuAdminSmoke TestMenuImportSmoke TestMenuAPISmoke) PG_FAQ_TESTS=(TestFaqAdminSmoke TestFaqImportSmoke TestFaqAPISmoke) PG_HOST_TESTS=(TestCompanionsHost) # admin stage: vitest spec files and full test names (describe + it). ADMIN_FILES=( tests/form/RichEditorField.test.ts tests/form/RepeaterField.test.ts tests/form/DependentDropdown.test.ts tests/views/ReorderView.test.ts ) ADMIN_REQUIRE=( "RichEditorField RichEditorField tracer: untouched value is submitted byte-identical" "RichEditorField RichEditorField edit emits editor HTML" "RepeaterField RepeaterField tracer: add, edit nested, reorder and remove change form state only" "DependentDropdown DependentDropdown loads options for the sibling value and clears a stale value" "ReorderView ReorderView tracer: a keyboard move sends one debounced POST with the full node set" "singleton form opens the one record from settings, without Delete, and returns to the settings index" ) # layers stage: test files (must exist and match the test script's glob) and # TAP test names. MENU_LAYER_FILES=(tests/menuLinks.test.ts tests/client.test.ts) MENU_LAYER_REQUIRE=( "javascript: and data: URLs render as text with no href" "_blank links get rel noopener noreferrer" "rendered: unsafe URLs are a span without href, _blank links carry rel" "a 404 resolves to null; other errors are thrown" ) FAQ_LAYER_FILES=(tests/client.test.ts) FAQ_LAYER_REQUIRE=( "a 404 resolves to null; other errors are thrown" ) usage() { cat >&2 <<'EOF' usage: check-phase17.sh --self-test check-phase17.sh go | plugins | admin | layers | docs | postgres check-phase17.sh --go | --plugins | --admin | --layers | --docs | --postgres check-phase17.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. # REQUIRE_FILES and REQUIRE_NAMES hold one entry per line. 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 } # detect_tap reads node --test --test-reporter=tap output. Exit 1 "not ok" or # a fail/cancelled count, 2 a skip/todo directive or count, 3 no passing test, # 5 a 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) passed.add(m.group(1).replace("\\#", "#")) 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 } # detect_files fails (exit 5) unless every REQUIRE_FILES entry (one per line, # relative to the directory given) is a non-empty regular file whose name # ends in .test.ts, the glob of the layers' test script. detect_files() { python3 - "$1" <<'PY' import os, sys base = sys.argv[1] files = [n for n in os.environ.get("REQUIRE_FILES", "").split("\n") if n.strip()] missing = [f for f in files if not (f.startswith("tests/") and f.endswith(".test.ts") and os.path.isfile(os.path.join(base, f)) and os.path.getsize(os.path.join(base, f)) > 0)] if missing: print("refuse: required test files missing in " + base + ": " + ", ".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 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" } # go_named DIR PACKAGES FLAGS NAME... runs exactly the named top-level tests # and requires every one of them to pass. PACKAGES and FLAGS are word-split. go_named() { local dir="$1" pkgs="$2" flags="$3" shift 3 local names=("$@") regex regex="^($(IFS='|'; echo "${names[*]}"))\$" # shellcheck disable=SC2086 REQUIRE_TESTS="${names[*]}" go_json "$dir" $flags -count=1 -run "$regex" $pkgs } 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" } # pnpm_tap runs `pnpm -C test` (node --test, TAP) and checks the # required files on disk and the TAP output. pnpm_tap() { local dir="$1" log rc=0 dc=0 fc=0 REQUIRE_FILES="${REQUIRE_FILES:-}" detect_files "$dir" || fc=$? log="$(mktemp)" pnpm -C "$dir" test >"$log" 2>&1 || rc=$? detect_tap "$log" || dc=$? if [[ "$rc" -ne 0 || "$dc" -ne 0 || "$fc" -ne 0 ]]; then grep -E '^\s*(not ok|# )|Error' "$log" | tail -n 40 >&2 || true rm -f "$log" echo "refuse: pnpm test in $dir (test=$rc detect=$dc files=$fc)" >&2 return 1 fi echo "tap: $(grep -E '^# (tests|pass|fail) ' "$log" | tr '\n' ' ')in $dir" rm -f "$log" } 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 } expect_files() { local name="$1" want="$2" dir="$3" dc=0 detect_files "$dir" >/dev/null 2>&1 || dc=$? if [[ "$dc" -ne "$want" ]]; then echo "refuse: self-test detect_files $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":"TestRegistryDuplicateKey"}' expect_detector detect_go fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"fail","Package":"p","Test":"TestSanitizeByteStable"}' 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":"TestMenuAPISmoke"}' 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="TestSitemapGolden TestFaqAPISmoke" expect_detector detect_go missing-named 5 \ '{"Action":"pass","Package":"p","Test":"TestSitemapGolden"}' REQUIRE_TESTS="TestSitemap" expect_detector detect_go prefix-is-not-the-test 5 \ '{"Action":"pass","Package":"p","Test":"TestSitemapGolden"}' REQUIRE_TESTS="TestRepeater" expect_detector detect_go subtest-is-not-the-test 5 \ '{"Action":"pass","Package":"p","Test":"TestRepeaterSave/TestRepeater"} {"Action":"pass","Package":"p","Test":"TestOther"}' local vok='{"success":true,"numPassedTests":2,"numFailedTests":0,"numFailedTestSuites":0,"numPendingTests":0,"numTodoTests":0,"testResults":[{"name":"/r/tests/form/RichEditorField.test.ts","status":"passed","assertionResults":[{"fullName":"RichEditorField RichEditorField tracer: untouched value is submitted byte-identical","status":"passed"},{"fullName":"RichEditorField RichEditorField edit emits editor HTML","status":"passed"}]}]}' expect_detector detect_vitest pass 0 "$vok" REQUIRE_FILES='tests/form/RichEditorField.test.ts' REQUIRE_NAMES='RichEditorField RichEditorField tracer: untouched value is submitted byte-identical' 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/views/ReorderView.test.ts' expect_detector detect_vitest missing-file 5 "$vok" REQUIRE_NAMES='missing case' expect_detector detect_vitest missing-name 5 "$vok" local tok='TAP version 13 # Subtest: javascript: and data: URLs render as text with no href ok 1 - javascript: and data: URLs render as text with no href ok 2 - the allowlist takes http, https, mailto, tel, / and \# 1..2 # tests 2 # suites 0 # pass 2 # fail 0 # cancelled 0 # skipped 0 # todo 0' expect_detector detect_tap pass 0 "$tok" REQUIRE_NAMES=$'javascript: and data: URLs render as text with no href\nthe allowlist takes http, https, mailto, tel, / and #' expect_detector detect_tap named 0 "$tok" expect_detector detect_tap not-ok 1 'ok 1 - a not ok 2 - b # pass 1 # fail 1' expect_detector detect_tap nested-not-ok 1 ' not ok 1 - sub ok 1 - a # pass 1 # fail 0' expect_detector detect_tap fail-count 1 'ok 1 - a # pass 1 # fail 1' expect_detector detect_tap cancelled 1 'ok 1 - a # pass 1 # cancelled 1' expect_detector detect_tap skip 2 'ok 1 - a # SKIP later # pass 1' expect_detector detect_tap todo-count 2 'ok 1 - a # pass 1 # todo 1' expect_detector detect_tap zero 3 '# tests 0 # pass 0 # fail 0' expect_detector detect_tap no-output 3 '' REQUIRE_NAMES=$'javascript: and data: URLs render as text with no href\nmissing' expect_detector detect_tap missing-named 5 "$tok" local tmp tmp="$(mktemp -d)" mkdir -p "$tmp/tests" printf 'test\n' >"$tmp/tests/a.test.ts" : >"$tmp/tests/empty.test.ts" REQUIRE_FILES='tests/a.test.ts' expect_files present 0 "$tmp" || { rm -rf "$tmp"; return 1; } REQUIRE_FILES=$'tests/a.test.ts\ntests/missing.test.ts' expect_files missing 5 "$tmp" || { rm -rf "$tmp"; return 1; } REQUIRE_FILES='tests/empty.test.ts' expect_files empty 5 "$tmp" || { rm -rf "$tmp"; return 1; } REQUIRE_FILES='tests/a.ts' expect_files outside-glob 5 "$tmp" || { rm -rf "$tmp"; return 1; } rm -rf "$tmp" local mode for mode in self-test go plugins admin layers docs postgres all; do grep -q -- "^ --$mode" "${BASH_SOURCE[0]}" || { echo "refuse: missing mode --$mode" >&2 return 1 } done echo "phase17 self-test passed" } require_dir() { [[ -d "$1" ]] || { echo "refuse: $2 $1 not found" >&2 return 1 } } run_go() { (cd "$ROOT" && go vet ./...) go_named "$ROOT" ./modules/signpost -short "${SIGNPOST_TESTS[@]}" go_named "$ROOT" ./modules/seaglass -short "${SEAGLASS_TESTS[@]}" echo "phase17 go passed" } run_plugins() { require_dir "$JOURNAL" "Journal plugin" require_dir "$SITEMAP" "sitemap plugin" require_dir "$MENU" "menu plugin" require_dir "$FAQ" "FAQ plugin" require_dir "$HOST" "host application" local repo for repo in "$JOURNAL" "$SITEMAP" "$MENU" "$FAQ" "$HOST"; do go -C "$repo" vet ./... echo "vet: $repo" done go_named "$SITEMAP" ./classes -short "${SITEMAP_SHORT_TESTS[@]}" echo "phase17 plugins 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 "phase17 admin passed" } run_layers() { require_dir "$MENU_LAYER" "menu layer" require_dir "$FAQ_LAYER" "FAQ layer" REQUIRE_FILES="$(printf '%s\n' "${MENU_LAYER_FILES[@]}")" REQUIRE_NAMES="$(printf '%s\n' "${MENU_LAYER_REQUIRE[@]}")" pnpm_tap "$MENU_LAYER" REQUIRE_FILES="$(printf '%s\n' "${FAQ_LAYER_FILES[@]}")" REQUIRE_NAMES="$(printf '%s\n' "${FAQ_LAYER_REQUIRE[@]}")" pnpm_tap "$FAQ_LAYER" echo "phase17 layers passed" } run_docs() { go_named "$ROOT" ./cmd/summer "" TestDocsTree (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 "phase17 docs passed" } docker_up() { command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 } run_postgres() { require_dir "$JOURNAL" "Journal plugin" require_dir "$SITEMAP" "sitemap plugin" require_dir "$MENU" "menu plugin" require_dir "$FAQ" "FAQ plugin" require_dir "$HOST" "host application" 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 go_named "$ROOT" ./modules/cabana "-timeout 20m" "${PG_CABANA_TESTS[@]}" go_named "$JOURNAL" . "-timeout 20m" "${PG_JOURNAL_TESTS[@]}" go_named "$SITEMAP" . "-timeout 20m" "${PG_SITEMAP_TESTS[@]}" go_named "$MENU" . "-timeout 20m" "${PG_MENU_TESTS[@]}" go_named "$FAQ" . "-timeout 20m" "${PG_FAQ_TESTS[@]}" go_named "$HOST" . "-timeout 20m" "${PG_HOST_TESTS[@]}" echo "phase17 postgres passed" } run_all() { local stage for stage in self-test go plugins admin layers 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 [[ "${PHASE17_REQUIRE_POSTGRES:-0}" == "1" ]]; then echo "FAIL postgres: no reachable Docker daemon and PHASE17_REQUIRE_POSTGRES=1" exit 1 else cat >&2 <<'EOF' ************************************************************************ NOT RUN postgres: no reachable Docker daemon on this host. The testcontainers suites of the framework (cabana companions, reorder, singleton, OpenAPI conformance), Journal (menu item types), sitemap, menu, FAQ and the host (TestCompanionsHost) did NOT run. Run on a Docker host before /msd-verify-work: bash scripts/check-phase17.sh postgres ************************************************************************ EOF echo "NOT RUN postgres (Docker-host only; see the notice above)" fi echo "Phase 17 gate passed" } case "${1:---all}" in --self-test | self-test) run_self_test ;; --go | go) run_go ;; --plugins | plugins) run_plugins ;; --admin | admin) run_admin ;; --layers | layers) run_layers ;; --docs | docs) run_docs ;; --postgres | postgres) run_postgres ;; --all | all) run_all ;; *) usage ;; esac