diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/implement.md b/.trellis/tasks/09-26-approve-success-state-unchanged/implement.md index 8303778a..6d070f32 100644 --- a/.trellis/tasks/09-26-approve-success-state-unchanged/implement.md +++ b/.trellis/tasks/09-26-approve-success-state-unchanged/implement.md @@ -1,4 +1,13 @@ -## 2026-09-26 调查进度(当前) +## 2026-09-26 根因确认与修复(当前) + +- [x] 带诊断复现命中(CG-20260926-0006,任务 48d14805):浏览器 01.973 发 GET,**05.049 收到 pending**;服务端 02.255 收到同一 GET,**05.598 才读库且读到 completed**。浏览器拿到的响应早于服务端读库,来源只能是 Service Worker 缓存。 +- [x] 根因:`client/vite.config.ts` 的 `app-api-runtime` 规则对 `/api/me`、`/api/workflow/tasks*`、`/api/workflow/approvers*`、`/api/notifications*` 用 `NetworkFirst` + `networkTimeoutSeconds: 3`;网络响应超过 3 秒时 SW 返回审批前缓存的旧响应,真实响应被丢弃。自 2026-03 P21PWA2(9112383d)起存在,生产同样受影响(慢网络/企微手机下审批后页面显示未通过,再点报「已处理」,待办列表可能显示已处理单据)。审批写入本身从未丢失。 +- [x] 修复:去掉 `networkTimeoutSeconds`,仅网络失败(离线)才回退缓存;退出登录清理该缓存(`client/src/pwa.ts:201`)不变。新增 `server/tests/unit/utils/client-pwa-api-cache.test.js`(有修复通过、去掉修复失败);构建产物 `dist/sw.js` 已无超时。 +- [ ] 修复后复现脚本 3 轮验证(修复前 3 轮命中 2 轮)。 +- [ ] 通过后 `verify-candidate` 取得 PASS 回执。 +- 后续另议:`/api/workflow/tasks*` 含报销审批内容仍会被 SW 缓存供离线读取,与「报销审批证据不进入离线缓存」口径是否冲突待确认;采购提交事务 P2028 见下方。 + +## 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)。 diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/prd.md b/.trellis/tasks/09-26-approve-success-state-unchanged/prd.md index 510cb7dd..67993bb2 100644 --- a/.trellis/tasks/09-26-approve-success-state-unchanged/prd.md +++ b/.trellis/tasks/09-26-approve-success-state-unchanged/prd.md @@ -24,7 +24,14 @@ ## Acceptance Criteria -- [ ] 在保留库的环境复现至少一次,并记录失败单据任务/实例/历史的库内真实状态 -- [ ] 给出有证据支撑的根因(代码位置 + 机制),区分测试问题与产品缺陷 -- [ ] 若为产品缺陷:说明影响实体范围,及 `b1d25d3c` 是否同样存在 +- [x] 在保留库的环境复现至少一次,并记录失败单据任务/实例/历史的库内真实状态 +- [x] 给出有证据支撑的根因(代码位置 + 机制),区分测试问题与产品缺陷 +- [x] 若为产品缺陷:说明影响实体范围,及 `b1d25d3c` 是否同样存在(SW 规则覆盖 /api/me、workflow/tasks、approvers、notifications;自 2026-03 9112383d 起存在,生产同样受影响) - [ ] 修复后 `verify-candidate` 完整通过(取得 PASS 回执) + +## Test Plan + +- 回归单测:`server/tests/unit/utils/client-pwa-api-cache.test.js` 锁定 `app-api-runtime` 不得设置网络超时(去掉修复即失败);`client-pwa-lifecycle.test.js` 保持退出登录清理运行时缓存。 +- 构建产物核对:`client/dist/sw.js` 中 `app-api-runtime` 的 `NetworkFirst` 不含 `networkTimeoutSeconds`。 +- 负载复现:`research/repro.sh`(采购 ×8 + permission-direct-access ×8,4 worker)修复前 3 轮命中 2 轮,修复后 3 轮 0 命中。 +- 完整门禁:`verify-candidate` 取得 PASS 回执。 diff --git a/.trellis/tasks/09-26-approve-success-state-unchanged/task.json b/.trellis/tasks/09-26-approve-success-state-unchanged/task.json index 65fcac25..671190a1 100644 --- a/.trellis/tasks/09-26-approve-success-state-unchanged/task.json +++ b/.trellis/tasks/09-26-approve-success-state-unchanged/task.json @@ -5,7 +5,7 @@ "description": "verify-candidate 34675e1a/750a184b 在4 worker负载下采购主链路3轮挂2次:经理approve返回200,随后任务详情仍pending且历史无approve;单跑每次通过。需保留数据库负载复现查库,确认是否为新引入并影响全部审批。", "status": "planning", "dev_type": null, - "scope": null, + "scope": "pwa-service-worker", "package": null, "priority": "P1", "creator": "shunli", @@ -20,9 +20,13 @@ "subtasks": [], "children": [], "parent": null, - "relatedFiles": [], + "relatedFiles": [ + "client/vite.config.ts", + "server/tests/unit/utils/client-pwa-api-cache.test.js", + "client/src/pwa.ts" + ], "notes": "", "meta": { "implementationApproval": "APPROVED_BY_USER_20260926" } -} \ No newline at end of file +} diff --git a/client/vite.config.ts b/client/vite.config.ts index c094bdce..88bcb4fc 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -136,10 +136,11 @@ export default defineConfig(({ mode }) => { url.pathname.startsWith("/api/notifications") ); }, + // Fall back to cache only when the network fails (offline). A network + // timeout here served pre-approval task state to slow-but-online clients. handler: "NetworkFirst", options: { cacheName: "app-api-runtime", - networkTimeoutSeconds: 3, cacheableResponse: { statuses: [200], }, diff --git a/server/tests/unit/utils/client-pwa-api-cache.test.js b/server/tests/unit/utils/client-pwa-api-cache.test.js new file mode 100644 index 00000000..85f2cf0c --- /dev/null +++ b/server/tests/unit/utils/client-pwa-api-cache.test.js @@ -0,0 +1,24 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const viteConfig = readFileSync(path.resolve(__dirname, "../../../../client/vite.config.ts"), "utf8"); + +function runtimeCacheRule(cacheName) { + const marker = `cacheName: "${cacheName}"`; + const at = viteConfig.indexOf(marker); + expect(at, `${cacheName} rule must exist`).toBeGreaterThan(-1); + const start = viteConfig.lastIndexOf("urlPattern:", at); + const end = viteConfig.indexOf("urlPattern:", at); + return viteConfig.slice(start, end === -1 ? undefined : end); +} + +describe("client PWA API runtime cache", () => { + it("falls back to cached API data only when the network fails, never on a slow response", () => { + const rule = runtimeCacheRule("app-api-runtime"); + expect(rule).toContain('handler: "NetworkFirst"'); + // A timeout made the service worker answer a post-approval refetch with the + // cached pre-approval task after 3s, so the page kept showing "待派发". + expect(rule).not.toMatch(/networkTimeoutSeconds/); + }); +});