#!/usr/bin/env bash # Local / Actions entry for Wave 2B nightly real-PI suite. # Classifies infrastructure vs product failures and writes a promotion receipt. # # Usage: # ./scripts/run-nightly-real-pi.sh [--dry-infra-fail] # # Environment: # PI_AGENT_RUNNER_URL default http://127.0.0.1:18865 # RUNNER_AUTH_TOKEN / PI_AGENT_RUNNER_AUTH_TOKEN # TEST_DATABASE_URL required for product run (isolated PG) # PI_AGENT_MODEL default MiniMax-M2.7-highspeed # NIGHTLY_SUITE_TIMEOUT go test timeout (default 90m) # NIGHTLY_EVIDENCE_DIR override evidence dir set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${ROOT}" DRY_INFRA_FAIL=0 if [[ "${1:-}" == "--dry-infra-fail" ]]; then DRY_INFRA_FAIL=1 fi RUNNER_URL="${PI_AGENT_RUNNER_URL:-http://127.0.0.1:18865}" TOKEN="${PI_AGENT_RUNNER_AUTH_TOKEN:-${RUNNER_AUTH_TOKEN:-}}" MODEL_ID="${PI_AGENT_MODEL:-MiniMax-M2.7-highspeed}" PROTOCOL="${PI_AGENT_PROTOCOL:-typed-result-v1}" TIMEOUT="${NIGHTLY_SUITE_TIMEOUT:-180m}" COMMIT="$(git rev-parse HEAD)" SUITE="nightly-real-pi-u8-four-workflows" STAMP="$(date -u +%Y%m%dT%H%M%SZ)" EVIDENCE_DIR="${NIGHTLY_EVIDENCE_DIR:-${ROOT}/.doc/test-reports/assets/promotion-receipts/evidence-${COMMIT:0:12}-${STAMP}}" mkdir -p "${EVIDENCE_DIR}" write_receipt() { local verdict="$1" local failure_class="$2" shift 2 local args=( --verdict "${verdict}" --commit "${COMMIT}" --suite "${SUITE}" --failure-class "${failure_class}" --runner-url "${RUNNER_URL}" --model-id "${MODEL_ID}" --protocol "${PROTOCOL}" --force ) local ref for ref in "$@"; do args+=(--evidence "${ref}") done PI_AGENT_RUNNER_URL="${RUNNER_URL}" PI_AGENT_MODEL="${MODEL_ID}" PI_AGENT_PROTOCOL="${PROTOCOL}" \ "${ROOT}/scripts/promotion-receipt-write.sh" "${args[@]}" } classify_test_log() { local log_file="$1" # Heuristics: auth/network/timeout/unavailable → infra; assertion/FAIL → product if grep -Eiq 'context deadline exceeded|connection refused|401 Unauthorized|Unauthorized|runner not available|no such host|i/o timeout|TLS handshake|dial tcp|read-only file system|mkdir .* read-only' "${log_file}"; then echo infra return fi if grep -Eiq '^--- FAIL:|FAIL\t|assertion|Expected|Fatalf|require\.' "${log_file}"; then echo product return fi echo product } echo "==> nightly real-PI suite commit=${COMMIT:0:12} runner=${RUNNER_URL}" # Wave 2C: ensure candidate image digests exist for PASS receipts / promote gate. ensure_candidate_images() { local images_json="${ROOT}/.doc/test-reports/assets/promotion-receipts/${COMMIT}.images.json" if [[ -f "${images_json}" ]]; then echo "==> reusing candidate images manifest ${images_json}" return 0 fi if [[ "${NIGHTLY_SKIP_IMAGE_BUILD:-0}" == "1" ]]; then echo "warning: NIGHTLY_SKIP_IMAGE_BUILD=1 and no images.json; PASS may lack digests" >&2 return 0 fi echo "==> building candidate images for digest pin..." "${ROOT}/scripts/candidate-images-build.sh" --commit "${COMMIT}" } if [[ "${DRY_INFRA_FAIL}" -eq 1 ]]; then echo "forced infra failure (--dry-infra-fail)" | tee "${EVIDENCE_DIR}/infra-forced.txt" write_receipt infra_fail infra "${EVIDENCE_DIR}/infra-forced.txt" echo "wrote infra_fail receipt (dry)" exit 1 fi if [[ -z "${TOKEN}" ]]; then echo "missing RUNNER_AUTH_TOKEN / PI_AGENT_RUNNER_AUTH_TOKEN" | tee "${EVIDENCE_DIR}/infra-missing-secret.txt" write_receipt infra_fail infra "${EVIDENCE_DIR}/infra-missing-secret.txt" echo "infrastructure failure: missing runner secret" >&2 exit 1 fi HEALTH_CODE=0 HEALTH_BODY="" if ! HEALTH_BODY="$(curl -fsS --max-time 5 "${RUNNER_URL}/health" 2>"${EVIDENCE_DIR}/health.err")"; then HEALTH_CODE=$? { echo "runner health check failed code=${HEALTH_CODE} url=${RUNNER_URL}/health" cat "${EVIDENCE_DIR}/health.err" 2>/dev/null || true } | tee "${EVIDENCE_DIR}/infra-runner-down.txt" write_receipt infra_fail infra "${EVIDENCE_DIR}/infra-runner-down.txt" echo "infrastructure failure: runner unreachable" >&2 exit 1 fi printf '%s\n' "${HEALTH_BODY}" > "${EVIDENCE_DIR}/runner-health.json" if [[ -z "${TEST_DATABASE_URL:-}" ]]; then echo "TEST_DATABASE_URL not set" | tee "${EVIDENCE_DIR}/infra-missing-db.txt" write_receipt infra_fail infra "${EVIDENCE_DIR}/infra-missing-db.txt" echo "infrastructure failure: missing TEST_DATABASE_URL" >&2 exit 1 fi export PI_AGENT_RUNNER_URL="${RUNNER_URL}" export PI_AGENT_RUNNER_AUTH_TOKEN="${TOKEN}" export RUNNER_AUTH_TOKEN="${TOKEN}" export PI_AGENT_MODEL="${MODEL_ID}" export RUN_PI_E2E=1 export KB_REQUIRE_ONLINE="${KB_REQUIRE_ONLINE:-1}" # Host Go tests must write workspaces into the same tree the runner mounts # (docker-compose.dev.yml: ./data/dev/workspaces → /app/data/workspaces). export DATA_DIR="${DATA_DIR:-${ROOT}/data/dev}" echo "==> DATA_DIR=${DATA_DIR}" # Shared KB preflight (host :5432). Fail closed unless KB_ALLOW_OFFLINE=1. preflight_shared_kb() { if [[ "${KB_ALLOW_OFFLINE:-0}" == "1" ]]; then echo "NOTE: KB offline degradation test (KB_ALLOW_OFFLINE=1)" | tee "${EVIDENCE_DIR}/kb-allow-offline.txt" return 0 fi local kb_host="${KB_DB_HOST:-127.0.0.1}" local kb_port="${KB_DB_PORT:-5432}" local kb_user="${KB_DB_USER:-regkb}" local kb_name="${KB_DB_NAME:-regulations}" local kb_pass="${KB_DB_PASSWORD:-}" if [[ -z "${kb_pass}" && -f "${ROOT}/.env" ]]; then kb_pass="$(grep -E '^KB_DB_PASSWORD=' "${ROOT}/.env" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")" fi if [[ -z "${kb_pass}" ]]; then echo "KB_DB_PASSWORD missing (root .env or env); cannot preflight shared KB" | tee "${EVIDENCE_DIR}/infra-kb-missing-password.txt" write_receipt infra_fail infra "${EVIDENCE_DIR}/infra-kb-missing-password.txt" echo "infrastructure failure: shared KB password missing" >&2 exit 1 fi if ! command -v psql >/dev/null 2>&1; then echo "psql not found; skip SQL preflight, rely on go test KB_REQUIRE_ONLINE" | tee "${EVIDENCE_DIR}/kb-preflight-skipped-no-psql.txt" return 0 fi if ! PGPASSWORD="${kb_pass}" psql -h "${kb_host}" -p "${kb_port}" -U "${kb_user}" -d "${kb_name}" -c 'SELECT 1' >/dev/null 2>"${EVIDENCE_DIR}/kb-preflight.err"; then { echo "shared KB preflight failed host=${kb_host} port=${kb_port} db=${kb_name} user=${kb_user}" cat "${EVIDENCE_DIR}/kb-preflight.err" 2>/dev/null || true } | tee "${EVIDENCE_DIR}/infra-kb-down.txt" write_receipt infra_fail infra "${EVIDENCE_DIR}/infra-kb-down.txt" echo "infrastructure failure: shared KB unreachable" >&2 exit 1 fi echo "shared KB preflight OK ${kb_host}:${kb_port}/${kb_name}" | tee "${EVIDENCE_DIR}/kb-preflight-ok.txt" } preflight_shared_kb # Clear env pollution that flakes short/config tests elsewhere. unset AGENT_TIMEOUT KB_DATABASE_URL KB_EMBEDDING_DIM 2>/dev/null || true # Four workflow governance chains (main / product_manager / test_manager / FullNone) # plus U8 confirm-generate. Rdev P3 remains the Python harness (not yet a Go PG top-level). RUN_FILTER='TestMultiIntentU8_RealPI_ConfirmGenerate_PG|TestWorkflowGovernance_PiAgent_PG_(FullChain|ProductManager_FullChain|TestManager_FullChain|FullNoneMixedChain)$' LOG_FILE="${EVIDENCE_DIR}/go-test.txt" set +e go test -count=1 -timeout "${TIMEOUT}" -v ./internal/server/ -run "${RUN_FILTER}" 2>&1 | tee "${LOG_FILE}" TEST_RC=${PIPESTATUS[0]} RESULT_LOG="${LOG_FILE}" # One retry for known flakes: model quality_failed, or PI hang / client deadline. if [[ "${TEST_RC}" -ne 0 ]] && grep -Eq 'quality_failed want completed|status=quality_failed|context deadline exceeded|Client\.Timeout exceeded|MISSING_TARGET|missing target deliverable' "${LOG_FILE}"; then echo "==> retry once after quality_failed/deadline/MISSING_TARGET flake..." | tee -a "${LOG_FILE}" RETRY_LOG="${EVIDENCE_DIR}/go-test-retry.txt" go test -count=1 -timeout "${TIMEOUT}" -v ./internal/server/ -run "${RUN_FILTER}" 2>&1 | tee "${RETRY_LOG}" TEST_RC=${PIPESTATUS[0]} RESULT_LOG="${RETRY_LOG}" cat "${RETRY_LOG}" >> "${LOG_FILE}" fi set -e if [[ "${TEST_RC}" -eq 0 ]]; then # Exit zero alone also covers no tests and partial skips; require all named top-level tests. if ! python3 "${ROOT}/scripts/verify_go_test_evidence.py" --log "${RESULT_LOG}" \ --expected TestMultiIntentU8_RealPI_ConfirmGenerate_PG \ --expected TestWorkflowGovernance_PiAgent_PG_FullChain \ --expected TestWorkflowGovernance_PiAgent_PG_ProductManager_FullChain \ --expected TestWorkflowGovernance_PiAgent_PG_TestManager_FullChain \ --expected TestWorkflowGovernance_PiAgent_PG_FullNoneMixedChain \ > "${EVIDENCE_DIR}/expected-tests.json"; then write_receipt infra_fail infra "${LOG_FILE}" "${EVIDENCE_DIR}/expected-tests.json" echo "infrastructure failure: expected real-PI tests did not all execute and pass" >&2 exit 1 fi if ! ensure_candidate_images; then echo "candidate image build failed" | tee "${EVIDENCE_DIR}/infra-image-build.txt" write_receipt infra_fail infra "${LOG_FILE}" "${EVIDENCE_DIR}/infra-image-build.txt" echo "infrastructure failure: candidate image build" >&2 exit 1 fi write_receipt pass none "${LOG_FILE}" "${EVIDENCE_DIR}/runner-health.json" echo "nightly real-PI PASS; receipt written for ${COMMIT:0:12}" exit 0 fi CLASS="$(classify_test_log "${LOG_FILE}")" if [[ "${CLASS}" == "infra" ]]; then write_receipt infra_fail infra "${LOG_FILE}" echo "infrastructure failure during product suite" >&2 exit 1 fi write_receipt product_fail product "${LOG_FILE}" echo "product failure; promotion must reject this commit" >&2 exit 1