diff --git a/scripts/check-phase14.2.sh b/scripts/check-phase14.2.sh index b2ef791..265ec27 100755 --- a/scripts/check-phase14.2.sh +++ b/scripts/check-phase14.2.sh @@ -1,19 +1,47 @@ #!/usr/bin/env bash -# Phase 14.2 fail-closed gate (the local fonoteka frontend on the local -# SummerCMS backend). Every stage exits non-zero on a failing command, a go -# test run that fails, skips, matches zero tests, prints "no tests to run", -# a named required test that did not pass or a data race. +# Phase 14.2 fail-closed gate: the local vue-fonoteka-app on the local +# fonoteka.go SummerCMS backend. Every stage exits non-zero on a failing +# command, a go test run that fails, skips, matches zero tests, prints "no +# tests to run", a named required test that did not pass or a data race. # # Modes: -# --self-test proves the go test detector fails closed on planted inputs -# --go the phase's named Go tests through the detector -# --all every stage in order; the last line is "Phase 14.2 gate passed" +# --self-test the go test detector, the Playwright verdict +# (dev-stack.sh e2e-verdict), the go.log scan and the summary +# checks fail closed on planted inputs; every mode exists +# --go go vet and go test ./... in summercms.go, in the fonoteka.go +# workspace root module and in the fonoteka plugin module, +# then the phase's named tests through the detector +# --docs TestDocsTree and summer docs:build --check +# --parity TestParityCorpus with the 175/175 summary +# --contracts the phase's Nuxt contract test (blocking) and the whole +# pnpm test:contracts suite (counts reported, not blocking: +# missing-catalog.test.mjs fails on HEAD before this phase, +# see the phase's deferred-items.md) +# --guard planted env files fed to dev-stack.sh guard: every case +# must print "refuse:" and exit non-zero; a fake SummerCMS +# marker must pass (control case) +# --e2e the running local stack (started with up --detach when it +# is not up), the guard on the real env file, the Go-gate +# Playwright specs through dev-stack.sh e2e (any skip, +# unexpected, flaky or zero expected fails), the D-15 +# report-only specs (counts printed, never blocking) and the +# Go log scan (search engine failure or a 5xx record fails) +# --all every stage in that order; the last line is +# "Phase 14.2 gate passed" # -# The fonoteka.go workspace is invoked with `go -C`. Postgres integration -# runs through testcontainers; an unavailable Docker daemon fails the gate, -# and -short is never final evidence. Go runs with TMPDIR on the -# workspace's device (tmp/dev-stack/go-tmp), because a tmpfs /tmp breaks -# the build cache and gocloud fileblob renames with a cross-device link. +# Paths: PHASE142_ROOT (this checkout), PHASE142_APP (default +# $ROOT/../fonoteka.go), PHASE142_NUXT (the vue-fonoteka-app checkout). +# Postgres integration runs through testcontainers: an unavailable Docker +# daemon fails the gate, and -short is never final evidence. A missing host +# service (Postgres, Typesense, Centrifugo) fails --e2e through the +# dev-stack.sh refusal; nothing is downgraded to a skip. +# +# TMPDIR: the gate leaves it as set by the caller (PHASE142_TMPDIR +# overrides). It must not point inside a go.work tree such as fonoteka.go: +# the scaffold tests in internal/build then edit that workspace's go.work, +# and `go test ./...` there picks up the build directories as packages. +# dev-stack.sh gives the Go processes and Playwright their own TMPDIR on the +# app's device (the file bucket renames across devices otherwise). set -euo pipefail unset FORCE_COLOR @@ -21,17 +49,39 @@ unset FORCE_COLOR ROOT="${PHASE142_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" APP="${PHASE142_APP:-$ROOT/../fonoteka.go}" NUXT="${PHASE142_NUXT:-/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app}" +DEV_STACK="$APP/scripts/dev-stack.sh" +PLUGIN_DIR="$APP/plugins/golem15/fonoteka" +GO_LOG="$APP/tmp/dev-stack/go.log" -export TMPDIR="${PHASE142_TMPDIR:-$APP/tmp/dev-stack/go-tmp}" -mkdir -p "$TMPDIR" +if [[ -n "${PHASE142_TMPDIR:-}" ]]; then + mkdir -p "$PHASE142_TMPDIR" + export TMPDIR="$PHASE142_TMPDIR" +fi -CONSOLE_REQUIRE=(TestDevSeedRefusesProduction) +CONSOLE_REQUIRE=(TestDevSeedRefusesProduction TestDevSeedShape TestDevSeedIdempotent TestReindexCommand) +APP_REQUIRE=(TestDevStackSmoke TestDevSeedOpensDatabase TestReindexOpensDatabase TestCommandsOpenDatabase TestDevMarkerRoute) +FRAMEWORK_REQUIRE=(TestUploadsMount TestAccessLog TestAccessLogSmoke) +GATE_SPECS=( + auth.setup.ts login.spec.ts smoke.spec.ts fonoteka-item.spec.ts public-share.spec.ts + realtime.spec.ts i18n.spec.ts album-edit-rate-cover.spec.ts album-search.spec.ts wishlist.spec.ts +) +REPORT_SPECS=( + fonoteka-ai-assist.spec.ts fonoteka-bulk-add.spec.ts offline.spec.ts + registration-invitation.spec.ts responsive.spec.ts landing.spec.ts +) +PHASE_CONTRACTS=(tests/contracts/e2e-backend-switch.test.mjs) +PARITY_SUMMARY="recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0" usage() { cat >&2 <<'EOF' usage: check-phase14.2.sh --self-test check-phase14.2.sh --go + check-phase14.2.sh --docs + check-phase14.2.sh --parity + check-phase14.2.sh --contracts + check-phase14.2.sh --guard + check-phase14.2.sh --e2e check-phase14.2.sh --all EOF exit 2 @@ -91,6 +141,8 @@ if missing: PY } +# go_json DIR ARGS... runs go test -json in DIR through detect. With +# GO_JSON_KEEP set the raw log is kept at that path. go_json() { local dir="$1" shift @@ -106,7 +158,12 @@ go_json() { echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2 return 1 fi - rm -f "$log" "$err" + if [[ -n "${GO_JSON_KEEP:-}" ]]; then + mv "$log" "$GO_JSON_KEEP" + else + rm -f "$log" + fi + rm -f "$err" } expect_detect() { @@ -127,8 +184,83 @@ run_regex() { printf '^(%s)$' "$*" } +# scan_go_log FILE refuses a silent search fallback or a 5xx request record +# (research G8, D-12). A 499 (client closed the request) is not a 5xx. +scan_go_log() { + local file="$1" hits + [[ -f "$file" ]] || { + echo "refuse: Go log $file not found" >&2 + return 1 + } + hits="$(grep -nE 'album search engine failed|status=5[0-9][0-9]([^0-9]|$)' "$file" || true)" + if [[ -n "$hits" ]]; then + printf '%s\n' "$hits" | tail -n 20 >&2 + echo "refuse: $file has a search engine failure or a 5xx record (rotate the log only after reading it)" >&2 + return 1 + fi +} + +# tap_clean FILE refuses a node --test TAP summary unless it has tests > 0 +# and fail, cancelled, skipped and todo all 0. +tap_clean() { + python3 - "$1" <<'PY' +import re, sys +text = open(sys.argv[1], encoding="utf-8", errors="replace").read() +counts = {} +for key in ("tests", "pass", "fail", "cancelled", "skipped", "todo"): + found = re.findall(rf"^# {key} (\d+)$", text, re.M) + if not found: + print(f"refuse: TAP summary has no '# {key}' line", file=sys.stderr) + sys.exit(1) + counts[key] = int(found[-1]) +if counts["tests"] == 0 or counts["pass"] != counts["tests"]: + print(f"refuse: TAP summary {counts}", file=sys.stderr) + sys.exit(1) +for key in ("fail", "cancelled", "skipped", "todo"): + if counts[key] != 0: + print(f"refuse: TAP summary {counts}", file=sys.stderr) + sys.exit(1) +print("tests={tests} pass={pass} fail={fail} skipped={skipped}".format(**counts)) +PY +} + +expect_status() { + local name="$1" want="$2" got=0 + shift 2 + "$@" >/dev/null 2>&1 || got=$? + if { [[ "$want" == 0 ]] && [[ "$got" -ne 0 ]]; } || { [[ "$want" != 0 ]] && [[ "$got" -eq 0 ]]; }; then + echo "refuse: self-test $name: exit $got, want $([[ "$want" == 0 ]] && echo 0 || echo non-zero)" >&2 + return 1 + fi +} + +# expect_verdict NAME WANT JSON: dev-stack.sh e2e-verdict on a planted +# report must refuse (WANT=refuse, non-zero and a "refuse:" line) or pass. +expect_verdict() { + local name="$1" want="$2" payload="$3" dir out rc=0 + dir="$(mktemp -d)" + printf '%s\n' "$payload" >"$dir/report.json" + out="$(env -i PATH="$PATH" HOME="$HOME" bash "$DEV_STACK" e2e-verdict "$dir/report.json" 2>&1)" || rc=$? + rm -rf "$dir" + if [[ "$want" == refuse ]]; then + if [[ "$rc" -eq 0 ]] || ! grep -q '^refuse:' <<<"$out"; then + echo "refuse: self-test verdict $name was not refused (exit $rc): $out" >&2 + return 1 + fi + elif [[ "$rc" -ne 0 ]] || ! grep -q '^e2e-verdict: passed' <<<"$out"; then + echo "refuse: self-test verdict $name was refused (exit $rc): $out" >&2 + return 1 + fi +} + run_self_test() { bash -n "${BASH_SOURCE[0]}" + [[ -f "$DEV_STACK" ]] || { + echo "refuse: $DEV_STACK not found" >&2 + return 1 + } + bash -n "$DEV_STACK" + expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestDevSeedRefusesProduction"}' expect_detect fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"fail","Package":"p","Test":"TestDevSeedShape"}' @@ -136,6 +268,9 @@ run_self_test() { {"Action":"fail","Package":"p"}' expect_detect build 1 '{"Action":"build-fail","ImportPath":"p"}' expect_detect skip 2 '{"Action":"skip","Package":"p","Test":"TestUploadsMount"}' + expect_detect subtest-skip 2 '{"Action":"pass","Package":"p","Test":"TestUploadsMount/off"} +{"Action":"skip","Package":"p","Test":"TestUploadsMount/db"} +{"Action":"pass","Package":"p","Test":"TestUploadsMount"}' expect_detect zero 3 '{"Action":"pass","Package":"p"}' expect_detect no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"} {"Action":"pass","Package":"p"}' @@ -144,8 +279,47 @@ run_self_test() { {"Action":"pass","Package":"p","Test":"TestA"}' REQUIRE_TESTS="TestDevSeedRefusesProduction TestDevMarkerRoute" expect_detect missing-named 5 \ '{"Action":"pass","Package":"p","Test":"TestDevSeedRefusesProduction"}' + + # Playwright verdicts (T-14.2-17). + expect_verdict clean pass '{"stats":{"expected":17,"unexpected":0,"skipped":0,"flaky":0}}' + expect_verdict skipped refuse '{"stats":{"expected":16,"unexpected":0,"skipped":1,"flaky":0}}' + expect_verdict unexpected refuse '{"stats":{"expected":16,"unexpected":1,"skipped":0,"flaky":0}}' + expect_verdict flaky refuse '{"stats":{"expected":16,"unexpected":0,"skipped":0,"flaky":1}}' + expect_verdict zero-expected refuse '{"stats":{"expected":0,"unexpected":0,"skipped":0,"flaky":0}}' + expect_verdict malformed refuse '{"stats":{"expected":17,' + expect_verdict no-stats refuse '{"suites":[]}' + local out rc=0 + out="$(env -i PATH="$PATH" HOME="$HOME" bash "$DEV_STACK" e2e-verdict "$(mktemp -u)" 2>&1)" || rc=$? + if [[ "$rc" -eq 0 ]] || ! grep -q '^refuse:' <<<"$out"; then + echo "refuse: self-test verdict missing-report was not refused" >&2 + return 1 + fi + + # Go log scan (T-14.2-20) and summaries. + local dir + dir="$(mktemp -d)" + printf '%s\n' '[go] level=INFO msg="http request" method=GET pattern="GET /a" status=200 duration_ms=1' \ + '[go] level=INFO msg="http request" method=GET pattern="GET /b" status=499 duration_ms=3' >"$dir/clean.log" + printf '%s\n' '[go] level=INFO msg="http request" method=GET pattern="GET /a" status=503 duration_ms=1' >"$dir/5xx.log" + printf '%s\n' '[go] level=WARN msg="album search engine failed" err=timeout' >"$dir/search.log" + expect_status go-log-clean 0 scan_go_log "$dir/clean.log" + expect_status go-log-5xx 1 scan_go_log "$dir/5xx.log" + expect_status go-log-search 1 scan_go_log "$dir/search.log" + expect_status go-log-missing 1 scan_go_log "$dir/absent.log" + printf '# tests 9\n# pass 9\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n' >"$dir/tap-clean" + printf '# tests 9\n# pass 8\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n' >"$dir/tap-fail" + printf '# tests 9\n# pass 8\n# fail 0\n# cancelled 0\n# skipped 1\n# todo 0\n' >"$dir/tap-skip" + printf '# tests 0\n# pass 0\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n' >"$dir/tap-zero" + printf 'ok 1 - x\n' >"$dir/tap-none" + expect_status tap-clean 0 tap_clean "$dir/tap-clean" + expect_status tap-fail 1 tap_clean "$dir/tap-fail" + expect_status tap-skip 1 tap_clean "$dir/tap-skip" + expect_status tap-zero 1 tap_clean "$dir/tap-zero" + expect_status tap-none 1 tap_clean "$dir/tap-none" + rm -rf "$dir" + local flag - for flag in --self-test --go --all; do + for flag in --self-test --go --docs --parity --contracts --guard --e2e --all; do grep -q -- "^ $flag)" "${BASH_SOURCE[0]}" || { echo "refuse: missing mode $flag" >&2 return 1 @@ -155,19 +329,213 @@ run_self_test() { } run_go() { - [[ -d "$APP" ]] || { + [[ -d "$APP" && -d "$PLUGIN_DIR" ]] || { echo "refuse: fonoteka.go workspace $APP not found" >&2 return 1 } - go -C "$APP" vet ./plugins/golem15/fonoteka/... + (cd "$ROOT" && go vet ./... && go test ./... -count=1 -timeout 60m) + go -C "$APP" vet ./... + go -C "$APP" test ./... -count=1 -timeout 60m + go -C "$PLUGIN_DIR" vet ./... + go -C "$PLUGIN_DIR" test ./... -count=1 -timeout 60m REQUIRE_TESTS="${CONSOLE_REQUIRE[*]}" go_json "$APP" ./plugins/golem15/fonoteka/console \ -count=1 -timeout 20m -run "$(run_regex "${CONSOLE_REQUIRE[@]}")" + REQUIRE_TESTS="${APP_REQUIRE[*]}" go_json "$APP" ./plugins/golem15/fonoteka \ + -count=1 -timeout 20m -run "$(run_regex "${APP_REQUIRE[@]}")" + REQUIRE_TESTS="${FRAMEWORK_REQUIRE[*]}" go_json "$ROOT" ./modules/surf \ + -count=1 -timeout 20m -run "$(run_regex "${FRAMEWORK_REQUIRE[@]}")" echo "phase14.2 go passed" } +run_docs() { + REQUIRE_TESTS="TestDocsTree" go_json "$ROOT" ./cmd/summer -count=1 -run '^TestDocsTree$' + local out + out="$(cd "$ROOT" && go run ./cmd/summer docs:build --check 2>&1)" || { + echo "$out" >&2 + echo "refuse: docs:build --check failed" >&2 + return 1 + } + echo "phase14.2 docs passed" +} + +run_parity() { + local log + log="$(mktemp)" + GO_JSON_KEEP="$log" REQUIRE_TESTS="TestParityCorpus" go_json "$APP" ./parity \ + -count=1 -run '^TestParityCorpus$' -timeout 30m -v + if ! grep -qF "$PARITY_SUMMARY" "$log"; then + grep -oE 'recorded [0-9]+/[0-9]+[^"\\]*' "$log" | tail -n 3 >&2 || true + rm -f "$log" + echo "refuse: TestParityCorpus did not report \"$PARITY_SUMMARY\"" >&2 + return 1 + fi + rm -f "$log" + echo "phase14.2 parity passed ($PARITY_SUMMARY)" +} + +run_contracts() { + [[ -f "$NUXT/package.json" ]] || { + echo "refuse: Nuxt checkout $NUXT not found" >&2 + return 1 + } + local log rc=0 counts + log="$(mktemp)" + (cd "$NUXT" && node --test "${PHASE_CONTRACTS[@]}") >"$log" 2>&1 || rc=$? + if [[ "$rc" -ne 0 ]] || ! counts="$(tap_clean "$log")"; then + grep -E '^not ok|^# ' "$log" >&2 || true + rm -f "$log" + echo "refuse: phase contract tests ${PHASE_CONTRACTS[*]} (exit $rc)" >&2 + return 1 + fi + echo "contracts (blocking) ${PHASE_CONTRACTS[*]}: $counts" + # The whole suite, reported only: missing-catalog.test.mjs fails on + # vue-fonoteka-app HEAD from before this phase (deferred-items.md). + rc=0 + pnpm --dir "$NUXT" test:contracts >"$log" 2>&1 || rc=$? + echo "contracts (report only) pnpm test:contracts exit $rc: $(grep -E '^# (tests|pass|fail|skipped) ' "$log" | paste -sd ' ')" + grep -E '^not ok' "$log" | sed 's/^/contracts (report only) /' || true + rm -f "$log" + echo "phase14.2 contracts passed" +} + +# guard_case NAME WANT FILE [ENV...]: dev-stack.sh guard on a planted env +# file in a clean environment. WANT is the reason the refusal must name. +guard_case() { + local name="$1" want="$2" file="$3" out rc=0 + shift 3 + out="$(env -i PATH="$PATH" HOME="$HOME" DEV_STACK_ENV_FILE="$file" "$@" bash "$DEV_STACK" guard 2>&1)" || rc=$? + if [[ "$rc" -eq 0 ]] || ! grep -q '^refuse:' <<<"$out" || ! grep -qF -- "$want" <<<"$out"; then + echo "$out" >&2 + echo "refuse: guard case $name was not refused for \"$want\" (exit $rc)" >&2 + return 1 + fi + echo "guard $name: $(grep '^refuse:' <<<"$out" | head -n 1)" +} + +GUARD_PIDS=() +stop_fakes() { + local pid + for pid in "${GUARD_PIDS[@]}"; do + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + done + GUARD_PIDS=() +} + +# fake_backend DIR STATUS BODY [HEADER]: a python3 http.server on a free +# loopback port, recorded in GUARD_PIDS; its port goes to FAKE_PORT. +FAKE_PORT="" +fake_backend() { + local dir="$1" status="$2" body="$3" header="${4:-}" portfile i + portfile="$dir/port-$RANDOM-$RANDOM" + python3 - "$portfile" "$status" "$body" "$header" >/dev/null 2>&1 <<'PY' & +import http.server, sys +portfile, status, body, header = sys.argv[1], int(sys.argv[2]), sys.argv[3].encode(), sys.argv[4] +class H(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(status) + if header: + k, v = header.split(":", 1) + self.send_header(k.strip(), v.strip()) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + def log_message(self, *args): + pass +srv = http.server.HTTPServer(("127.0.0.1", 0), H) +with open(portfile + ".tmp", "w") as fh: + fh.write(str(srv.server_address[1])) +import os +os.rename(portfile + ".tmp", portfile) +srv.serve_forever() +PY + GUARD_PIDS+=("$!") + for ((i = 0; i < 50; i++)); do + [[ -s "$portfile" ]] && break + sleep 0.1 + done + [[ -s "$portfile" ]] || { + echo "refuse: fake backend did not start" >&2 + return 1 + } + FAKE_PORT="$(cat "$portfile")" +} + +run_guard() { + [[ -f "$DEV_STACK" ]] || { + echo "refuse: $DEV_STACK not found" >&2 + return 1 + } + local dir port + dir="$(mktemp -d)" + GUARD_DIR="$dir" + trap 'stop_fakes; rm -rf "$GUARD_DIR"' EXIT + + printf 'NUXT_DEV_BACKEND_ORIGIN=http://127.0.0.1:8422\nNUXT_INTERNAL_API_ORIGIN=http://127.0.0.1:8422\n' >"$dir/guard-php-port.conf" + guard_case php-port "port 8422" "$dir/guard-php-port.conf" + + printf 'NUXT_DEV_BACKEND_ORIGIN=http://127.0.0.1:8423\n' >"$dir/guard-internal-unset.conf" + guard_case internal-unset "NUXT_INTERNAL_API_ORIGIN is unset" "$dir/guard-internal-unset.conf" + + printf 'NUXT_DEV_BACKEND_ORIGIN=http://127.0.0.1:8423\nNUXT_INTERNAL_API_ORIGIN=http://localhost:8423\n' >"$dir/guard-mismatch.conf" + guard_case mismatch "differ" "$dir/guard-mismatch.conf" + + printf 'NUXT_DEV_BACKEND_ORIGIN=https://plytarium.com\nNUXT_INTERNAL_API_ORIGIN=https://plytarium.com\n' >"$dir/guard-production.conf" + guard_case production "not loopback" "$dir/guard-production.conf" + + fake_backend "$dir" 404 '{}' 'X-Powered-By: PHP/8.5.10' + port="$FAKE_PORT" + printf 'NUXT_DEV_BACKEND_ORIGIN=http://127.0.0.1:%s\nNUXT_INTERNAL_API_ORIGIN=http://127.0.0.1:%s\n' "$port" "$port" >"$dir/guard-fake-php.conf" + guard_case fake-php "X-Powered-By: PHP" "$dir/guard-fake-php.conf" DEV_STACK_GO_ADDR="127.0.0.1:$port" + + fake_backend "$dir" 200 '{"backend":"winter","app":"fonoteka","env":"local"}' + port="$FAKE_PORT" + printf 'NUXT_DEV_BACKEND_ORIGIN=http://127.0.0.1:%s\nNUXT_INTERNAL_API_ORIGIN=http://127.0.0.1:%s\n' "$port" "$port" >"$dir/guard-other-marker.conf" + guard_case other-marker 'did not answer "backend":"summercms"' "$dir/guard-other-marker.conf" DEV_STACK_GO_ADDR="127.0.0.1:$port" + + # Control: the same rules accept a SummerCMS marker, so the refusals + # above are the cases', not a guard that refuses everything. + fake_backend "$dir" 200 '{"backend":"summercms","app":"fonoteka","env":"local"}' + port="$FAKE_PORT" + printf 'NUXT_DEV_BACKEND_ORIGIN=http://127.0.0.1:%s\nNUXT_INTERNAL_API_ORIGIN=http://127.0.0.1:%s\n' "$port" "$port" >"$dir/guard-control.conf" + local out rc=0 + out="$(env -i PATH="$PATH" HOME="$HOME" DEV_STACK_ENV_FILE="$dir/guard-control.conf" DEV_STACK_GO_ADDR="127.0.0.1:$port" bash "$DEV_STACK" guard 2>&1)" || rc=$? + if [[ "$rc" -ne 0 ]] || grep -q '^refuse:' <<<"$out"; then + echo "$out" >&2 + echo "refuse: guard control case (a SummerCMS marker) was refused" >&2 + return 1 + fi + echo "guard control: $out" + echo "phase14.2 guard passed" +} + +run_e2e() { + [[ -f "$DEV_STACK" ]] || { + echo "refuse: $DEV_STACK not found" >&2 + return 1 + } + local was_up=0 rc=0 + if bash "$DEV_STACK" status >/dev/null 2>&1; then + was_up=1 + echo "e2e: the local stack is already up; it is left running" + fi + if ((was_up == 0)); then + trap 'bash "$DEV_STACK" down || true' EXIT + fi + bash "$DEV_STACK" up --detach + bash "$DEV_STACK" guard + bash "$DEV_STACK" e2e "${GATE_SPECS[@]}" + echo "e2e (report only, D-15): ${REPORT_SPECS[*]}" + bash "$DEV_STACK" e2e --report-only "${REPORT_SPECS[@]}" 2>&1 | sed 's/^/e2e (report only) /' || rc=$? + ((rc == 0)) || echo "e2e (report only) run exited $rc (not blocking)" + scan_go_log "$GO_LOG" + echo "phase14.2 e2e passed" +} + run_all() { local stage - for stage in self-test go; do + for stage in self-test go docs parity contracts guard e2e; do if bash "${BASH_SOURCE[0]}" "--$stage"; then echo "PASS $stage" else @@ -181,6 +549,11 @@ run_all() { case "${1:---all}" in --self-test) run_self_test ;; --go) run_go ;; + --docs) run_docs ;; + --parity) run_parity ;; + --contracts) run_contracts ;; + --guard) run_guard ;; + --e2e) run_e2e ;; --all) run_all ;; *) usage ;; esac