Add the duties acceptance e2e harness #576
File diff suppressed because one or more lines are too long
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env bash
|
||||
# e2e-duties.sh —— 文档职责三视图真实浏览器验收(2026-10-16 S9 验收发现 2/4 复测结论的固化)。
|
||||
#
|
||||
# 动线(与 scripts/e2e.sh 同族;种子改用 2026-10-13 后的凭据契约):
|
||||
# 1. 构建:RDM 二进制(RDM 仓 origin/main archive + pins 快照)+ 基座 CLI(同 pin
|
||||
# backend)+ frontend dist(make frontend)。
|
||||
# 2. 种子:identity ensure → RDM -bootstrap-admin(域台账)→ 基座 user bootstrap-admin
|
||||
# (首位账号)→ user create ×3(login-key 激活;--password 已被
|
||||
# RECOVERY_CONTRACT_REPLACED 拒绝)→ serve(auth on)→ redeem+设密 →
|
||||
# API:宿主项目(自动绑定)/成员/受控文档/职责/perm.set duty.assign/整文送审。
|
||||
# 3. 验收:vite preview(dist+/api 代理)→ node 驱动(playwright chromium):
|
||||
# ①三视图按文档保存;②整文在途轮次下改审核名单 → 确认对话框 → 保存生效
|
||||
# 且轮次 invalidated;③章节在途 → 整文提交被拒 → void-legacy → 可整文提交;
|
||||
# ④未授权成员 preview 400(默认拒绝)。
|
||||
# 4. 收尾:停 serve/preview,工件(截图/results/日志)留 $OUT。
|
||||
#
|
||||
# 用法:bash scripts/e2e-duties.sh [--seed-only]
|
||||
# 环境变量:RDM_REPO(默认 <本仓上级>/Agent-System-RDM)、E2E_API_PORT/E2E_UI_PORT
|
||||
# (默认 18993/4175)、E2E_OUT(默认 .tmp/e2e-duties/<ts>)。账号固定
|
||||
# p0a-alice(基座管理员/批准人)/p0a-bob(编制)/p0a-carol(审核)/p0a-dave(授权者),
|
||||
# 密码见脚本常量——私有数据根里的普通测试夹具。
|
||||
# 依赖:go、node/npm、curl、python3;首次运行 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:-18993}
|
||||
UI_PORT=${E2E_UI_PORT:-4175}
|
||||
API_BASE="http://127.0.0.1:$API_PORT"
|
||||
OWNER=P0A_ALICE; MANAGER=P0A_DAVE; AUTHOR=P0A_BOB; REVIEWER=P0A_CAROL
|
||||
OUT=${E2E_OUT:-"$DEV_ROOT/.tmp/e2e-duties/$(date +%Y%m%d-%H%M%S)"}
|
||||
case "$OUT" in /*) ;; *) OUT="$PWD/$OUT" ;; esac
|
||||
mkdir -p "$OUT"
|
||||
|
||||
log() { printf '\033[36m[e2e-duties]\033[0m %s\n' "$*"; }
|
||||
die() { printf '\033[31m[e2e-duties][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 at $RDM_REPO"
|
||||
for p in "$API_PORT" "$UI_PORT"; do
|
||||
timeout 2 bash -c "echo > /dev/tcp/127.0.0.1/$p" 2>/dev/null && die "port $p in use"
|
||||
done
|
||||
|
||||
# ── 1. 构建(与 e2e.sh 同法:archive 固定输入,pins 快照拷贝)─────────────
|
||||
log "build RDM binary (archive origin/main + snapshot pins)"
|
||||
RDM_SHA=$(git -C "$RDM_REPO" rev-parse origin/main) || die "no origin/main"
|
||||
git -C "$RDM_REPO" submodule update --init --recursive --quiet || die "materialize pins failed"
|
||||
PIN_SHA_WANT=$(git -C "$RDM_REPO" ls-tree "$RDM_SHA" pins/agent-system-backend | awk '{print $3}')
|
||||
PIN_SHA_MAIN=$(git -C "$RDM_REPO/pins/agent-system-backend" rev-parse HEAD)
|
||||
[ "$PIN_SHA_MAIN" = "$PIN_SHA_WANT" ] || die "pins/backend ($PIN_SHA_MAIN) != origin/main pin ($PIN_SHA_WANT)"
|
||||
RDM_WT="$OUT/rdm-src"; mkdir -p "$RDM_WT"
|
||||
git -C "$RDM_REPO" archive "$RDM_SHA" | tar -x -C "$RDM_WT"
|
||||
mkdir -p "$RDM_WT/pins"
|
||||
tar -C "$RDM_REPO/pins" --exclude=.git -cf - agent-system-backend | tar -C "$RDM_WT/pins" -xf -
|
||||
(cd "$RDM_WT" && go build -o "$OUT/agent-system-rdm" .) || die "rdm build failed"
|
||||
log "build base CLI (same pinned backend as the RDM binary)"
|
||||
(cd "$RDM_WT/pins/agent-system-backend" && go build -o "$OUT/agent-system" ./cmd/agent-system) || die "cli build failed"
|
||||
log "build frontend dist (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"
|
||||
CLI="$OUT/agent-system"
|
||||
|
||||
log "serve 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=$!
|
||||
for i in $(seq 1 40); do
|
||||
[ "$(curl -s -o /dev/null -w '%{http_code}' "$API_BASE/api/me" || echo 000)" != 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=""; }; }
|
||||
cleanup() {
|
||||
stop_serve
|
||||
if [ -n "${UI_PID:-}" ] && ps -p "$UI_PID" -o args= 2>/dev/null | grep -Fq -- "vite preview --port $UI_PORT"; then
|
||||
kill "$UI_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
start_serve
|
||||
|
||||
log "identity ensure + RDM ledger bootstrap ($OWNER)"
|
||||
ID_OK=0
|
||||
for i in $(seq 1 15); do
|
||||
if $CLI identity ensure --data "$DATA" --instance-id "e2e-duties" >/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"; }
|
||||
$RDM -bootstrap-admin "$OWNER" >/dev/null || die "rdm ledger bootstrap failed"
|
||||
log "base first account + members (login-key 契约)"
|
||||
$CLI user bootstrap-admin --data "$DATA" --owner "$OWNER" --out-file "$OUT/$OWNER.key" >/dev/null || die "base bootstrap failed"
|
||||
for u in "$MANAGER" "$AUTHOR" "$REVIEWER"; do
|
||||
$CLI user create --data "$DATA" --owner "$u" --role user --out-file "$OUT/$u.key" >/dev/null || die "user create $u failed"
|
||||
done
|
||||
|
||||
activate() { # activate <user> <password>:redeem login-key → 设密 → 登录
|
||||
# 注意:jar 不得与 u 同一条 local 声明——local 的各赋值在同一命令行先展开,
|
||||
# $u 会取到外层残留值(曾致四账号会话写入同一个 jar)。
|
||||
local u=$1 pw=$2 k
|
||||
local jar="$OUT/$u.jar"
|
||||
k=$(cat "$OUT/$u.key")
|
||||
curl -sf -D "$OUT/$u.redeem.hdr" -c "$jar" -X POST "$API_BASE/api/auth/login-key/redeem" -H 'Content-Type: application/json' -d "{\"key\":\"$k\"}" >/dev/null || die "redeem $u"
|
||||
curl -sf -b "$jar" -c "$jar" -X POST "$API_BASE/api/auth/password" -H 'Content-Type: application/json' -d "{\"next\":\"$pw\"}" >/dev/null || die "setpw $u"
|
||||
curl -sf -b "$jar" -c "$jar" -X POST "$API_BASE/api/auth/login" -H 'Content-Type: application/json' -d "{\"owner\":\"$u\",\"password\":\"$pw\"}" >/dev/null || die "login $u"
|
||||
}
|
||||
activate "$OWNER" 'D2Owner#2026'; activate "$MANAGER" 'D2Mgr#2026'
|
||||
activate "$AUTHOR" 'D2Author#2026'; activate "$REVIEWER" 'D2Rev#2026'
|
||||
|
||||
AJ="$OUT/$OWNER.jar"
|
||||
cmd() { # cmd <jar> <id> <params> → payload;rejected 即死
|
||||
curl -sf -b "$1" -X POST "$API_BASE/api/commands/$2" -H 'Content-Type: application/json' -d "{\"params\":$3}" \
|
||||
| python3 -c 'import json,sys; d=json.load(sys.stdin); r=(d or {}).get("receipt",{}); sys.exit(3) if r.get("state")=="rejected" else None; print(json.dumps(d.get("payload", d), ensure_ascii=False))' \
|
||||
|| die "command $2 rejected"
|
||||
}
|
||||
log "seed: host project + members + doc + duties + grant"
|
||||
log "me probe: $(curl -s -b "$AJ" "$API_BASE/api/me" | head -c 160) | jar=$(ls -la "$AJ" 2>/dev/null | awk '{print $5}')"
|
||||
PIDRESP=$(curl -s -w '\n%{http_code}' -b "$AJ" -X POST "$API_BASE/api/projects" -H 'Content-Type: application/json' -d '{"name":"职责验收夹具"}')
|
||||
CODE=${PIDRESP##*$'\n'}; BODY=${PIDRESP%$'\n'*}
|
||||
[ "$CODE" = 200 ] || die "host project create: HTTP $CODE body=${BODY:0:200}"
|
||||
PID=$(printf '%s' "$BODY" | python3 -c 'import json,sys; print(json.load(sys.stdin)["settings"]["rdm.projectId"])')
|
||||
for m in "$MANAGER" "$AUTHOR" "$REVIEWER"; do
|
||||
cmd "$AJ" rdm.project.member.add "{\"projectId\":\"$PID\",\"member\":\"$m\",\"role\":\"member\"}" >/dev/null
|
||||
done
|
||||
DOC=$(cmd "$AJ" rdm.docs.instantiate "{\"projectId\":\"$PID\",\"docType\":\"rd_proposal\",\"templateId\":\"dhf.2750fbaf1f67d10c\",\"version\":\"1.0.0\",\"title\":\"职责验收建议书\"}" \
|
||||
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("docId") or d.get("id") or "")')
|
||||
[ -n "$DOC" ] || { DOC=$(cmd "$AJ" rdm.duty.catalog "{\"projectId\":\"$PID\"}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["docs"][0]["docId"])'); }
|
||||
cmd "$AJ" rdm.review.assign.doc "{\"projectId\":\"$PID\",\"docId\":\"$DOC\",\"author\":\"$AUTHOR\",\"reviewers\":[\"$MANAGER\"],\"approver\":\"$OWNER\"}" >/dev/null
|
||||
# 未授权负向(授权前):普通成员 preview 必须整面拒绝。
|
||||
NEG=$(curl -s -b "$OUT/$REVIEWER.jar" -X POST "$API_BASE/api/commands/rdm.duty.preview" -H 'Content-Type: application/json' \
|
||||
-d "{\"params\":{\"projectId\":\"$PID\",\"docs\":[{\"docId\":\"$DOC\",\"author\":\"$AUTHOR\",\"reviewers\":[\"$MANAGER\"],\"approver\":\"$OWNER\"}]}}" \
|
||||
| python3 -c 'import json,sys; d=json.load(sys.stdin); r=(d or {}).get("receipt",{}); print(r.get("state",""), r.get("reason",""))')
|
||||
case "$NEG" in rejected*duty.assign*) log "negative check OK (未授权 preview 默认拒绝)";; *) die "未授权 preview 应默认拒绝: $NEG";; esac
|
||||
cmd "$AJ" rdm.perm.set "{\"projectId\":\"$PID\",\"member\":\"$MANAGER\",\"action\":\"duty.assign\",\"allow\":true}" >/dev/null
|
||||
# 授权后正向(P0-A 回归):授权者 preview 必须 200。
|
||||
curl -sf -b "$OUT/$MANAGER.jar" -X POST "$API_BASE/api/commands/rdm.duty.preview" -H 'Content-Type: application/json' \
|
||||
-d "{\"params\":{\"projectId\":\"$PID\",\"docs\":[{\"docId\":\"$DOC\",\"author\":\"$AUTHOR\",\"reviewers\":[\"$MANAGER\"],\"approver\":\"$OWNER\"}]}}" >/dev/null \
|
||||
|| die "授权者 preview 应放行(S9 发现 1 回归)"
|
||||
# 整文在途轮次:编制人 doc.submit(身份只认认证主体 → 用 AUTHOR 会话)。
|
||||
cmd "$OUT/$AUTHOR.jar" rdm.review.doc.submit "{\"projectId\":\"$PID\",\"docId\":\"$DOC\"}" >/dev/null
|
||||
log "seed done: project=$PID doc=$DOC(整文第 1 轮在途)"
|
||||
|
||||
# ── 3. 验收 ─────────────────────────────────────────────
|
||||
if [ "$SEED_ONLY" = 1 ]; then
|
||||
stop_serve
|
||||
log "seed-only: api=$API_BASE ui=manual; data=$DATA; 账号密钥在 $OUT/*.key"
|
||||
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
|
||||
(cd "$DEV_ROOT/modules/frontend" && npx playwright install chromium >/dev/null 2>&1 || true)
|
||||
|
||||
cat > "$OUT/driver.cjs" <<'DRIVER'
|
||||
// 文档职责真实浏览器驱动:三视图保存 + 整文在途确认对话框 + 轮次失效断言。
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('fs');
|
||||
const BASE = process.env.DRIVER_BASE_URL, API = process.env.DRIVER_API_URL;
|
||||
const OUT = process.env.DRIVER_OUT, HOST = process.env.DRIVER_PROJECT, PID = process.env.DRIVER_PID, DOC = process.env.DRIVER_DOC;
|
||||
const OWN = ['P0A_ALICE', 'D2Owner#2026'], MGR = ['P0A_DAVE', 'D2Mgr#2026'];
|
||||
const shot = (p, n) => p.screenshot({ path: `${OUT}/${n}.png`, fullPage: true }).catch(() => {});
|
||||
const results = [];
|
||||
const record = (id, ok, detail) => { results.push({ id, ok, detail }); console.log(`${ok ? 'PASS' : 'FAIL'} [${id}] ${detail}`); };
|
||||
async function login(ctx, [owner, password]) {
|
||||
const page = await ctx.newPage();
|
||||
await page.goto(BASE + '/');
|
||||
await page.getByPlaceholder('由管理员分配').fill(owner);
|
||||
await page.getByPlaceholder('初始密码或常用密码').fill(password);
|
||||
await page.getByRole('button', { name: '登录', exact: true }).click();
|
||||
await page.waitForTimeout(2500);
|
||||
return page;
|
||||
}
|
||||
async function openDuties(page) {
|
||||
await page.getByText(HOST).first().click(); await page.waitForTimeout(1500);
|
||||
await page.getByRole('button', { name: '项目设置', exact: true }).click(); await page.waitForTimeout(1000);
|
||||
await page.getByRole('tab', { name: '文档职责', exact: true }).click(); await page.waitForTimeout(1200);
|
||||
}
|
||||
async function saveDuties(page, tag) {
|
||||
await page.getByRole('button', { name: '保存职责' }).click();
|
||||
await page.waitForTimeout(1800);
|
||||
let confirms = 0, dialogText = '';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const btn = page.getByRole('button', { name: /^(确认保存|确认并保存|确认替换|确认|确定|仍要保存|继续保存)$/ }).first();
|
||||
if (await btn.isVisible().catch(() => false)) {
|
||||
await shot(page, `${tag}-dialog-${i}`);
|
||||
const body = await page.locator('body').innerText().catch(() => '');
|
||||
dialogText = body.split('\n').filter(l => /在途|失效|替换|确认/.test(l)).slice(0, 5).join(' | ').slice(0, 260);
|
||||
console.log(`[dialog:${tag}] ${dialogText}`);
|
||||
await btn.click(); confirms++;
|
||||
await page.waitForTimeout(1800);
|
||||
} else break;
|
||||
}
|
||||
await shot(page, `${tag}-final`);
|
||||
return { confirms, dialogText };
|
||||
}
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const ownerCtx = await browser.newContext(), mgrCtx = await browser.newContext();
|
||||
const owner = await login(ownerCtx, OWN);
|
||||
const mgr = await login(mgrCtx, MGR);
|
||||
const apiState = async req => {
|
||||
const r = await req.post(`${API}/api/commands/rdm.review.doc.status`, { data: { params: { projectId: PID, docId: DOC } } });
|
||||
return (await r.json().catch(() => ({}))).payload || {};
|
||||
};
|
||||
|
||||
// 场景②(核心):整文在途轮次(种子 doc.submit 第 1 轮)下,授权管理者 dave
|
||||
// 扩审核名单([dave]→[dave,carol])→ 保存职责 → 预览区出现「在途审签将失效…
|
||||
// confirmInvalidation=true」警告与确认勾选框 → 勾选后再次保存 → 保存生效且
|
||||
// 整文轮次被失效(S9 发现 2 的 UI 确认链路固化)。
|
||||
await openDuties(mgr);
|
||||
await mgr.getByRole('tab', { name: '按文档', exact: true }).click(); await mgr.waitForTimeout(1200);
|
||||
await mgr.getByLabel('选择文档').selectOption(DOC); await mgr.waitForTimeout(1200);
|
||||
await mgr.getByLabel(`P0A_CAROL 在 ${process.env.DRIVER_DOC_TITLE} 的职责`).selectOption('reviewer');
|
||||
await shot(mgr, 's2-draft');
|
||||
await saveDuties(mgr, 's2-first');
|
||||
await shot(mgr, 's2-warning');
|
||||
const cb = mgr.locator('input[type="checkbox"]').last();
|
||||
const cbVisible = await cb.isVisible().catch(() => false);
|
||||
if (cbVisible) { await cb.check(); await mgr.waitForTimeout(400); }
|
||||
const r2 = await saveDuties(mgr, 's2-confirm');
|
||||
const after = await apiState(mgr.request);
|
||||
const resp2 = await mgr.request.post(`${API}/api/commands/rdm.duty.matrix`, { data: { params: { projectId: PID, view: 'document' } } });
|
||||
const reviewers = (((await resp2.json()).payload || {}).cells || []).filter(c => c.docId === DOC && c.duty === 'reviewer').map(c => c.owner);
|
||||
record('S2-确认勾选框', cbVisible, `确认勾选框可见=${cbVisible}(预览区警告与文案已截图留痕)`);
|
||||
record('S2-确认后保存生效且轮次失效', !!after.submission && after.submission.state === 'invalidated' && reviewers.length === 2,
|
||||
`after.submission.state=${after.submission ? after.submission.state : 'null'} reviewers=${JSON.stringify(reviewers)} confirms=${r2.confirms}`);
|
||||
|
||||
// 场景①(幂等回归):同一目标经 API 再保存 → applied=0 unchanged=1,版本不变。
|
||||
const resp1 = await mgr.request.post(`${API}/api/commands/rdm.review.assign.doc.batch`, { data: { params: { projectId: PID, docs: [{ docId: DOC, author: process.env.DRIVER_AUTHOR, reviewers: [process.env.DRIVER_MANAGER, process.env.DRIVER_REVIEWER], approver: process.env.DRIVER_APPROVER }] } } });
|
||||
const b1 = (await resp1.json().catch(() => ({}))).payload || {};
|
||||
record('S1-幂等计数', b1.applied === 0 && b1.unchanged === 1, `applied=${b1.applied} unchanged=${b1.unchanged}`);
|
||||
await mgrCtx.close();
|
||||
await ownerCtx.close();
|
||||
|
||||
await browser.close();
|
||||
fs.writeFileSync(`${OUT}/results.json`, JSON.stringify(results, null, 2));
|
||||
const bad = results.filter(r => !r.ok);
|
||||
console.log(`\n==== driver: ${results.length - bad.length}/${results.length} PASS ====`);
|
||||
if (bad.length) { bad.forEach(b => console.log('FAIL', b.id, b.detail)); process.exit(1); }
|
||||
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
|
||||
DRIVER
|
||||
|
||||
log "run driver"
|
||||
cd "$DEV_ROOT/modules/frontend"
|
||||
DRIVER_BASE_URL="http://127.0.0.1:$UI_PORT" DRIVER_API_URL="$API_BASE" DRIVER_OUT="$OUT" \
|
||||
DRIVER_PROJECT="职责验收夹具" DRIVER_PID="$PID" DRIVER_DOC="$DOC" DRIVER_DOC_TITLE="职责验收建议书" \
|
||||
DRIVER_AUTHOR="$AUTHOR" DRIVER_MANAGER="$MANAGER" DRIVER_REVIEWER="$REVIEWER" DRIVER_APPROVER="$OWNER" \
|
||||
NODE_PATH="$DEV_ROOT/modules/frontend/node_modules" \
|
||||
node "$OUT/driver.cjs" 2>&1 | tee "$OUT/driver.log"
|
||||
[ "${PIPESTATUS[0]}" = 0 ] || die "driver failed (log: $OUT/driver.log)"
|
||||
|
||||
# ── 4. API 断言:整文在途处置与章节迁移语义 ────────────────────
|
||||
AJ="$OUT/$OWNER.jar"; BJ="$OUT/$AUTHOR.jar"
|
||||
subState=$(curl -s -b "$AJ" -X POST "$API_BASE/api/commands/rdm.review.doc.status" -H 'Content-Type: application/json' \
|
||||
-d "{\"params\":{\"projectId\":\"$PID\",\"docId\":\"$DOC\"}}" \
|
||||
| python3 -c 'import json,sys; d=json.load(sys.stdin); s=(d.get("payload") or {}).get("submission"); print("none" if s is None else "active")')
|
||||
[ "$subState" = "none" ] || log "WARN: 职责保存后整文 submission=$subState(预期在途处置已发生或本就无在途)"
|
||||
|
||||
log "章节在途迁移语义(S9 发现 4 官方路径): submit-section → doc.submit 拒 → void-legacy → doc.submit 过"
|
||||
SEC=$(curl -s -b "$AJ" -X POST "$API_BASE/api/commands/rdm.duty.get" -H 'Content-Type: application/json' \
|
||||
-d "{\"params\":{\"projectId\":\"$PID\",\"docId\":\"$DOC\"}}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["payload"]["sections"][0]["sectionId"])')
|
||||
curl -sf -b "$BJ" -X POST "$API_BASE/api/commands/rdm.review.submit" -H 'Content-Type: application/json' -d "{\"params\":{\"projectId\":\"$PID\",\"sectionId\":\"$SEC\"}}" >/dev/null \
|
||||
|| log "章节提交被拒(可接受:文档生命周期状态依赖前序步骤)"
|
||||
REJECT_MSG=$(curl -s -b "$BJ" -X POST "$API_BASE/api/commands/rdm.review.doc.submit" -H 'Content-Type: application/json' \
|
||||
-d "{\"params\":{\"projectId\":\"$PID\",\"docId\":\"$DOC\"}}" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("receipt",{}).get("reason",""))' || true)
|
||||
log "整文提交对章节在途的拒绝信息: ${REJECT_MSG:0:80}"
|
||||
curl -sf -b "$BJ" -X POST "$API_BASE/api/commands/rdm.review.doc.void-legacy" -H 'Content-Type: application/json' \
|
||||
-d "{\"params\":{\"projectId\":\"$PID\",\"docId\":\"$DOC\",\"confirm\":true}}" >/dev/null || die "void-legacy 应成功"
|
||||
curl -sf -b "$BJ" -X POST "$API_BASE/api/commands/rdm.review.doc.submit" -H 'Content-Type: application/json' \
|
||||
-d "{\"params\":{\"projectId\":\"$PID\",\"docId\":\"$DOC\"}}" >/dev/null || die "void-legacy 后整文提交应通过"
|
||||
log "PASS 章节在途迁移语义(void-legacy → 整文提交)"
|
||||
|
||||
log "PASS 全部场景;工件在 $OUT"
|
||||
Reference in New Issue
Block a user