#!/usr/bin/env bash set -u -o pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" API_BASE="${API_BASE:-http://localhost:5202/api}" DOC_PATH="$REPO_ROOT/docs/s06-acceptance-run.md" ARTIFACT_DIR="$REPO_ROOT/docs/artifacts/s06-acceptance" LOG_DIR="$ARTIFACT_DIR/logs" AUTH_TOKEN="${AUTH_TOKEN:-}" TEST_TARGET="${TEST_TARGET:-src/end-to-end-trust-loop.test.tsx}" PRECHECK_TOKEN_FILE="$ARTIFACT_DIR/.dev-auth-token.txt" mkdir -p "$LOG_DIR" TIMESTAMP_UTC="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" RUN_ID="$(date -u +"%Y%m%dT%H%M%SZ")" SEED_STATUS="not-run" SEED_EXIT_CODE="" SEED_DURATION_MS="0" SEED_LOG="$LOG_DIR/${RUN_ID}-acceptance-data.log" SEED_SUMMARY="Not run." TEST_STATUS="not-run" TEST_EXIT_CODE="" TEST_DURATION_MS="0" TEST_LOG="$LOG_DIR/${RUN_ID}-end-to-end-trust-loop.log" TEST_SUMMARY="Not run." PREFLIGHT_STATUS="not-run" PREFLIGHT_EXIT_CODE="" PREFLIGHT_DURATION_MS="0" PREFLIGHT_LOG="$LOG_DIR/${RUN_ID}-preflight.log" PREFLIGHT_SUMMARY="Not run." RUN_AUTH_SOURCE="provided" OVERALL_RESULT="pass" HARD_FAIL=0 note() { printf '%s\n' "$*" } run_logged() { local key="$1" local log_file="$2" shift 2 local start_ms end_ms exit_code status summary start_ms="$(python3 - <<'PY' import time print(int(time.time() * 1000)) PY )" set +e "$@" >"$log_file" 2>&1 exit_code=$? set -e end_ms="$(python3 - <<'PY' import time print(int(time.time() * 1000)) PY )" if [[ "$exit_code" -eq 0 ]]; then status="pass" summary="Command passed." else status="fail" summary="Command failed with exit ${exit_code}." fi case "$key" in preflight) PREFLIGHT_STATUS="$status" PREFLIGHT_EXIT_CODE="$exit_code" PREFLIGHT_DURATION_MS="$((end_ms - start_ms))" PREFLIGHT_SUMMARY="$summary" ;; seed) SEED_STATUS="$status" SEED_EXIT_CODE="$exit_code" SEED_DURATION_MS="$((end_ms - start_ms))" SEED_SUMMARY="$summary" ;; test) TEST_STATUS="$status" TEST_EXIT_CODE="$exit_code" TEST_DURATION_MS="$((end_ms - start_ms))" TEST_SUMMARY="$summary" ;; esac return "$exit_code" } mint_local_dev_token() { python3 - "$REPO_ROOT" <<'PY' import base64 import hashlib import hmac import json import sqlite3 import sys import time from pathlib import Path repo = Path(sys.argv[1]) api_base = repo / 'JobTrackerApi' config_path = api_base / 'appsettings.Development.json' db_path = api_base / 'jobtracker.db' if not config_path.exists() or not db_path.exists(): raise SystemExit(1) cfg = json.loads(config_path.read_text(encoding='utf-8')) auth = cfg.get('Auth') or {} key = (auth.get('JwtKey') or '').strip() issuer = (auth.get('JwtIssuer') or 'JobTrackerApi').strip() audience = (auth.get('JwtAudience') or 'job-tracker-ui').strip() if not key: raise SystemExit(1) conn = sqlite3.connect(db_path) cur = conn.cursor() row = cur.execute( """ SELECT u.Id, COALESCE(u.Email,''), COALESCE(u.UserName,''), COALESCE(r.Name,'') FROM AspNetUsers u LEFT JOIN AspNetUserRoles ur ON ur.UserId = u.Id LEFT JOIN AspNetRoles r ON r.Id = ur.RoleId WHERE LOWER(COALESCE(u.Email,'')) = 'admin@example.com' LIMIT 1 """ ).fetchone() if not row: raise SystemExit(1) user_id, email, username, role = row now = int(time.time()) payload = { 'iss': issuer, 'aud': audience, 'nbf': now - 5, 'exp': now + 12 * 60 * 60, 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier': user_id, } if email: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] = email if username: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'] = username if role: payload['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'] = role header = {'alg': 'HS256', 'typ': 'JWT'} def b64url(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii') segments = [ b64url(json.dumps(header, separators=(',', ':')).encode('utf-8')), b64url(json.dumps(payload, separators=(',', ':')).encode('utf-8')), ] signing_input = '.'.join(segments).encode('ascii') signature = hmac.new(key.encode('utf-8'), signing_input, hashlib.sha256).digest() segments.append(b64url(signature)) print('.'.join(segments)) PY } extract_browser_section() { python3 - "$DOC_PATH" <<'PY' import sys from pathlib import Path path = Path(sys.argv[1]) start = '' end = '' placeholder = "- Pending guided browser run. Update this section with `/jobs`, workspace, `/reminders`, and `/dashboard` observations.\n- Record the manual-send boundary observation, Gmail continuity status, and screenshot/debug-bundle paths." if not path.exists(): print(placeholder) raise SystemExit(0) text = path.read_text(encoding='utf-8') if start not in text or end not in text: print(placeholder) raise SystemExit(0) segment = text.split(start, 1)[1].split(end, 1)[0].strip('\n') print(segment.strip() or placeholder) PY } render_doc() { local browser_section generated_section browser_section="$(extract_browser_section)" generated_section=$(cat <"$DOC_PATH" < ${generated_section} ## Guided Browser Observations ${browser_section} EOF } set -e if [[ -z "$AUTH_TOKEN" && "$API_BASE" == "http://localhost:5202/api" ]]; then if AUTH_TOKEN="$(mint_local_dev_token 2>/dev/null)" && [[ -n "$AUTH_TOKEN" ]]; then RUN_AUTH_SOURCE="minted-local-dev-admin" printf '%s' "$AUTH_TOKEN" > "$PRECHECK_TOKEN_FILE" else AUTH_TOKEN="" RUN_AUTH_SOURCE="missing-no-dev-fallback" fi elif [[ -n "$AUTH_TOKEN" ]]; then RUN_AUTH_SOURCE="provided" else RUN_AUTH_SOURCE="missing-nondefault-api" fi if run_logged preflight "$PREFLIGHT_LOG" env AUTH_TOKEN="$AUTH_TOKEN" bash "$SCRIPT_DIR/s06-preflight.sh"; then PREFLIGHT_SUMMARY="Backend reachable. Preflight passed or reached the expected auth-limited partial-pass state." else PREFLIGHT_SUMMARY="Backend preflight failed. See log for the exact connection/auth error." OVERALL_RESULT="hard-fail" HARD_FAIL=1 render_doc note "acceptance.result=${OVERALL_RESULT}" note "acceptance.doc=${DOC_PATH#$REPO_ROOT/}" exit 1 fi if [[ -n "$AUTH_TOKEN" ]]; then if run_logged seed "$SEED_LOG" env AUTH_TOKEN="$AUTH_TOKEN" bash "$SCRIPT_DIR/s06-acceptance-data.sh"; then SEED_SUMMARY="Acceptance fixture seeded or updated successfully." else SEED_SUMMARY="Acceptance seed failed even with a token source available. Review the log for API or data-contract issues." OVERALL_RESULT="partial" fi else if run_logged seed "$SEED_LOG" env -u AUTH_TOKEN bash "$SCRIPT_DIR/s06-acceptance-data.sh"; then SEED_SUMMARY="Unexpected success without AUTH_TOKEN. Review the environment assumptions." else SEED_SUMMARY="Acceptance seed is auth blocked because no bearer token was available and the local dev fallback was not usable." OVERALL_RESULT="partial" fi fi if run_logged test "$TEST_LOG" bash -lc "cd '$REPO_ROOT/job-tracker-ui' && CI=true npm test -- --runInBand --watch=false '$TEST_TARGET'"; then TEST_SUMMARY="Relevant trust-loop regression passed." else TEST_SUMMARY="Relevant trust-loop regression failed. The artifact keeps the failure visible for follow-up." OVERALL_RESULT="partial" fi render_doc note "acceptance.result=${OVERALL_RESULT}" note "acceptance.doc=${DOC_PATH#$REPO_ROOT/}" note "acceptance.preflight=${PREFLIGHT_STATUS}" note "acceptance.seed=${SEED_STATUS}" note "acceptance.test=${TEST_STATUS}" exit 0