summaryrefslogtreecommitdiff
path: root/qa/run.sh
diff options
context:
space:
mode:
Diffstat (limited to 'qa/run.sh')
-rwxr-xr-xqa/run.sh133
1 files changed, 133 insertions, 0 deletions
diff --git a/qa/run.sh b/qa/run.sh
new file mode 100755
index 0000000..bdfba3f
--- /dev/null
+++ b/qa/run.sh
@@ -0,0 +1,133 @@
+#!/usr/bin/env bash
+# End-to-end browser QA for fund-lab (work order 3c-1 scope and regressions).
+# Starts: disposable Postgres (port 55434), API (5095), Vite dev server (5175),
+# then drives the UI with headless Chromium via qa/driver/browser-test.js.
+# All ports and the container name are non-default and cleaned up on exit.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+QA_DIR="$ROOT/qa"
+API_PORT=5095
+WEB_PORT=5175
+PG_PORT=55434
+PG_CONTAINER=fund-lab-qa-pg-3c1
+PG_IMAGE=postgres:16-alpine
+PG_DB=fund_lab_qa
+PG_USER=fund_qa
+AUTH_TOKEN=qa-token
+TMP_DIR="$(mktemp -d /tmp/opencode/fund-lab-qa-XXXXXX)"
+ENV_FILE="$TMP_DIR/pg-env"
+API_LOG="$TMP_DIR/api.log"
+WEB_LOG="$TMP_DIR/web.log"
+API_PID=""
+WEB_PID=""
+CREATED_CONTAINER_ID=""
+
+log() { printf '[qa] %s\n' "$*"; }
+die() { printf '[qa] FATAL: %s\n' "$*" >&2; exit 1; }
+
+cleanup() {
+ local status=$?
+ trap - EXIT INT TERM
+ [ -n "$WEB_PID" ] && kill "$WEB_PID" 2>/dev/null || true
+ [ -n "$API_PID" ] && kill "$API_PID" 2>/dev/null || true
+ [ -n "$WEB_PID" ] && wait "$WEB_PID" 2>/dev/null || true
+ [ -n "$API_PID" ] && wait "$API_PID" 2>/dev/null || true
+ if [ -n "$CREATED_CONTAINER_ID" ]; then
+ owned="$(docker ps -a --filter "id=$CREATED_CONTAINER_ID" --format '{{.Names}}' 2>/dev/null || true)"
+ [ "$owned" = "$PG_CONTAINER" ] && docker rm -f "$CREATED_CONTAINER_ID" >/dev/null 2>&1 || true
+ fi
+ rm -f "$ENV_FILE"
+ log "cleanup done (exit=$status)"
+ log "artifacts preserved in $TMP_DIR"
+ exit "$status"
+}
+trap cleanup EXIT INT TERM
+
+port_in_use() {
+ (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null && { exec 3>&- 3<&-; return 0; } || return 1
+}
+
+command -v docker >/dev/null || die "docker is required"
+command -v openssl >/dev/null || die "openssl is required"
+docker info >/dev/null 2>&1 || die "docker daemon is not reachable"
+docker image inspect "$PG_IMAGE" >/dev/null 2>&1 || die "$PG_IMAGE is not available locally; pull it first"
+docker ps -a --format '{{.Names}}' | grep -qx "$PG_CONTAINER" && die "container $PG_CONTAINER already exists; refusing to touch it"
+for port in "$API_PORT" "$WEB_PORT" "$PG_PORT"; do
+ port_in_use "$port" && die "port $port is already in use; refusing to continue"
+done
+[ -x "$QA_DIR/stub/fund-lab-python" ] || chmod +x "$QA_DIR/stub/fund-lab-python"
+export PATH="$ROOT/.tools/node-v22.23.2/bin:$PATH"
+command -v node >/dev/null || die "node not found in .tools"
+command -v dotnet >/dev/null || die "dotnet not found on PATH"
+
+mkdir -p "$TMP_DIR"
+umask 077
+PG_PASSWORD="$(openssl rand -hex 24)"
+printf 'PG_PASSWORD=%s\n' "$PG_PASSWORD" > "$ENV_FILE"
+
+log "starting $PG_IMAGE as $PG_CONTAINER on 127.0.0.1:$PG_PORT"
+CREATED_CONTAINER_ID="$(docker run -d --name "$PG_CONTAINER" \
+ -e POSTGRES_DB="$PG_DB" -e POSTGRES_USER="$PG_USER" -e POSTGRES_PASSWORD="$PG_PASSWORD" \
+ -p "127.0.0.1:$PG_PORT:5432" "$PG_IMAGE")"
+
+pg_ready=0
+for _ in $(seq 1 60); do
+ if docker exec "$PG_CONTAINER" pg_isready -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1; then
+ pg_ready=1
+ break
+ fi
+ sleep 0.5
+done
+if [ "$pg_ready" != 1 ]; then
+ docker ps -a --format '{{.Names}} {{.Status}}' | grep "$PG_CONTAINER" >&2 || true
+ docker logs --tail 20 "$PG_CONTAINER" >&2 || true
+ die "postgres did not become ready"
+fi
+
+log "building API"
+dotnet build "$ROOT/src/FundLab.Api/FundLab.Api.fsproj" -v q --nologo >/dev/null
+
+log "starting API on 127.0.0.1:$API_PORT (stub interpreter, _synthetic payloads)"
+FUND_LAB_DATABASE_URL="Host=127.0.0.1;Port=$PG_PORT;Database=$PG_DB;Username=$PG_USER;Password=$PG_PASSWORD;Pooling=false" \
+FUND_LAB_AUTH_TOKEN="$AUTH_TOKEN" \
+FUND_LAB_AKSHARE_PYTHON="$QA_DIR/stub/fund-lab-python" \
+ASPNETCORE_URLS="http://127.0.0.1:$API_PORT" \
+ dotnet "$ROOT/src/FundLab.Api/bin/Debug/net8.0/FundLab.Api.dll" > "$API_LOG" 2>&1 &
+API_PID=$!
+
+api_up=0
+for _ in $(seq 1 60); do
+ code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$API_PORT/api/instruments?query=x" || true)"
+ [ "$code" != "000" ] && { api_up=1; break; }
+ sleep 0.5
+done
+[ "$api_up" = 1 ] || { cat "$API_LOG" >&2; die "API did not become ready"; }
+
+log "compiling F# to JS (fable)"
+( cd "$ROOT/src/FundLab.Web" && dotnet fable FundLab.Web.fsproj --outDir dist --noCache ) > "$TMP_DIR/fable.log" 2>&1
+
+log "starting Vite dev server on 127.0.0.1:$WEB_PORT"
+( cd "$ROOT/src/FundLab.Web" && FUND_LAB_API_TARGET="http://127.0.0.1:$API_PORT" exec node node_modules/vite/bin/vite.js --host 127.0.0.1 --port "$WEB_PORT" --strictPort ) \
+ > "$WEB_LOG" 2>&1 &
+WEB_PID=$!
+
+web_up=0
+for _ in $(seq 1 60); do
+ code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$WEB_PORT/" || true)"
+ [ "$code" = "200" ] && { web_up=1; break; }
+ sleep 0.5
+done
+[ "$web_up" = 1 ] || { cat "$WEB_LOG" >&2; die "vite dev server did not become ready"; }
+
+if [ ! -d "$QA_DIR/driver/node_modules" ]; then
+ log "installing driver dependencies"
+ ( cd "$QA_DIR/driver" && npm install --no-audit --no-fund --loglevel=error >/dev/null )
+fi
+
+log "running browser scenarios"
+mkdir -p "$TMP_DIR/screens"
+QA_BASE_URL="http://127.0.0.1:$WEB_PORT/" \
+QA_SHOTS="$TMP_DIR/screens" \
+QA_TOKEN="$AUTH_TOKEN" \
+ node "$QA_DIR/driver/browser-test.js" 2>&1 | tee "$TMP_DIR/driver.log"