summaryrefslogtreecommitdiff
path: root/qa/run.sh
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 02:23:43 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 02:23:43 +0800
commitf55ed58d2c75b53774ef7256fddb973ae3e73323 (patch)
tree63269f233f4305d5b68aa252e9548244a362adcf /qa/run.sh
parentbe1afd248e4cc9b03a1e21090602a945af6988b2 (diff)
downloadfund-lab-f55ed58d2c75b53774ef7256fddb973ae3e73323.tar.gz
feat(web): 基金创建/读取界面与仓库内浏览器 QA
[变更性质] - 本提交完成 3c-1 切片:Web 端基金建档(创建/重读)表单接入既有幂等 API,并提供仓库内可复现的端到端 QA 入口与生命周期回归。 [新增功能] - 前端新增 03 / CREATE 面板:名称与初始现金(decimal 字符串)输入、创建/重新读取按钮、服务端返回的基金档案行(份额诚实显示 0、来源标注真实建档/合成标记);移动端单列布局。 - 创建请求携带 32 位十六进制 Idempotency-Key;网络失败重试复用同一 key,输入变更或成功后换新 key;token 变更清空档案并按序列号拒绝过期响应。 [实现方案] - 金额全程 decimal 字符串,不经 JS float;update 层 fundSeq 守卫 FundCreate/FundRead 完成消息;BoundaryTests 新增 FundBoundaryTests 9 项(幂等键长度/复用、token 失效、stale 读取拒绝等)。 - qa/run.sh 一次性拉起非默认端口 Postgres/API/Vite(桩行情,_synthetic 标记),清理仅限本次创建的容器(ID+名称归属校验);临时目录 mktemp 隔离;qa/lifecycle-test.sh 验证拒绝路径不破坏既有资源。 [影响范围] - Web 测试增至 17 项,全套 56 项通过;浏览器 QA 30/30 通过;README 更新阶段描述。本提交不包含 docs/overnight-progress.md 的现有修改。
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"