#!/usr/bin/env bash set -uo pipefail # CASAN CI harness gate. # Runs the reproducible harness suites in a safe order. `run-casan4` must run # first because it rewrites `.specify/logs`. # # Env: # CASAN_CI_RUN_FRONTEND=0|1 default 1 # CASAN_CI_RUN_INFRA_LAB=0|1 default 0 (Docker Compose lab is optional in CI) # CASAN_CI_STEP_TIMEOUT_SEC default 600 # CASAN_CI_SUITE_FILTER optional regex; run matching suite names only SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" TESTS="$ROOT/.specify/tests" PASS=0 FAIL=0 SKIP=0 run() { local name="$1" shift if [[ -n "${CASAN_CI_SUITE_FILTER:-}" && ! "$name" =~ $CASAN_CI_SUITE_FILTER ]]; then skip "$name (filtered)" return 0 fi echo "==> $name" if run_with_timeout "$@"; then echo "CI_GATE_PASS $name" PASS=$((PASS + 1)) else echo "CI_GATE_FAIL $name" >&2 FAIL=$((FAIL + 1)) fi } skip() { echo "CI_GATE_SKIP $1" SKIP=$((SKIP + 1)) } run_with_timeout() { if ! command -v python3 >/dev/null 2>&1; then "$@" return $? fi CASAN_CI_STEP_TIMEOUT_SEC="${CASAN_CI_STEP_TIMEOUT_SEC:-600}" python3 - "$@" <<'PY' import os import subprocess import sys timeout = int(os.environ.get("CASAN_CI_STEP_TIMEOUT_SEC", "600")) cmd = sys.argv[1:] try: raise SystemExit(subprocess.run(cmd, timeout=timeout).returncode) except subprocess.TimeoutExpired: print(f"CI_GATE_TIMEOUT seconds={timeout} command={' '.join(cmd)}", file=sys.stderr) raise SystemExit(124) PY } cd "$ROOT" || exit 1 run "run-casan4-harness" bash "$TESTS/run-casan4-harness-tests.sh" run "adversarial-harness" bash "$TESTS/adversarial-harness-tests.sh" run "phase1-track-a" bash "$TESTS/phase1-track-a-tests.sh" run "phase2-track-c" bash "$TESTS/phase2-track-c-tests.sh" run "phase2-sourcegen" bash "$TESTS/phase2-sourcegen-tests.sh" run "phase3-evidence-pack" bash "$TESTS/phase3-evidence-pack-tests.sh" run "phase3-model-router" bash "$TESTS/phase3-model-router-tests.sh" run "phase-h5-approval" bash "$TESTS/phase-h5-approval-tests.sh" run "phase-h5-infra" bash "$TESTS/phase-h5-infra-tests.sh" run "phase-h6-agentops" bash "$TESTS/phase-h6-agentops-tests.sh" run "phase-c7-incident" bash "$TESTS/phase-c7-incident-tests.sh" run "phase-h4-multilingual" bash "$TESTS/phase-h4-multilingual-tests.sh" run "phase-c6-sandbox" bash "$TESTS/phase-c6-sandbox-tests.sh" run "phase-h4-split-inject" bash "$TESTS/phase-h4-split-inject-tests.sh" run "phase10-traceability" bash "$TESTS/phase10-traceability-tests.sh" run "phase08-compression" bash "$TESTS/phase08-compression-tests.sh" run "phase-control-plane" bash "$TESTS/phase-control-plane-tests.sh" run "phase-rbac" bash "$TESTS/phase-rbac-tests.sh" run "phase-rai" bash "$TESTS/phase-rai-tests.sh" run "phase-selfimprove" bash "$TESTS/phase-selfimprove-tests.sh" run "phase-governance-report" bash "$TESTS/phase-governance-report-tests.sh" run "phase-preflight" bash "$TESTS/phase-preflight-tests.sh" if [[ "${CASAN_CI_RUN_BACKEND:-1}" == "1" ]]; then if command -v npm >/dev/null 2>&1; then run "backend-tests" npm test -w backend else skip "backend-tests (npm unavailable)" fi else skip "backend-tests (CASAN_CI_RUN_BACKEND=0)" fi if [[ "${CASAN_CI_RUN_FRONTEND:-1}" == "1" ]]; then if command -v npm >/dev/null 2>&1; then run "frontend-vitest" npm test -w frontend else skip "frontend-vitest (npm unavailable)" fi else skip "frontend-vitest (CASAN_CI_RUN_FRONTEND=0)" fi if [[ "${CASAN_CI_RUN_INFRA_LAB:-0}" == "1" ]]; then if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then run "local-prod-infra-lab" bash "$TESTS/phase-prod-infra-lab-tests.sh" else skip "local-prod-infra-lab (docker compose unavailable)" fi else skip "local-prod-infra-lab (CASAN_CI_RUN_INFRA_LAB=0)" fi echo "CI_GATE_SUMMARY PASS=$PASS FAIL=$FAIL SKIP=$SKIP" [[ "$FAIL" -eq 0 ]] || exit 1