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
This commit is contained in:
Executable
+309
@@ -0,0 +1,309 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user