Files
Agent-System-Dev/scripts/e2e.sh
T
agent-exec-transport 0bef900271
ci / svc-realcombo (pull_request) Failing after 9s
ci / test-pg (pull_request) Failing after 10s
ci / test (pull_request) Failing after 10s
agent-exec transport 7fcb6fe7bcd8c87d45817a4df898a522/a352823d21b0
2026-10-23 08:04:28 +00:00

390 lines
22 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.reqs.import /
# rdm.ingest 等携 conversationId/anchorMessageId;
# 普通账号(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"
# 直接构建 FE dist:不走 make frontend(其 contracts→test-worktree 要求全部
# submodule 已物化;本脚本只需 FE+@ui 源码组合)。contracts.ts 以 FE pin 为准。
log "build frontend dist (npm ci @ui + frontend, vite build)"
[ -f "$DEV_ROOT/modules/ui/package.json" ] \
|| die "modules/ui missing (need gitlink checkout for @ui alias)"
[ -f "$DEV_ROOT/modules/frontend/package.json" ] \
|| die "modules/frontend missing"
(cd "$DEV_ROOT/modules/ui" && npm ci --no-fund --no-audit) >/dev/null \
|| die "ui npm ci failed"
(cd "$DEV_ROOT/modules/frontend" && npm ci --no-fund --no-audit && npm run build) \
|| 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> [extra-body-fields] → payload(失败即死,种子步骤不容忍部分成功)
local id=$1 params=${2:-{\}} extra=${3:-}
curl -sf -b "$JAR" -X POST "$API_BASE/api/commands/$id" \
-H 'Content-Type: application/json' -d "{${extra:+$extra,}\"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] [isAdmin]
# 超管会话 POST /api/admin/users;200 且 mustChangePassword:true(Playwright 首登改密)。
# 第 4 参为 true 时任命基座管理员(admin-user-create 需「系统管理…」入口;仅超管可任命)。
local owner=$1 password=$2 name=${3:-$1} is_admin=${4:-false} 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\"},\"isAdmin\":$is_admin}") \
|| 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"
}
# 浏览器用例若不以改密门处理该账号(如 cockpit 的 e2e-reader),种子阶段代完 must-change。
complete_must_change() { # complete_must_change <owner> <initial> <final>
local owner=$1 initial=$2 final=$3 jar i
jar="$OUT/${owner}.changepw.jar"
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\":\"$owner\",\"password\":\"$initial\"}" >/dev/null; then
break
fi
sleep 0.25
[ "$i" = 20 ] && die "must-change login failed for $owner"
done
curl -sf -b "$jar" -c "$jar" -X POST "$API_BASE/api/auth/password" \
-H 'Content-Type: application/json' \
-d "{\"current\":\"$initial\",\"next\":\"$final\"}" >/dev/null \
|| die "must-change password failed for $owner"
}
# 正式项目命令(rdm.reqs.*/rdm.review.*/rdm.draft.*/rdm.ingest 等,基座
# cmdbus.RequiresProjectWorkDecision)须带已认证会话来源:conversationId 绑定宿主项目,
# anchorMessageId 为该会话中已提交、经 RDM 来源分析放行的用户消息;裸调用被预检拒绝。
seed_source() { # seed_source <hostProjectId>:超管在宿主项目建种子会话并提交一条来源消息
local host=$1 body
body=$(curl -sf -b "$JAR" -X POST "$API_BASE/api/projects/$host/conversations" \
-H 'Content-Type: application/json' -d '{"title":"e2e seed source"}') \
|| die "seed source conversation create failed"
SRC_CONV=$(printf '%s' "$body" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
body=$(curl -sf -b "$JAR" -X POST "$API_BASE/api/conversations/$SRC_CONV/messages" \
-H 'Content-Type: application/json' -d '{"text":"e2e 种子:导入需求台账、入库需求语料、登记驾驶舱探针与文档职责"}') \
|| die "seed source message failed"
SRC_MSG=$(printf '%s' "$body" | python3 -c 'import json,sys; print(json.load(sys.stdin)["message"]["id"])')
}
fcmd() { # fcmd <id> <params-json>:正式项目命令,附种子会话来源
[ -n "${SRC_MSG:-}" ] || die "formal command $1 needs seed_source first"
cmd "$1" "$2" "\"conversationId\":\"$SRC_CONV\",\"anchorMessageId\":\"$SRC_MSG\""
}
# 阶段①:超管会话 + 管理 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
# Fixture owner(默认 e2e-auditor):RDM -bootstrap-admin 已授域管理员;此处再建基座账号
# 并任命 isAdmin(admin-user-create 需「系统管理…」)。初始密码=PASSWORD,mustChange
# 由 Playwright 首登改密门处理为 PASSWORD2(不覆盖超管会话 jar)。
admin_create_user "$OWNER" "$PASSWORD" "$OWNER" true
log "seed: create projects"
read -r PID_MAIN HOST_MAIN < <(cmd rdm.project.create "{\"name\":\"$PROJ_MAIN\"}" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["projectId"], d["hostProjectId"])')
[ -n "${HOST_MAIN:-}" ] || die "rdm.project.create: missing projectId/hostProjectId"
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 (host $HOST_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
# doc visibility comes from membership + member-role defaults.
log "seed: add fixture owner as project member (owner role; 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
# audit.read is never a member-role default and its platform cap ships closed
# (rdm_perm_caps audit.read=0): open the cap, then grant it explicitly per project
# (rdm.perm.set is rejected above the cap).
log "seed: audit.read cap + explicit grant to fixture owner on both projects"
cmd rdm.perm.cap '{"action":"audit.read","allow":true}' >/dev/null
for p in "$PID_MAIN" "$PID_ISO"; do
cmd rdm.perm.set "{\"projectId\":\"$p\",\"member\":\"$OWNER\",\"action\":\"audit.read\",\"allow\":true}" >/dev/null
done
log "seed: conversation source in $HOST_MAIN (formal project commands)"
seed_source "$HOST_MAIN"
log "seed: import requirement ledger into $PID_MAIN"
fcmd 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"
fcmd 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 采纳后观察投影一致)。
fcmd 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
fcmd 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)"
# cockpit 场景 6 直接以 E2E_READER_PASSWORD 登录(无改密门);种子代完 must-change,
# 使最终密码与 Playwright 缺省 E2eReader#2026 一致。
READER=${E2E_READER:-e2e-reader}
READER_PASSWORD=${E2E_READER_PASSWORD:-E2eReader#2026}
admin_create_user "$READER" "$READER_PASSWORD" "$READER"
complete_must_change "$READER" "$READER_PASSWORD" "$READER_PASSWORD"
# ── 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="$PASSWORD" E2E_PASSWORD2="$PASSWORD2" \
E2E_READER="$READER" E2E_READER_PASSWORD="$READER_PASSWORD" \
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"