Files
jobtrackingapp/scripts/s06-acceptance-run.sh
cesnimda 48b24b4516 feat: Added a repeatable live acceptance runner and recorded real S06 b…
- "scripts/s06-acceptance-run.sh"
- "docs/s06-acceptance-run.md"
- ".gsd/KNOWLEDGE.md"
- ".gsd/DECISIONS.md"
- ".gsd/milestones/M001/slices/S06/tasks/T03-SUMMARY.md"

GSD-Task: S06/T03
2026-03-27 09:24:27 +01:00

310 lines
9.7 KiB
Bash
Executable File

#!/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 = '<!-- acceptance-run:browser:start -->'
end = '<!-- acceptance-run:browser: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 <<EOF
## Run Metadata
- Run id: "${RUN_ID}"
- Generated at (UTC): "${TIMESTAMP_UTC}"
- API base: "${API_BASE}"
- Auth token source: ${RUN_AUTH_SOURCE}
- Overall runner result: **${OVERALL_RESULT}**
## Shell Verification Summary
| Step | Status | Exit | Duration | Notes | Log |
|---|---|---:|---:|---|---|
| Preflight | ${PREFLIGHT_STATUS} | ${PREFLIGHT_EXIT_CODE:-n/a} | ${PREFLIGHT_DURATION_MS}ms | ${PREFLIGHT_SUMMARY} | "${PREFLIGHT_LOG#$REPO_ROOT/}" |
| Seed acceptance data | ${SEED_STATUS} | ${SEED_EXIT_CODE:-n/a} | ${SEED_DURATION_MS}ms | ${SEED_SUMMARY} | "${SEED_LOG#$REPO_ROOT/}" |
| UI trust-loop test | ${TEST_STATUS} | ${TEST_EXIT_CODE:-n/a} | ${TEST_DURATION_MS}ms | ${TEST_SUMMARY} | "${TEST_LOG#$REPO_ROOT/}" |
## Runner Observations
- Preflight is the only hard-stop gate. If the backend is unreachable, this runner exits non-zero after recording the failure.
- Seeding failures and UI regression failures are still written into this artifact so auth/test blockers are visible instead of disappearing behind a shell exit.
- Secrets are redacted by design: the runner never prints bearer tokens and only records the token source category.
## Auth / Blocker Guidance
- If the environment already exported "AUTH_TOKEN", the runner reuses it.
- If "AUTH_TOKEN" is missing and the API base is the default localhost dev target, the runner attempts a **local dev JWT fallback** using the checked-in dev JWT settings plus the local SQLite admin user so acceptance seeding can proceed without printing a token.
- If the fallback cannot mint or validate a token, treat the seed/browser run as **auth blocked**. In that case, log in via "POST /api/auth/login" with the real local account (or reuse an already-authenticated browser session), export only the access token, and rerun the runner.
- Gmail continuity may legitimately remain blocked when Gmail is not connected/configured for the local user; that is expected to be called out explicitly below instead of triggering auto-send behavior.
EOF
)
cat >"$DOC_PATH" <<EOF
# S06 Acceptance Run
This document captures the live S06 acceptance rerun for the "/jobs → workspace → reminders/dashboard → follow-up/manual-send boundary" loop.
<!-- acceptance-run:generated:start -->
${generated_section}
<!-- acceptance-run:generated:end -->
## Guided Browser Observations
<!-- acceptance-run:browser:start -->
${browser_section}
<!-- acceptance-run:browser:end -->
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