326 lines
18 KiB
Bash
326 lines
18 KiB
Bash
#!/usr/bin/env bash
|
||
# e2e.sh —— U4 运行验收一条龙(真实浏览器:Playwright × RDM 审计工作区)。
|
||
#
|
||
# 动线(与 frontend/e2e/rdm-audit.spec.ts 的环境契约一一对应):
|
||
# 1. 构建:RDM 二进制(RDM 仓 origin/main 临时 worktree,pins 物化)
|
||
# + 基座 CLI(与 RDM pin 同源;identity ensure / plain-base serve)
|
||
# + frontend dist(本 worktree modules/frontend)
|
||
# 2. 种子:RDM -bootstrap-admin(域管理员)→ SUPER_ADMIN_* → serve 首启建基座存储
|
||
# (超管)→ identity ensure → 超管密码登录会话 → API rdm.project.create×2
|
||
# + rdm.ingest;普通账号(e2e-auditor 等)经超管会话 POST /api/admin/users 创建
|
||
# 3. 验收:vite preview(dist+/api 代理)→ npx playwright test
|
||
# 4. 收尾:停 serve/preview,保存认证/失败工件到 $E2E_OUT
|
||
#
|
||
# 用法:bash scripts/e2e.sh [--seed-only]
|
||
# --seed-only 只做构建+种子+起服务(留端口与路径在 stdout,供人工/调试)
|
||
# 环境变量:
|
||
# RDM_REPO RDM 仓路径(默认 <本仓上级>/Agent-System-RDM)
|
||
# E2E_API_PORT/E2E_UI_PORT 服务端口(默认 18791/4173)
|
||
# E2E_OWNER/E2E_PASSWORD 登录账号(默认 e2e-auditor/E2eAudit#2026)
|
||
# E2E_PASSWORD2 首次登录后的设密结果(默认 E2eAudit#2026b;Playwright 改密门消费)
|
||
# E2E_PROJECT_MAIN/E2E_PROJECT_ISOLATED 项目名(默认 审计演示项目/隔离检查项目)
|
||
# E2E_OUT 工件目录(默认 .tmp/e2e/<ts>;.gitignore 已覆盖 .tmp)
|
||
# 依赖:go、node/npm、curl;首次运行会 npx playwright install chromium。
|
||
set -euo pipefail
|
||
|
||
DEV_ROOT=$(git -C "$(dirname "$0")/.." rev-parse --show-toplevel)
|
||
cd "$DEV_ROOT"
|
||
|
||
SEED_ONLY=0
|
||
[ "${1:-}" = "--seed-only" ] && SEED_ONLY=1
|
||
|
||
RDM_REPO=${RDM_REPO:-"$DEV_ROOT/../Agent-System-RDM"}
|
||
API_PORT=${E2E_API_PORT:-18791}
|
||
UI_PORT=${E2E_UI_PORT:-4173}
|
||
OWNER=${E2E_OWNER:-e2e-auditor}
|
||
PASSWORD=${E2E_PASSWORD:-E2eAudit#2026}
|
||
PASSWORD2=${E2E_PASSWORD2:-E2eAudit#2026b}
|
||
PROJ_MAIN=${E2E_PROJECT_MAIN:-审计演示项目}
|
||
PROJ_ISO=${E2E_PROJECT_ISOLATED:-隔离检查项目}
|
||
API_BASE="http://127.0.0.1:$API_PORT"
|
||
# 工件目录一律绝对路径(git -C / 后台进程的 cwd 解析不能依赖调用方位置)。
|
||
OUT=${E2E_OUT:-"$DEV_ROOT/.tmp/e2e/$(date +%Y%m%d-%H%M%S)"}
|
||
case "$OUT" in /*) ;; *) OUT="$PWD/$OUT" ;; esac
|
||
mkdir -p "$OUT"
|
||
|
||
log() { printf '\033[36m[e2e]\033[0m %s\n' "$*"; }
|
||
die() { printf '\033[31m[e2e][FAIL]\033[0m %s\n' "$*" >&2; exit 1; }
|
||
|
||
command -v go >/dev/null || die "go not found"
|
||
command -v node >/dev/null || die "node not found"
|
||
command -v curl >/dev/null || die "curl not found"
|
||
[ "$(git -C "$RDM_REPO" rev-parse --is-inside-work-tree 2>/dev/null)" = true ] \
|
||
|| die "RDM repo not found at $RDM_REPO (set RDM_REPO)"
|
||
|
||
for p in "$API_PORT" "$UI_PORT"; do
|
||
if timeout 2 bash -c "echo > /dev/tcp/127.0.0.1/$p" 2>/dev/null; then
|
||
die "port $p already in use (set E2E_API_PORT/E2E_UI_PORT)"
|
||
fi
|
||
done
|
||
|
||
# ── 1. 构建 ─────────────────────────────────────────────
|
||
# RDM 源码经 git archive 从本地对象导出(不建 worktree、不触远端——Gitea 抖动时
|
||
# submodule 克隆会挂死);pins/* 从 RDM 主检出的已物化 submodule 快照拷贝(tar 排除
|
||
# .git;含 go.mod replace 引用的全部 pin,不仅 backend),构建输入由此固定为
|
||
# RDM origin/main + 其 gitlink 指向的本地内容。
|
||
log "build RDM binary (archive origin/main + snapshot pins from local checkout)"
|
||
RDM_WT="$OUT/rdm-src"
|
||
RDM_SHA=$(git -C "$RDM_REPO" rev-parse origin/main) || die "no origin/main in RDM repo"
|
||
mkdir -p "$RDM_WT"
|
||
git -C "$RDM_REPO" archive "$RDM_SHA" | tar -x -C "$RDM_WT"
|
||
# 主检出 pins 对齐 gitlink(本地对象已有时秒回;此时才需要网络 fetch 一次)。
|
||
git -C "$RDM_REPO" submodule update --init --recursive --quiet \
|
||
|| die "materialize RDM pins failed (is the RDM checkout reachable?)"
|
||
PIN_SHA_MAIN=$(git -C "$RDM_REPO/pins/agent-system-backend" rev-parse HEAD)
|
||
PIN_SHA_WANT=$(git -C "$RDM_REPO" ls-tree "$RDM_SHA" pins/agent-system-backend | awk '{print $3}')
|
||
[ "$PIN_SHA_MAIN" = "$PIN_SHA_WANT" ] \
|
||
|| die "RDM checkout pins/backend ($PIN_SHA_MAIN) != origin/main pin ($PIN_SHA_WANT); git -C $RDM_REPO submodule update first"
|
||
mkdir -p "$RDM_WT/pins"
|
||
# 快照全部 pins/*(go.mod replace 依赖 agent-document-renderer、agent-squad 等,
|
||
# 仅拷贝 backend 会导致 RDM 构建缺 pin);--exclude=.git 保持不变。
|
||
tar -C "$RDM_REPO/pins" --exclude=.git -cf - . | tar -C "$RDM_WT/pins" -xf -
|
||
cleanup() {
|
||
[ -n "${API_PID:-}" ] && { kill "$API_PID" 2>/dev/null || true; wait "$API_PID" 2>/dev/null || true; }
|
||
# Preview runs directly as one owned Vite process. Verify its command before
|
||
# signaling: a recycled PID must never stop another worktree's process.
|
||
if [ -n "${UI_PID:-}" ] && ps -p "$UI_PID" -o args= 2>/dev/null | rg -Fq -- "vite preview --port $UI_PORT"; then
|
||
kill "$UI_PID" 2>/dev/null || true
|
||
fi
|
||
}
|
||
trap cleanup EXIT
|
||
(cd "$RDM_WT" && go build -o "$OUT/agent-system-rdm" .) || die "rdm build failed"
|
||
log "rdm binary: $OUT/agent-system-rdm (RDM@${RDM_SHA:0:12}, pins@${PIN_SHA_WANT:0:12})"
|
||
|
||
log "build base CLI (identity ensure + plain-base serve) from the same pinned backend as the RDM binary"
|
||
# 版本一致性:基座 CLI 必须与被测 RDM 二进制共用同一基座 pin——用 Dev 顶层
|
||
# backend(main 最新)会把库迁到更高 schema,RDM pins(较旧)打不开。
|
||
(cd "$RDM_WT/pins/agent-system-backend" && go build -o "$OUT/agent-system" ./cmd/agent-system) \
|
||
|| die "backend cli build failed"
|
||
|
||
log "build frontend dist (contracts + ui deps + bundle, via make frontend)"
|
||
make -C "$DEV_ROOT" frontend >/dev/null || die "frontend build failed"
|
||
|
||
# ── 2. 种子 ─────────────────────────────────────────────
|
||
DATA="$OUT/data"
|
||
mkdir -p "$DATA"
|
||
RDM="$OUT/agent-system-rdm -data $DATA"
|
||
|
||
log "bootstrap: RDM domain admin = $OWNER"
|
||
$RDM -bootstrap-admin "$OWNER" >/dev/null
|
||
|
||
# ── 凭据契约:auth on 时超管由 serve 启动期 SUPER_ADMIN_OWNER/SUPER_ADMIN_PASSWORD
|
||
# 建立(基座已禁用 bootstrap-admin)。种子 API 会话走超管密码登录;普通账号
|
||
# (e2e-auditor 等)经 POST /api/admin/users 创建(初始密码合成值,mustChangePassword
|
||
# 由 Playwright 首登改密门处理)。合成测试凭据(非真实秘密)。
|
||
: "${SUPER_ADMIN_OWNER:=e2e-superadmin}"
|
||
: "${SUPER_ADMIN_PASSWORD:=E2eSuperAdmin#FixedTest9kQm2NpR}"
|
||
export SUPER_ADMIN_OWNER SUPER_ADMIN_PASSWORD
|
||
log "serve RDM at $API_BASE (auth on;首启创建基座存储)"
|
||
start_serve() {
|
||
"$OUT/agent-system-rdm" -data "$DATA" -addr "127.0.0.1:$API_PORT" -auth on \
|
||
>"$OUT/serve.log" 2>&1 &
|
||
API_PID=$!
|
||
# auth on 时未登录 /api/me 返回 401——服务已就绪的信号是「有 HTTP 应答」而非 2xx。
|
||
for i in $(seq 1 40); do
|
||
CODE=$(curl -s -o /dev/null -w '%{http_code}' "$API_BASE/api/me" || echo 000)
|
||
[ "$CODE" != "000" ] && return 0
|
||
sleep 0.25
|
||
[ "$i" = 40 ] && { tail -5 "$OUT/serve.log"; die "api not ready"; }
|
||
done
|
||
}
|
||
stop_serve() {
|
||
[ -n "${API_PID:-}" ] && { kill "$API_PID" 2>/dev/null || true; wait "$API_PID" 2>/dev/null || true; API_PID=""; }
|
||
}
|
||
login() {
|
||
# Seed API session uses serve-established superadmin (not E2E_OWNER).
|
||
local i
|
||
for i in $(seq 1 20); do
|
||
if curl -sf -c "$JAR" -X POST "$API_BASE/api/auth/login" \
|
||
-H 'Content-Type: application/json' \
|
||
-d "{\"owner\":\"$SUPER_ADMIN_OWNER\",\"password\":\"$SUPER_ADMIN_PASSWORD\"}" >/dev/null; then
|
||
return 0
|
||
fi
|
||
sleep 0.25
|
||
done
|
||
die "superadmin login failed"
|
||
}
|
||
cmd() { # cmd <id> <params-json> → payload(失败即死,种子步骤不容忍部分成功)
|
||
local id=$1 params=${2:-{\}}
|
||
curl -sf -b "$JAR" -X POST "$API_BASE/api/commands/$id" \
|
||
-H 'Content-Type: application/json' -d "{\"params\":$params}" \
|
||
| python3 -c 'import json,sys; d=json.load(sys.stdin); r=(d or {}).get("receipt",{}); sys.exit(1) if r.get("state")=="rejected" else None; print(json.dumps(d.get("payload", d), ensure_ascii=False))' \
|
||
|| die "command $id rejected"
|
||
}
|
||
admin_create_user() { # admin_create_user <owner> <password> [display-name]
|
||
# 超管会话 POST /api/admin/users;200 且 mustChangePassword:true(Playwright 首登改密)。
|
||
local owner=$1 password=$2 name=${3:-$1} body
|
||
body=$(curl -sf -b "$JAR" -X POST "$API_BASE/api/admin/users" \
|
||
-H 'Content-Type: application/json' \
|
||
-d "{\"owner\":\"$owner\",\"password\":\"$password\",\"profile\":{\"name\":\"$name\"}}") \
|
||
|| die "admin create user $owner failed"
|
||
printf '%s' "$body" | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get("mustChangePassword") is True else 1)' \
|
||
|| die "admin create user $owner: expected mustChangePassword:true"
|
||
}
|
||
|
||
# 阶段①:超管会话 + 管理 API 建号,再建项目与需求台账(API;需 serve)。
|
||
start_serve
|
||
JAR="$OUT/cookies.txt"
|
||
log "seed: identity ensure + base first account (SUPER_ADMIN session)"
|
||
ID_OK=0
|
||
for i in $(seq 1 15); do
|
||
if "$OUT/agent-system" identity ensure --data "$DATA" --instance-id "e2e-audit" >/dev/null 2>&1; then ID_OK=1; break; fi
|
||
sleep 1 # 服务端受控迁移(含 identity 表)可能晚于 HTTP 就绪
|
||
done
|
||
[ "$ID_OK" = 1 ] || { tail -5 "$OUT/serve.log"; die "identity ensure failed"; }
|
||
# Legacy bootstrap-admin is disabled; super admin is established at serve startup
|
||
# from SUPER_ADMIN_OWNER/SUPER_ADMIN_PASSWORD. Login via the normal session endpoint.
|
||
login
|
||
# Ordinary fixture owner (e2e-auditor):超管会话管理 API 建号;初始密码为 PASSWORD,
|
||
# mustChangePassword 由 Playwright 首登改密门处理为 PASSWORD2(不覆盖超管会话 jar)。
|
||
admin_create_user "$OWNER" "$PASSWORD" "$OWNER"
|
||
log "seed: create projects"
|
||
PID_MAIN=$(cmd rdm.project.create "{\"name\":\"$PROJ_MAIN\"}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["projectId"])')
|
||
PID_ISO=$(cmd rdm.project.create "{\"name\":\"$PROJ_ISO\"}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["projectId"])')
|
||
log "seed: projects main=$PID_MAIN iso=$PID_ISO"
|
||
|
||
# Projects are created under the SUPER_ADMIN session (immutable creator). The
|
||
# Playwright fixture owner is a separate account: add it as project owner so
|
||
# audit/doc visibility comes from membership + member-role / linked-capability
|
||
# defaults (Backend project.audit.read / document.read in memberBasicCollaborate),
|
||
# not legacy rdm.perm.set overrides (rejected after RDM #454/#456 role cutover).
|
||
log "seed: add fixture owner as project member (owner role; audit/doc via member defaults)"
|
||
cmd rdm.project.member.add "{\"projectId\":\"$PID_MAIN\",\"member\":\"$OWNER\",\"role\":\"owner\"}" >/dev/null
|
||
cmd rdm.project.member.add "{\"projectId\":\"$PID_ISO\",\"member\":\"$OWNER\",\"role\":\"owner\"}" >/dev/null
|
||
|
||
log "seed: import requirement ledger into $PID_MAIN"
|
||
cmd rdm.reqs.import "{\"projectId\":\"$PID_MAIN\",\"items\":$(
|
||
python3 - <<'PY'
|
||
import json
|
||
rows = [
|
||
("UR", "UR-01 离线可用", "断网时已有会话可继续浏览"),
|
||
("UR", "UR-02 权限收敛", "普通用户无审计入口,接口拒绝"),
|
||
("UR", "UR-03 取证效率", "追溯取证不超过三次主要点击"),
|
||
]
|
||
print(json.dumps([
|
||
{"kind": k, "title": t, "statement": s, "source": "01-用户需求与系统需求.md"}
|
||
for k, t, s in rows
|
||
], ensure_ascii=False))
|
||
PY
|
||
)}" >/dev/null
|
||
|
||
# 阶段②:CLI 直写库(-structure 项目归属装载;与 serve 不并发写 SQLite)。
|
||
stop_serve
|
||
log "seed: load doc-tree structure into $PID_MAIN"
|
||
"$OUT/agent-system-rdm" -data "$DATA" \
|
||
-structure "$RDM_WT/docs/structure/rdm-self.yaml" -project "$PID_MAIN" \
|
||
>"$OUT/structure.log" 2>&1 || { tail -5 "$OUT/structure.log"; die "structure load failed"; }
|
||
|
||
log "seed: load cockpit doc-tree into $PID_MAIN"
|
||
"$OUT/agent-system-rdm" -data "$DATA" \
|
||
-structure "$DEV_ROOT/docs/structure/cockpit-e2e.yaml" -project "$PID_MAIN" \
|
||
>"$OUT/structure-cockpit.log" 2>&1 || { tail -5 "$OUT/structure-cockpit.log"; die "cockpit structure load failed"; }
|
||
grep -m1 -o '装载(.*)' "$OUT/structure.log" || true
|
||
|
||
# 阶段③:重启服务,切片入库挂载到已装载骨架(API)。
|
||
start_serve
|
||
login
|
||
|
||
SEED_MD='# 用户需求与系统需求
|
||
|
||
| UR-01 | 离线可用:断网时已有会话可继续浏览 |
|
||
| UR-02 | 权限收敛:普通用户无审计入口 |
|
||
| UR-03 | 追溯取证不超过三次主要点击 |
|
||
|
||
## 背景说明
|
||
|
||
审计工作区按项目隔离读取;管理员可跨项目目录进入。事件按操作归组,
|
||
历史模式按 seq 还原,缺失事实明确标注不可恢复。'
|
||
log "seed: ingest corpus into $PID_MAIN"
|
||
cmd rdm.ingest "{\"projectId\":\"$PID_MAIN\",\"source\":\"01-用户需求与系统需求.md\",\"stage\":\"BD\",\"text\":$(python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' <<<"$SEED_MD")}" >/dev/null
|
||
log "seed done"
|
||
|
||
# ── 驾驶舱 I4 种子:章节探针提案 + 非成员账号(正文经结构 YAML 装载) ──
|
||
# Fixture owner already holds doc.write via owner membership defaults; seed
|
||
# draft.propose runs as SUPER_ADMIN (project creator) and needs no perm.set.
|
||
log "seed: cockpit pending proposal probe (non-danger; scenario 4)"
|
||
COCKPIT_OV=$(cmd rdm.cockpit.overview "{\"projectId\":\"$PID_MAIN\"}")
|
||
DOC_ID=$(printf '%s' "$COCKPIT_OV" | python3 -c 'import json,sys; d=json.load(sys.stdin); print([i["ref"]["id"] for i in d["documents"] if i["title"]=="验证主体报告"][0])')
|
||
SEC_JSON=$(cmd rdm.cockpit.tree "{\"projectId\":\"$PID_MAIN\",\"parent\":\"$DOC_ID\"}")
|
||
SEC_DESIGN=$(printf '%s' "$SEC_JSON" | python3 -c 'import json,sys; d=json.load(sys.stdin); print([i["ref"]["id"] for i in d["items"] if i["title"]=="设计说明"][0])')
|
||
# 场景 4 驱动用:留一个未采纳提案(浏览器内经 API 采纳后观察投影一致)。
|
||
cmd rdm.draft.propose "{\"projectId\":\"$PID_MAIN\",\"sectionId\":\"$SEC_DESIGN\",\"content\":\"一致性探针内容。\"}" >/dev/null
|
||
# 场景 2 驱动:验证主体报告设文档级三权(职责判据与正文版本作者无关;
|
||
# 结构装载作者为 structure,owner 经文档职责获得正文可读)。
|
||
log "seed: duty accounts + doc-level duties (scenario 2)"
|
||
DOC_MAIN=$(printf '%s' "$COCKPIT_OV" | python3 -c 'import json,sys; d=json.load(sys.stdin); print([i["ref"]["id"] for i in d["documents"] if i["title"]=="验证主体报告"][0])')
|
||
admin_create_user e2e-rev 'E2eRev#2026' e2e-rev
|
||
admin_create_user e2e-app 'E2eApp#2026' e2e-app
|
||
cmd rdm.project.member.add "{\"projectId\":\"$PID_MAIN\",\"member\":\"e2e-rev\",\"role\":\"member\"}" >/dev/null
|
||
cmd rdm.project.member.add "{\"projectId\":\"$PID_MAIN\",\"member\":\"e2e-app\",\"role\":\"member\"}" >/dev/null
|
||
cmd rdm.review.assign.doc "{\"projectId\":\"$PID_MAIN\",\"docId\":\"$DOC_MAIN\",\"author\":\"$OWNER\",\"reviewers\":[\"e2e-rev\"],\"approver\":\"e2e-app\"}" >/dev/null
|
||
log "seed: second account (non-member negative case)"
|
||
admin_create_user e2e-reader 'E2eReader#2026' e2e-reader
|
||
|
||
# ── 3. 验收 ─────────────────────────────────────────────
|
||
if [ "$SEED_ONLY" = 1 ]; then
|
||
log "seed-only mode: api=$API_BASE ui=next;cookie jar=$JAR;数据=$DATA"
|
||
log "preview: (cd modules/frontend && PLAYWRIGHT_API_TARGET=$API_BASE npx vite preview --port $UI_PORT)"
|
||
log "inspect: curl -b $JAR $API_BASE/api/commands/rdm.inspect.summary -X POST -H 'Content-Type: application/json' -d '{\"params\":{\"projectId\":\"$PID_MAIN\"}}'"
|
||
wait
|
||
exit 0
|
||
fi
|
||
|
||
log "start vite preview at http://127.0.0.1:$UI_PORT"
|
||
(
|
||
cd "$DEV_ROOT/modules/frontend"
|
||
export PLAYWRIGHT_API_TARGET="$API_BASE"
|
||
exec ./node_modules/.bin/vite preview --port "$UI_PORT" --strictPort
|
||
) >"$OUT/preview.log" 2>&1 &
|
||
UI_PID=$!
|
||
for i in $(seq 1 40); do
|
||
curl -sf -o /dev/null "http://127.0.0.1:$UI_PORT/" && break
|
||
sleep 0.25
|
||
[ "$i" = 40 ] && { tail -5 "$OUT/preview.log"; die "preview not ready"; }
|
||
done
|
||
|
||
log "playwright (install chromium if missing)"
|
||
(cd "$DEV_ROOT/modules/frontend" && npx playwright install chromium >/dev/null 2>&1 || true)
|
||
|
||
log "run specs"
|
||
cd "$DEV_ROOT/modules/frontend"
|
||
export PLAYWRIGHT_BASE_URL="http://127.0.0.1:$UI_PORT" E2E_OWNER="$OWNER" \
|
||
E2E_PASSWORD="${E2E_PASSWORD:-E2eAudit#2026}" E2E_PASSWORD2="$PASSWORD2" \
|
||
E2E_PROJECT_MAIN="$PROJ_MAIN" E2E_PROJECT_ISOLATED="$PROJ_ISO"
|
||
set +e
|
||
# 本脚本的种子只覆盖审计工作区。u7 现场验收用例依赖另一套入口,不在这里跑。
|
||
# admin-create 场景复用本种子的基座超管会话(SUPER_ADMIN_*)。
|
||
npx playwright test e2e/rdm-audit.playwright.ts e2e/rdm-cockpit.playwright.ts e2e/admin-user-create.playwright.ts --reporter=list >"$OUT/playwright.log" 2>&1
|
||
PW_EXIT=$?
|
||
set -e
|
||
cat "$OUT/playwright.log"
|
||
[ "$PW_EXIT" = 0 ] && log "PASS" || die "playwright exit=$PW_EXIT (log: $OUT/playwright.log, trace: $OUT/frontend/test-results/)"
|
||
|
||
# ── 4. 普通基座缺能力场景(增量 2 验收)──────────────────
|
||
# 同一 $DATA 换基座 CLI 服务(与 RDM 二进制同源同 pin,命令面无 rdm.inspect.*),
|
||
# preview 不动只换 /api 指向,跑 plain-base spec:审计入口隐藏、代理常驻、
|
||
# 全程无 rdm.* 命令请求——消除「入口可达、进入即 400」的原缺陷路径。
|
||
log "plain-base phase: serve base CLI (no RDM commands)"
|
||
stop_serve
|
||
"$OUT/agent-system" serve --data "$DATA" --addr "127.0.0.1:$API_PORT" --auth on \
|
||
>"$OUT/serve-plain.log" 2>&1 &
|
||
API_PID=$!
|
||
for i in $(seq 1 40); do
|
||
CODE=$(curl -s -o /dev/null -w '%{http_code}' "$API_BASE/api/me" || echo 000)
|
||
[ "$CODE" != "000" ] && break
|
||
sleep 0.25
|
||
[ "$i" = 40 ] && { tail -5 "$OUT/serve-plain.log"; die "plain api not ready"; }
|
||
done
|
||
set +e
|
||
E2E_PLAIN_BASE=1 npx playwright test e2e/plain-base.playwright.ts e2e/changepw-form.playwright.ts --reporter=list >"$OUT/playwright-plain.log" 2>&1
|
||
PW_PLAIN_EXIT=$?
|
||
set -e
|
||
cat "$OUT/playwright-plain.log"
|
||
[ "$PW_PLAIN_EXIT" = 0 ] && log "PASS (plain-base + changepw-form)" || die "plain-base playwright exit=$PW_PLAIN_EXIT (log: $OUT/playwright-plain.log)"
|
||
|
||
log "done; artifacts in $OUT"
|