From 5aaa628d54974f9bbab87b94272c98317a723e1d Mon Sep 17 00:00:00 2001 From: shunli Date: Tue, 24 Mar 2026 08:58:52 +0800 Subject: [PATCH] =?UTF-8?q?R31-5:=20=E6=94=B6=E5=8F=A3=E9=97=A8=E7=A6=81?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A=E5=8C=96=E4=B8=8E=E8=B5=84=E4=BA=A7=E4=BA=8B?= =?UTF-8?q?=E5=8A=A1=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/playwright.config.ts | 1 + client/tests/e2e/procurement-flow.spec.ts | 40 ++++---- ...19-57-35-r31-r30-closure-stabilization.csv | 7 ++ server/src/routes/assets.js | 92 +++++++++++++------ 4 files changed, 93 insertions(+), 47 deletions(-) create mode 100644 issues/2026-03-23_19-57-35-r31-r30-closure-stabilization.csv diff --git a/client/playwright.config.ts b/client/playwright.config.ts index 4d5ae47a..6062628c 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ expect: { timeout: 30_000, }, + workers: process.env.CI ? 2 : 4, retries: process.env.CI ? 1 : 0, use: { baseURL, diff --git a/client/tests/e2e/procurement-flow.spec.ts b/client/tests/e2e/procurement-flow.spec.ts index 39f1ebe1..eb2ae89e 100644 --- a/client/tests/e2e/procurement-flow.spec.ts +++ b/client/tests/e2e/procurement-flow.spec.ts @@ -1,25 +1,8 @@ -import { expect, test, type Locator, type Page } from "@playwright/test"; +import { expect, test, type Locator } from "@playwright/test"; +import { EMPLOYEE, FINANCE_MGR, login, logout, MANAGER } from "./helpers/auth"; import { openNavLink } from "./helpers/navigation"; -const EMPLOYEE = { identifier: "employee@hyinda.com", password: "EmployeePassword1234", name: "Employee" }; -const MANAGER = { identifier: "manager@hyinda.com", password: "ManagerPassword1234", name: "Manager" }; -const FINANCE_MGR = { identifier: "finance_mgr@hyinda.com", password: "FinanceMgrPassword1234", name: "Finance Manager" }; - -async function login(page: Page, user: { identifier: string; password: string }) { - await page.goto("/login"); - await page.getByTestId("login-identifier").fill(user.identifier); - await page.getByTestId("login-password").fill(user.password); - await page.getByTestId("login-submit").click(); - await expect(page).toHaveURL(/\/portal/); -} - -async function logout(page: Page, userDisplayName: string) { - await page.getByRole("button", { name: userDisplayName }).click(); - await page.getByRole("menuitem", { name: "退出登录" }).click(); - await expect(page).toHaveURL(/\/login/); -} - async function selectFirstBusinessOption(locator: Locator) { await expect .poll(async () => { @@ -45,10 +28,22 @@ async function selectFirstBusinessOption(locator: Locator) { test("E2E 采购主链路:员工提单→经理审批→财务归档", async ({ page }) => { const title = `Playwright 采购 ${Date.now()}`; + let requestNo = ""; await login(page, EMPLOYEE); await (await openNavLink(page, "采购申请")).click(); + const createResponsePromise = page.waitForResponse( + (response) => response.request().method() === "POST" && new URL(response.url()).pathname === "/api/purchase-requests", + ); await page.getByTestId("purchase-requests-create").click(); + const createResponse = await createResponsePromise; + const createPayload = (await createResponse.json()) as { + data: { purchaseRequest: { id: string } }; + }; + const purchaseRequestId = createPayload.data.purchaseRequest.id; + await page.waitForResponse( + (response) => response.request().method() === "GET" && new URL(response.url()).pathname === `/api/purchase-requests/${purchaseRequestId}`, + ); await expect(page.getByTestId("purchase-title")).toBeVisible(); await page.getByTestId("purchase-title").fill(title); @@ -72,6 +67,8 @@ test("E2E 采购主链路:员工提单→经理审批→财务归档", async ( }; const currentTaskId = submitPayload.data.currentTaskId; await expect(page.getByTestId("purchase-detail-status")).toHaveText("审批中", { timeout: 30_000 }); + requestNo = (await page.getByRole("heading", { level: 3 }).filter({ hasText: /^CG-/ }).first().textContent())?.trim() ?? ""; + expect(requestNo).toMatch(/^CG-/); await logout(page, EMPLOYEE.name); @@ -80,7 +77,8 @@ test("E2E 采购主链路:员工提单→经理审批→财务归档", async ( const managerTaskRow = page.getByTestId("workflow-task-row").filter({ hasText: currentTaskId }).first(); await expect(managerTaskRow).toBeVisible({ timeout: 30_000 }); await managerTaskRow.getByTestId("workflow-task-detail-link").click(); - await expect(page.getByTestId("workflow-task-summary")).toContainText(title); + await expect(page.getByTestId("workflow-task-summary")).toContainText("对象类型:采购申请"); + await expect(page.getByTestId("workflow-task-summary")).toContainText("申请人:Employee"); await page.getByTestId("workflow-task-approve").click(); await expect(page.getByTestId("workflow-task-summary")).toContainText("状态:已完成"); @@ -90,7 +88,7 @@ test("E2E 采购主链路:员工提单→经理审批→财务归档", async ( await (await openNavLink(page, "采购申请")).click(); await page.getByRole("button", { name: "全范围" }).click(); await page.getByRole("combobox").first().selectOption("approved"); - const purchaseRow = page.getByRole("row").filter({ hasText: title }).first(); + const purchaseRow = page.getByRole("row").filter({ hasText: requestNo }).first(); await expect(purchaseRow).toBeVisible({ timeout: 30_000 }); await purchaseRow.click(); diff --git a/issues/2026-03-23_19-57-35-r31-r30-closure-stabilization.csv b/issues/2026-03-23_19-57-35-r31-r30-closure-stabilization.csv new file mode 100644 index 00000000..8c9154db --- /dev/null +++ b/issues/2026-03-23_19-57-35-r31-r30-closure-stabilization.csv @@ -0,0 +1,7 @@ +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +R31-0,新执行合同建档与事实校正,基于当前WIP与R30未闭环项建立新一轮执行合同并记录spec testid WIP真相,新CSV校验通过 且 Notes 写明当前WIP归属与 R30-4 R30-5 覆盖映射,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-23_19-57-35-r31-r30-closure-stabilization.csv,manual,DONE,TODO,DONE,issues/2026-03-23_19-57-35-r31-r30-closure-stabilization.csv,[],"当前WIP: ReimbursementDetailPage.tsx 属于 R30-8;client/tests/e2e/helpers/* 与 settings-governance.spec.ts 属于 R30-2。R30-4 已补真实 weekly-todo-flow.spec.ts;R30-5 继续以 settings-governance.spec.ts 与 live-smoke.spec.ts 作为 merged coverage。assets/_template.csv 缺失时沿用 issues/README.md 与现有 CSV 表头;旧 R30 合同视为历史快照,不回写。2026-03-23 已完成新合同建档并校验通过。" +R31-1,fixture与detail test contract收口,把共享账号 testIds fixtures pageObjects 收成单一测试契约并迁移相关 spec,ADMIN 共享账号统一 detail page objects 可复用 overview testid 命名一致 且 settings-governance main-flow leave-flow 改为走共享契约,cd client && npm run typecheck && npm run test:e2e -- tests/e2e/settings-governance.spec.ts tests/e2e/main-flow.spec.ts tests/e2e/leave-flow.spec.ts,manual,DONE,TODO,DONE,client/tests/e2e/helpers/*|client/tests/e2e/settings-governance.spec.ts|client/tests/e2e/main-flow.spec.ts|client/tests/e2e/leave-flow.spec.ts|client/src/pages/ReimbursementDetailPage.tsx|client/src/pages/WorkflowTaskDetailPage.tsx,"[""R31-0""]","已落共享 TEST_IDS fixtures pageObjects,并把 settings-governance main-flow leave-flow 切到共享契约。2026-03-23 full-local 通过。" +R31-2,报销详情工作台化收口,把报销详情切到统一工作台骨架并补稳定 overview 契约,报销详情使用 PageScaffold 与 EntityOverviewHero 形成概览 执行 风险桥接结构 且存在 reimbursement-overview 同时原 submit pay item testid 保持兼容,cd client && npm run typecheck && npm run test:e2e -- tests/e2e/main-flow.spec.ts,manual,DONE,TODO,DONE,client/src/pages/ReimbursementDetailPage.tsx|client/src/components/patterns/*|client/tests/e2e/helpers/pageObjects/ReimbursementDetailPage.ts,"[""R31-1""]","已完成报销详情工作台骨架与 reimbursement-overview 契约,原 submit pay item testid 保持兼容。2026-03-23 full-local 通过。" +R31-3,审批详情工作台化收口,把审批详情切到统一工作台骨架并统一动作区与对象摘要契约,审批详情使用统一 overview 骨架并保留对象摘要 审批动作 处理历史三段结构 且存在 workflow-task-overview 同时转审选择器统一为 workflow-task-transfer-approver,cd client && npm run typecheck && npm run test:e2e -- tests/e2e/leave-flow.spec.ts tests/e2e/main-flow.spec.ts,manual,DONE,TODO,DONE,client/src/pages/WorkflowTaskDetailPage.tsx|client/src/components/patterns/*|client/tests/e2e/helpers/pageObjects/WorkflowTaskDetailPage.ts,"[""R31-1""]","已完成 workflow-task-overview 与 workflow-task-transfer-approver 契约收口,并补中文状态摘要兼容。2026-03-23 full-local 通过。" +R31-4,周报待办联动真实E2E补齐,补一个真实 weekly-todo-flow spec 并为周报待办页加最小稳定 testid,存在 weekly-todo-flow.spec.ts 且覆盖 周报保存 提交 从周报生成待办 待办列表可见 可完成 的闭环,cd client && npm run typecheck && npm run test:e2e -- tests/e2e/weekly-todo-flow.spec.ts,manual,DONE,TODO,DONE,client/tests/e2e/weekly-todo-flow.spec.ts|client/src/pages/WeeklyReportsPage.tsx|client/src/pages/TodosPage.tsx,"[""R31-1""]","已新增真实 weekly-todo-flow.spec.ts,并补齐 weekly-report-save submit create-todo 与 todo-row complete 等稳定 testid。2026-03-23 full-local 通过。" +R31-5,本地回归与三次绿灯放行,完成本地门禁与 trialrun 放行证据收集并更新新合同状态,build smoke-local full-local 通过 且 release gate 与 smoke-live 留下三次连续成功证据后才可宣告放行,cd client && npm run build && bash scripts/qa/run-web-gates.sh smoke-local && bash scripts/qa/run-web-gates.sh full-local && bash scripts/qa/run-release-gate.sh && bash scripts/qa/run-web-gates.sh smoke-live,manual,DOING,TODO,DOING,issues/2026-03-23_19-57-35-r31-r30-closure-stabilization.csv|scripts/qa/*|client/tests/e2e/*|deploy/trialrun/*,"[""R31-2"",""R31-3"",""R31-4""]","2026-03-23 已完成 client typecheck 与 full-local 绿灯,Playwright 结果为 22 passed / 3 skipped,skipped 为 live-smoke 预期跳过。23:25 后新增 toast 可交互、报销提交 confirm guard、Playwright workers=4,并补上 procurement-flow 创建后等待详情 hydration 的稳定化;随后再次 full-local 绿灯,结果仍为 22 passed / 3 skipped。2026-03-23 夜间继续执行 ci-release 时,assets-flow 暴露资产建卡事务超时:server/src/routes/assets.js 中资产创建事务内调用 handleAssetLedgerSummaryErpSync,导致 Prisma P2028 interactive transaction timeout,并在前端表现为 SYS_001。2026-03-24 已把资产 ERP 摘要同步移出主事务,assets-flow 已转绿;但 full-local 随后暴露 main-flow.spec.ts 120s timeout,失败产物显示测试结束时页面回落到登录页,根因仍待继续定位。因此 release-gate deploy 与 smoke-live 证据尚未收齐,放行项继续保持 DOING。" diff --git a/server/src/routes/assets.js b/server/src/routes/assets.js index b6d833f5..6444c519 100644 --- a/server/src/routes/assets.js +++ b/server/src/routes/assets.js @@ -651,6 +651,43 @@ async function handleAssetLedgerSummaryErpSync({ }; } +async function runAssetLedgerSummaryErpSyncSafely({ + prisma, + config, + request, + actorId, + asset, + sourceEventName, + changeType, + changedFields, +}) { + try { + return await handleAssetLedgerSummaryErpSync({ + tx: prisma, + config, + request, + actorId, + asset, + sourceEventName, + changeType, + changedFields, + }); + } catch (error) { + request.log.error( + { + err: error, + assetId: asset?.id ?? null, + assetNo: asset?.asset_no ?? null, + sourceEventName, + changeType, + changedFields: Array.isArray(changedFields) ? changedFields : [], + }, + "Asset ledger summary ERP sync failed after asset commit", + ); + return null; + } +} + async function assetsRoutes(fastify) { fastify.get( "/asset-categories", @@ -975,20 +1012,20 @@ async function assetsRoutes(fastify) { }), }); - await handleAssetLedgerSummaryErpSync({ - tx, - config: fastify.config, - request, - actorId: request.employee.id, - asset: created, - sourceEventName: 'asset.card.created', - changeType: 'created', - changedFields: [], - }); - return created; }); + await runAssetLedgerSummaryErpSyncSafely({ + prisma: fastify.prisma, + config: fastify.config, + request, + actorId: request.employee.id, + asset, + sourceEventName: 'asset.card.created', + changeType: 'created', + changedFields: [], + }); + return sendData(reply, request, { asset: mapAsset(asset, request, fastify) }); }, ); @@ -1024,7 +1061,7 @@ async function assetsRoutes(fastify) { const assetId = parseUuid("id", request.params?.id); const payload = normalizeAssetPayload(parsePatchBody(ASSET_PATCH_SCHEMA, request.body)); - const asset = await fastify.prisma.$transaction(async (tx) => { + const { asset, assetLedgerSummaryChange } = await fastify.prisma.$transaction(async (tx) => { const current = await tx.assetCard.findUnique({ where: { id: assetId }, select: { @@ -1097,22 +1134,25 @@ async function assetsRoutes(fastify) { }); const assetLedgerSummaryChange = resolveAssetLedgerSummaryChange(current, updated); - if (assetLedgerSummaryChange) { - await handleAssetLedgerSummaryErpSync({ - tx, - config: fastify.config, - request, - actorId: request.employee.id, - asset: updated, - sourceEventName: assetLedgerSummaryChange.sourceEventName, - changeType: assetLedgerSummaryChange.changeType, - changedFields: assetLedgerSummaryChange.changedFields, - }); - } - - return updated; + return { + asset: updated, + assetLedgerSummaryChange, + }; }); + if (assetLedgerSummaryChange) { + await runAssetLedgerSummaryErpSyncSafely({ + prisma: fastify.prisma, + config: fastify.config, + request, + actorId: request.employee.id, + asset, + sourceEventName: assetLedgerSummaryChange.sourceEventName, + changeType: assetLedgerSummaryChange.changeType, + changedFields: assetLedgerSummaryChange.changedFields, + }); + } + return sendData(reply, request, { asset: mapAsset(asset, request, fastify) }); }, );