#!/usr/bin/env bash # Repeatable Phase 3 verification: root and app vet/test/race, focused real # genres parity, corpus audit, and the Phase 2 Go/CLI harness regression. # Docker is required; unavailable Docker is a failed gate, never a skip. # PHP --fresh-php stays at scripts/check-phase2.sh (Phase 2 sign-off). set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" APP="$(cd "$ROOT/../fonoteka.go" && pwd)" PHP_ROOT="${PHP_ROOT:-/media/nvme/dev/golem15/fonoteka}" ROUTES_PHP="$PHP_ROOT/plugins/golem15/fonoteka/routes.php" if ! command -v docker >/dev/null 2>&1; then echo "refuse: docker is required for Phase 3 Postgres" >&2 exit 1 fi if ! docker info >/dev/null 2>&1; then echo "refuse: docker daemon is not available" >&2 exit 1 fi run_module() { local name="$1" local dir="$2" echo "==> ${name} (${dir})" ( cd "$dir" go vet ./... go test ./... go test -race ./... ) } echo "==> docker is available" run_module "root" "$ROOT" run_module "app" "$APP" echo "==> focused genres parity (one real ported pass)" ( cd "$APP" go test ./parity -count=1 -run 'TestParityCorpus$' ) echo "==> focused genre security and tenant isolation" ( cd "$APP" go test ./parity -count=1 -run 'TestGenreSecurityBoundaries|TestGenreCountsScopedToActiveCollection|TestGenreQueryFailsOnWrongLocale' ) echo "==> corpus audit" CORPUS_ARGS=( --manifest "$APP/parity/manifest.yaml" --require-recorded --require-clients --check-secrets ) if [[ -f "$ROUTES_PHP" ]]; then CORPUS_ARGS+=(--routes "$ROUTES_PHP") fi ( cd "$APP" go run ./parity/check_corpus.go "${CORPUS_ARGS[@]}" ) echo "==> Phase 2 Go/CLI harness regression" echo "==> TestParitySynthetic (testcontainers Postgres)" ( cd "$APP" go test ./parity -run TestParitySynthetic -count=1 ) echo "==> CLI synthetic record/replay smoke" SMOKE_DIR="$(mktemp -d /tmp/summercms-parity-smoke-XXXXXX)" SMOKE_PORT="" SMOKE_PID="" cleanup_smoke() { if [[ -n "${SMOKE_PID:-}" ]]; then kill "$SMOKE_PID" 2>/dev/null || true wait "$SMOKE_PID" 2>/dev/null || true fi rm -rf "$SMOKE_DIR" } python3 - "$SMOKE_DIR" <<'PY' & import http.server, socketserver, sys, pathlib d = pathlib.Path(sys.argv[1]) class H(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(b'{"data":"ok"}') def log_message(self, *args): pass with socketserver.TCPServer(("127.0.0.1", 0), H) as httpd: port = httpd.server_address[1] (d / "port").write_text(str(port)) httpd.serve_forever() PY SMOKE_PID=$! for _ in $(seq 1 50); do if [[ -f "$SMOKE_DIR/port" ]]; then SMOKE_PORT="$(cat "$SMOKE_DIR/port")" break fi sleep 0.1 done if [[ -z "$SMOKE_PORT" ]]; then cleanup_smoke echo "refuse: synthetic smoke server did not start" >&2 exit 1 fi ( cd "$ROOT" go build -o "$SMOKE_DIR/summer" ./cmd/summer "$SMOKE_DIR/summer" parity:record \ --spec modules/tide/testdata/one-route-spec.yaml \ --target "http://127.0.0.1:${SMOKE_PORT}" \ --output "$SMOKE_DIR/sample.yaml" "$SMOKE_DIR/summer" parity:replay \ --fixtures "$SMOKE_DIR/sample.yaml" \ --target "http://127.0.0.1:${SMOKE_PORT}" ) cleanup_smoke echo "phase3 check passed" echo "note: scripts/check-phase2.sh --fresh-php remains the PHP self-replay sign-off;" echo " as of 2026-09-17 it reports 150/154 on four wishlist album_count routes" echo " that Phase 3 did not change (PHP expected 1, live 2)."