diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/check.jsonl b/.trellis/tasks/09-26-approve-success-state-unchanged/check.jsonl new file mode 100644 index 00000000..e69de29b diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/implement.jsonl b/.trellis/tasks/09-26-approve-success-state-unchanged/implement.jsonl new file mode 100644 index 00000000..e69de29b diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/implement.md b/.trellis/tasks/09-26-approve-success-state-unchanged/implement.md new file mode 100644 index 00000000..8303778a --- /dev/null +++ b/.trellis/tasks/09-26-approve-success-state-unchanged/implement.md @@ -0,0 +1,8 @@ +## 2026-09-26 调查进度(当前) + +- [x] 非 managed 复现环境:`research/repro.sh`(需先 `sed` 抽取 run-web-gates.sh 1–929 行并删 9–17 行为 gates-lib.sh;库前缀须 `hyb_oa_webgate_`;直接调用 `prepareAccounts`)。采购 ×6 + permission-direct-access ×6,4 worker,约 13 分钟。 +- [x] 第 1 轮复现命中:CG-20260926-0006 页面停在「待派发」、历史仅「提交」;**保留库中该单 approved、任务 completed/approved、历史 start,approve**。结论:审批写入未丢失,问题在审批后页面刷新拿到/显示旧状态。时序:approve 06.418→09.868(200),task completed_at 07.948,页面 GET 10.289→13.099(200)。 +- [x] 第 2 轮加服务端 DIAG 读日志 + 浏览器响应监听(`research/diag-instrumentation.patch`,未提交,已从工作区还原):未复现;22 次成功审批后的 27 次读取无 pending。 +- [ ] 带诊断多跑几轮直至命中,确定旧状态来自服务端响应还是前端未应用。 +- [ ] 另行处理:负载下采购提交事务超过 Prisma 默认 5000ms 交互事务超时(P2028→500,`server/src/routes/purchaseRequests.js:1704`,两轮各 1–2 次)。生产 b1d25d3c 同写法,非本次回归。 +- 顺带出现的其他随机失败:permission-direct-access AP6(待办汇总计数)/PA6(VALID_001),负载相关,未分析。 diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/prd.md b/.trellis/tasks/09-26-approve-success-state-unchanged/prd.md new file mode 100644 index 00000000..510cb7dd --- /dev/null +++ b/.trellis/tasks/09-26-approve-success-state-unchanged/prd.md @@ -0,0 +1,30 @@ +# 审批通过返回成功但任务状态未变的并发异常调查 + +## Goal + +查清完整门禁中采购主链路间歇失败的根因:经理审批接口返回 200,但随后任务详情仍为 `pending`、处理历史无 `approve`。确认它是测试隔离问题还是产品缺陷;若为产品缺陷,确认影响范围(是否波及全部审批实体)及是否在生产 `b1d25d3c` 已存在。 + +## Background / Evidence(2026-09-26) + +- 候选 `750a184b` / `34675e1a` 共 4 轮 `verify-candidate`(2 核/8G,4 worker 固定)。`procurement-flow.spec.ts:29` 在第 2、4 轮失败,第 1 轮通过;managed `retained-local` 单跑两次均通过。第 4 轮 371/372,只剩此项。 +- 失败断言 `procurement-flow.spec.ts:118`:审批后 30s 内 `main` 不含「已完成」。 +- 第 4 轮服务端日志(相对采购 submit):+45.6s `POST /api/workflow/tasks/024c0c17…/approve` → 200(2917ms);+48.8s 同页 `GET /api/workflow/tasks/024c0c17…` → 200(3085ms)。审批期间无 warn/error 日志。 +- 失败录像:确认框「处理中…」结束后,页面仍是同一任务、状态徽标「待派发」(= task `pending`)、可执行「通过/退回/驳回」,处理历史只有「提交」。 +- 静态核对:`executeApproveWorkflowTask`(`server/src/routes/workflow.js:1395`)全部写入在同一个 `$transaction` 内;任务详情 GET 直接读库、无缓存;前端 `onSuccess` 用 `refetchTask()` 刷新(`client/src/pages/WorkflowTaskDetailPage.tsx:96`),PWA 不缓存 `/api`。采购单未匹配预算池,`confirmBudgetForEntity` 不写账。审批路径及任务详情页自 `b1d25d3c` 以来仅删除 ERP 同步。 +- 第 1 轮(通过)与第 2/4 轮(失败)请求序列一致,差异只能在数据层判断;managed 环境强制 `QA_KEEP_DB=0`。 + +## Scope + +- 非 managed 本地复现:本机 `hyb-oa-db` 临时库、`QA_KEEP_DB=1`,采购主链路与 `permission-direct-access` 并发重复运行;失败后直接查库区分「写入丢失」与「读取陈旧/错读」。 +- 根因确认后给出最小修复方案(测试隔离或产品代码),修复另行确认后实施。 + +## Out of Scope + +生产/ARM 访问、真实数据写入、push/部署、放宽断言或改用重试掩盖。 + +## Acceptance Criteria + +- [ ] 在保留库的环境复现至少一次,并记录失败单据任务/实例/历史的库内真实状态 +- [ ] 给出有证据支撑的根因(代码位置 + 机制),区分测试问题与产品缺陷 +- [ ] 若为产品缺陷:说明影响实体范围,及 `b1d25d3c` 是否同样存在 +- [ ] 修复后 `verify-candidate` 完整通过(取得 PASS 回执) diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/research/diag-instrumentation.patch b/.trellis/tasks/09-26-approve-success-state-unchanged/research/diag-instrumentation.patch new file mode 100644 index 00000000..1a11fa97 --- /dev/null +++ b/.trellis/tasks/09-26-approve-success-state-unchanged/research/diag-instrumentation.patch @@ -0,0 +1,25 @@ +diff --git a/client/tests/e2e/procurement-flow.spec.ts b/client/tests/e2e/procurement-flow.spec.ts +index aa156213..e4777e10 100644 +--- a/client/tests/e2e/procurement-flow.spec.ts ++++ b/client/tests/e2e/procurement-flow.spec.ts +@@ -30,6 +30,8 @@ test("E2E 采购主链路:员工提单→经理审批→财务归档", async ( + const title = `Playwright 采购 ${Date.now()}`; + let requestNo = ""; + ++ page.on("response", async (r) => { const u = new URL(r.url()); if (r.request().method() === "GET" && /^\/api\/workflow\/tasks\/[0-9a-f-]{36}$/.test(u.pathname)) { try { const j = await r.json(); console.log("DIAG-CLIENT", new Date().toISOString(), u.pathname.slice(-36), r.status(), j?.data?.task?.status, j?.data?.workflowInstance?.currentStatus); } catch { console.log("DIAG-CLIENT parse-fail", u.pathname); } } }); ++ page.on("request", (q) => { const u = new URL(q.url()); if (/^\/api\/workflow\/tasks\//.test(u.pathname)) console.log("DIAG-REQ", new Date().toISOString(), q.method(), u.pathname.slice(-44)); }); + await page.setViewportSize({ width: 1440, height: 960 }); + await login(page, EMPLOYEE); + await (await openNavLink(page, "采购")).click(); +diff --git a/server/src/routes/workflow.js b/server/src/routes/workflow.js +index 3bb49e47..5b5c715e 100644 +--- a/server/src/routes/workflow.js ++++ b/server/src/routes/workflow.js +@@ -2306,6 +2306,7 @@ async function workflowRoutes(fastify) { + }, + }); + ++ request.log.info({ diagTaskId: taskId, diagTaskStatus: task?.status ?? null, diagInstanceStatus: task?.workflow_instance?.current_status ?? null }, "DIAG task detail read"); + if (!task) { + const existing = await fastify.prisma.workflowTask.findUnique({ + where: { id: taskId }, diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/research/repro.sh b/.trellis/tasks/09-26-approve-success-state-unchanged/research/repro.sh new file mode 100755 index 00000000..7ce78d63 --- /dev/null +++ b/.trellis/tasks/09-26-approve-success-state-unchanged/research/repro.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +S=/tmp/claude-1000/-home-shunli-Projects-hyb-oa/118e4fec-64a1-446b-8573-31e26633f5d6/scratchpad +MODE=diag-repro +export QA_KEEP_DB=1 QA_DB_PREFIX=hyb_oa_webgate_repro +export QA_FULL_REPORT_DIR=/home/shunli/Projects/hyb-oa/artifacts/qa-flow/repro-$(date +%H%M%S) +source $S/gates-lib.sh +trap qa_cleanup EXIT +qa_select_port +export JOBS_ENABLED=true RATE_LIMIT_ENABLED=false IM_NOTIFIER_ENABLED=false +qa_export_app_env +export IM_NOTIFIER_ENABLED=false +qa_seed_synthetic_credentials +qa_prepare_db +qa_build_client +qa_start_server +QA_DB_NAME="$QA_DB_NAME" E2E_BASE_URL="$QA_BASE_URL" node --input-type=module -e ' +import { prepareAccounts } from "/home/shunli/Projects/hyb-oa/scripts/qa/prepare-local-e2e-accounts.mjs"; +const r = await prepareAccounts({ origin: process.env.E2E_BASE_URL, initialPassword: process.env.QA_SYNTHETIC_INITIAL_PASSWORD, databaseName: process.env.QA_DB_NAME }); +console.log("accounts", r.status, r.accounts.length);' +echo "REPRO_DB=$QA_DB_NAME REPRO_URL=$QA_BASE_URL SERVER_LOG=$QA_SERVER_LOG" +cd "$CLIENT_DIR" +set +e +E2E_BASE_URL="$QA_BASE_URL" E2E_MODE=local node node_modules/@playwright/test/cli.js test procurement-flow.spec.ts permission-direct-access.spec.ts \ + --repeat-each="${REPEAT:-6}" --workers=4 --retries=0 --reporter=line --output="$QA_FULL_REPORT_DIR/test-results" +echo "playwright_exit=$?" diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/task.json b/.trellis/tasks/09-26-approve-success-state-unchanged/task.json new file mode 100644 index 00000000..65fcac25 --- /dev/null +++ b/.trellis/tasks/09-26-approve-success-state-unchanged/task.json @@ -0,0 +1,28 @@ +{ + "id": "approve-success-state-unchanged", + "name": "approve-success-state-unchanged", + "title": "审批通过返回成功但任务状态未变的并发异常调查", + "description": "verify-candidate 34675e1a/750a184b 在4 worker负载下采购主链路3轮挂2次:经理approve返回200,随后任务详情仍pending且历史无approve;单跑每次通过。需保留数据库负载复现查库,确认是否为新引入并影响全部审批。", + "status": "planning", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P1", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-09-26", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": { + "implementationApproval": "APPROVED_BY_USER_20260926" + } +} \ No newline at end of file