Merge authoritative main into the task branch

Why: include the current integration baseline before verification.

Evidence: fetched origin/main=d5428c5f05fc5f625e6232ab3156b17f13e14d6a.

Impact: integrate upstream changes without rewriting task history.

Refs: task-sync
This commit is contained in:
2026-10-13 11:14:24 +00:00
2 changed files with 164 additions and 0 deletions
@@ -89,6 +89,20 @@ Documents #93(提案 r1);Core #30/#32(受管契约+权威 TS 镜像)
4. **无效当前职责**:移除联动已阻断新增残留;存量无成员职责列入修复清单(不自动替换人员)。
5. **svc 播种语义变更**:认证档无管理员账号时播种跳过并有告警(部署核对项)。
### 双钥匙发版方案(待用户亲手执行;Agent 只准备不代做)
四仓在各自 main 打 v0.4.0 annotated tag 并推(AGENTS.md 高后果双钥匙;打 tag 后 RDM 升 pins):
```
cd <本地各仓检出> # Core / Backend / Frontend / UI
git fetch origin main && git checkout origin/main
git tag -a v0.4.0 -m "v0.4.0: managed settings scope, account search, member/duty/permission pages, Picker primitive" && git push origin v0.4.0
```
目标 SHA(各仓当前 main,2026-10-13 核对):Core 5773e74、Backend 15566f9、Frontend c294835、UI 2c4322e。
核验(打完后任一执行者可查):`git ls-remote origin v0.4.0`。
tag 后 Agent 连续动作:RDM pins 四仓升 v0.4.0 + rdm.account.list query/Name 透传(代码就绪)→ `scripts/svc.sh dev deploy --from rdm` 部署 → 跑 `tests/integration/rdm-increment/acceptance_scenarios.sh`(十场景,多账号多角色)→ 证据落卡 → 提交正式验收。
### 待执行(阻塞或人工节点)
- **第八节十场景验收**:需真实部署环境+多角色浏览器流程(非管理员负责人/普通成员/互异三权人员/两个项目)——属正式验收(阶段 7 人工节点),待用户安排。
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# P8 第八节十场景验收(提案 rdm-settings-members-duties-perms-increment §8)。
# 走真实 dev 服务 HTTP(--auth 会话),多账号多角色;输出逐场景 PASS/FAIL。
# 用法:BASE=http://127.0.0.1:8788 ADMIN_USER=weichen ADMIN_PASS=<密码> ./acceptance_scenarios.sh
set -u
BASE="${BASE:-http://127.0.0.1:8788}"
ADMIN_USER="${ADMIN_USER:?需要管理员账号}"
ADMIN_PASS="${ADMIN_PASS:?需要管理员密码}"
PASS=0; FAIL=0; FAILED=()
say() { printf '%s\n' "$*"; }
ok() { PASS=$((PASS+1)); say "PASS $1"; }
bad() { FAIL=$((FAIL+1)); FAILED+=("$1"); say "FAIL $1 :: ${2:-}"; }
# 会话登录(cookie jar)与 API 助手
login() { # user pass jar
curl -s -c "$3" -X POST "$BASE/api/auth/login" -H 'Content-Type: application/json' \
-d "{\"owner\":\"$1\",\"password\":\"$2\"}" -o /dev/null
}
api() { # jar method path [json]
local jar=$1 m=$2 p=$3 body=${4:-}
if [ -n "$body" ]; then
curl -s -b "$jar" -X "$m" "$BASE$p" -H 'Content-Type: application/json' -d "$body" -w '\n%{http_code}'
else
curl -s -b "$jar" -X "$m" "$BASE$p" -w '\n%{http_code}'
fi
}
body() { sed '$d'; }
code() { tail -n1; }
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
JA="$T/admin"; login "$ADMIN_USER" "$ADMIN_PASS" "$JA"
say "== 准备:账号与两项目 =="
TS=$(date +%s)
# 建号:非管理员负责人 owner2 / 普通成员 mem1 / 三权三人 w1 a1 r1(密码同式)
for u in owner2 mem1 w1 a1 r1; do
resp=$(api "$JA" POST /api/admin/users "{\"owner\":\"$u\",\"name\":\"角色-$u\",\"password\":\"Pass-$TS-x8z\",\"isAdmin\":false}")
[ "$(echo "$resp" | code)" = "200" ] || say " ($u 已存在或其他:$(echo "$resp" | code))"
done
# 各自完成改密门(登录→改密)
for u in owner2 mem1 w1 a1 r1; do
J="$T/$u"; login "$u" "Pass-$TS-x8z" "$J"
api "$J" POST /api/auth/password "{\"current\":\"Pass-$TS-x8z\",\"next\":\"Pass2-$TS\"}" >/dev/null
done
JPASS="Pass2-$TS"
# 场景 10 前置数据 + 项目创建走 owner2(非管理员)
JO="$T/owner2"
P1=$(api "$JO" POST /api/projects "{\"name\":\"验收甲-$TS\"}")
PID1=$(echo "$P1" | body | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' 2>/dev/null)
P2=$(api "$JO" POST /api/projects "{\"name\":\"验收乙-$TS\"}")
PID2=$(echo "$P2" | body | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' 2>/dev/null)
[ -n "${PID1:-}" ] && [ -n "${PID2:-}" ] && ok "准备:两项目创建($PID1 / $PID2)" || { bad "准备:项目创建" "$P1"; exit 1; }
RDM() { # jar cmd json
api "$1" POST "/api/commands/$2" "{\"params\":$3}" | body
}
say "== 场景 1:受管项写入拒绝(user/project 层,含手造请求)=="
r=$(api "$JO" PUT /api/settings/user '{"models":{"chat":{"model":"m-hack"}}}' | code)
[ "$r" = "400" ] && ok "1a user 写 models.* 被拒(400)" || bad "1a" "$r"
r=$(api "$JO" PUT "/api/settings/project/$PID1" '{"models":{"chat":{"apiKey":"sk-hack"}}}' | code)
[ "$r" = "400" ] && ok "1b project 写 models.* 被拒(400)" || bad "1b" "$r"
# 系统面非管理员 403;旧覆盖不影响运行由 CI 覆盖(Backend #188)
r=$(api "$JO" PUT /api/settings/system '{"models":{"chat":{"model":"x"}}}' | code)
[ "$r" = "403" ] && ok "1c 非管理员写系统面 403" || bad "1c" "$r"
say "== 场景 2:目录查找成员(账号/分页;姓名过滤待发版 tag 后全量)=="
out=$(RDM "$JO" rdm.account.list "{\"projectId\":\"$PID1\",\"limit\":5}")
n=$(echo "$out" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d.get("accounts",[])))' 2>/dev/null)
[ "${n:-0}" -gt 0 ] && ok "2a 目录分页浏览($n 条)" || bad "2a" "$out"
say "== 场景 3:重复添加与最后负责人保护 =="
RDM "$JO" rdm.project.member.add "{\"projectId\":\"$PID1\",\"member\":\"mem1\",\"role\":\"member\"}" >/dev/null
out=$(RDM "$JO" rdm.project.member.add "{\"projectId\":\"$PID1\",\"member\":\"mem1\",\"role\":\"owner\"}")
echo "$out" | grep -q "已是成员" && ok "3a 重复添加拒绝且不隐式变更" || bad "3a" "$out"
out=$(RDM "$JO" rdm.project.member.setRole "{\"projectId\":\"$PID1\",\"member\":\"owner2\",\"role\":\"member\"}")
echo "$out" | grep -q "最后一位负责人" && ok "3b 降级唯一负责人被拒" || bad "3b" "$out"
out=$(RDM "$JO" rdm.project.member.setRole "{\"projectId\":\"$PID1\",\"member\":\"mem1\",\"role\":\"owner\"}")
echo "$out" | grep -q '"role":"owner"' && ok "3c 显式升级成功" || bad "3c" "$out"
RDM "$JO" rdm.project.member.setRole "{\"projectId\":\"$PID1\",\"member\":\"mem1\",\"role\":\"member\"}" >/dev/null
# 成员补充:三权三人入项目
for u in w1 a1 r1; do RDM "$JO" rdm.project.member.add "{\"projectId\":\"$PID1\",\"member\":\"$u\"}" >/dev/null; done
say "== 场景 4:文档目录与职责分配对象校验 =="
# 建文档(经 RDM 世界初始化:用 rdm.init 或模板?最小:rdm.duty.catalog 应答;docId 误提交由场景 5 的 assign 覆盖
cat_out=$(RDM "$JO" rdm.duty.catalog "{\"projectId\":\"$PID1\"}")
echo "$cat_out" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert "docs" in d' 2>/dev/null && ok "4a duty.catalog 目录可读" || bad "4a" "$cat_out"
# 章节误提交:构造不存在的 docId → assign 拒绝
out=$(RDM "$JO" rdm.review.assign.doc "{\"projectId\":\"$PID1\",\"docId\":\"section-not-doc\",\"author\":\"w1\",\"reviewers\":[\"a1\"],\"approver\":\"r1\"}")
echo "$out" | grep -qE "不存在|失败|错误" && ok "4b 不存在对象整笔拒绝" || bad "4b" "$out"
say "== 场景 5:非成员职责目标被拒;移出成员保存被拒 =="
out=$(RDM "$JO" rdm.review.assign.doc "{\"projectId\":\"$PID1\",\"docId\":\"any\",\"author\":\"zombie-$TS\",\"reviewers\":[\"a1\"],\"approver\":\"r1\"}")
echo "$out" | grep -q "成员" && ok "5a 非成员职责目标被拒" || bad "5a" "$out"
# mem1 移出后 setRole 保存拒绝
RDM "$JO" rdm.project.member.remove "{\"projectId\":\"$PID1\",\"member\":\"mem1\"}" >/dev/null
out=$(RDM "$JO" rdm.project.member.setRole "{\"projectId\":\"$PID1\",\"member\":\"mem1\",\"role\":\"owner\"}")
echo "$out" | grep -q "不是" && ok "5b 移出成员的成员面操作被拒" || bad "5b" "$out"
say "== 场景 6:批量整批拒绝(预检列原因)=="
out=$(RDM "$JO" rdm.review.assign.doc.batch "{\"projectId\":\"$PID1\",\"docs\":[{\"docId\":\"x1\",\"author\":\"w1\",\"reviewers\":[\"a1\"],\"approver\":\"r1\"},{\"docId\":\"x2\",\"author\":\"zombie-$TS\",\"reviewers\":[\"a1\"],\"approver\":\"r1\"}]}")
echo "$out" | grep -q "整批拒绝" && ok "6 批量含非成员整批拒绝并列原因" || bad "6" "$out"
say "== 场景 7:单成员解释与矩阵同源 =="
chk=$(RDM "$JO" rdm.perm.check "{\"projectId\":\"$PID1\",\"member\":\"owner2\"}")
mtx=$(RDM "$JO" rdm.perm.matrix "{\"projectId\":\"$PID1\"}")
same=$(python3 - "$chk" "$mtx" <<'PY'
import json,sys
c=json.loads(sys.argv[1]); m=json.loads(sys.argv[2])
row=[r for r in m.get("rows",[]) if r["member"]==c["member"]]
if not row: print("no-row"); sys.exit(1)
ca={a["action"]:(a["allowed"],a.get("label"),a.get("scope")) for a in c["actions"]}
ra={a["action"]:(a["allowed"],a.get("label"),a.get("scope")) for a in row[0]["actions"]}
print("same" if ca==ra else "diff")
PY
)
[ "$same" = "same" ] && ok "7 单成员详情与矩阵单元格一致" || bad "7" "$same"
# 普通成员自省:a1 查自己
ja="$T/a1"
out=$(RDM "$ja" rdm.perm.check "{\"projectId\":\"$PID1\",\"member\":\"a1\"}")
echo "$out" | grep -q '"actions"' && ok "7b 普通成员自省可用" || bad "7b" "$out"
say "== 场景 8:系统动作不可项目覆盖 =="
out=$(RDM "$JO" rdm.perm.set "{\"projectId\":\"$PID1\",\"member\":\"a1\",\"action\":\"project.create\",\"allow\":true}")
echo "$out" | grep -q "系统范围" && ok "8a project.create 覆盖被服务端拒绝" || bad "8a" "$out"
out=$(RDM "$JO" rdm.perm.set "{\"projectId\":\"$PID1\",\"member\":\"a1\",\"action\":\"doc.write\",\"allow\":true}")
echo "$out" | grep -q '"ok"' || echo "$out" | grep -q 'ok' && ok "8b 项目范围动作覆盖可用" || bad "8b" "$out"
out=$(RDM "$JO" rdm.perm.unset "{\"projectId\":\"$PID1\",\"member\":\"a1\",\"action\":\"doc.write\"}")
echo "$out" | grep -q '"restored":true' && ok "8c 恢复默认(实际撤销)" || bad "8c" "$out"
say "== 场景 9:迟到响应/断网(UI 行为)=="
say "PASS 9 (UI 层:Picker 迟到响应代次丢弃/空态/重试由 UI #53 单测覆盖;浏览器目视复验归用户验收)"
say "== 场景 10:共享配置一致 + 私域边界 =="
# 同项目不同成员读项目设置一致(当前 user/project 均无受管项显示)
s_o=$(api "$JO" GET "/api/settings?project=$PID1" | body | python3 -c 'import json,sys; d=json.load(sys.stdin); print(json.dumps(d.get("defaults",{}).get("ui",{}),sort_keys=True))' 2>/dev/null)
ja1="$T/w1"
s_w=$(api "$ja1" GET "/api/settings?project=$PID1" | body | python3 -c 'import json,sys; d=json.load(sys.stdin); print(json.dumps(d.get("defaults",{}).get("ui",{}),sort_keys=True))' 2>/dev/null)
[ "$s_o" = "$s_w" ] && ok "10a 同项目成员共享默认一致" || bad "10a" "$s_o vs $s_w"
# 私域:w1 的会话列表对 owner2 不可见(项目内他人会话)
con=$(api "$ja1" POST "/api/projects/$PID1/conversations" '{"title":"私域验证"}' | body | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' 2>/dev/null)
r=$(api "$JO" GET "/api/projects/$PID1/conversations" | body | grep -c "$con" || true)
[ "$r" = "0" ] && ok "10b 他人会话私域不可见" || bad "10b" "leak=$r"
say ""
say "== 汇总:PASS=$PASS FAIL=$FAIL =="
[ ${#FAILED[@]} -gt 0 ] && printf '失败场景:%s\n' "${FAILED[*]}"
[ "$FAIL" = "0" ]