blob: 72af775945a524a6a543e0c877c65b07f713de88 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
#!/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)"
# The stub serves NAV through 2026-09-21 and the driver asserts that trade date, so pin the
# trading-date clock (same test hook the xUnit fixture uses) instead of crossing the shanghai cutoff.
FUND_LAB_TEST_TRADE_DATE="2026-09-21" \
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"
|