diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 02:23:43 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 02:23:43 +0800 |
| commit | f55ed58d2c75b53774ef7256fddb973ae3e73323 (patch) | |
| tree | 63269f233f4305d5b68aa252e9548244a362adcf /qa/run.sh | |
| parent | be1afd248e4cc9b03a1e21090602a945af6988b2 (diff) | |
| download | fund-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-x | qa/run.sh | 133 |
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" |
