diff --git a/.trellis/spec/hyb-oa/backend-fastify-prisma.md b/.trellis/spec/hyb-oa/backend-fastify-prisma.md index 7f61236c..e0e5c2ae 100644 --- a/.trellis/spec/hyb-oa/backend-fastify-prisma.md +++ b/.trellis/spec/hyb-oa/backend-fastify-prisma.md @@ -23,6 +23,83 @@ QA script patterns over new abstractions. - `cd server && ./scripts/regression-workflow-actions.sh` - `cd server && ./scripts/regression-all.sh` +## Scenario: Local Regression Migration Drift + +### 1. Scope / Trigger + +- Trigger: a local or attached E2E fails with Prisma `missing column`, + `column does not exist`, enum missing, or route 500 immediately after a + schema-backed feature was added. +- Applies to `server/prisma/schema.prisma`, `server/prisma/migrations/`, + local regression databases such as `hyb_oa_regression`, and Playwright tests + that use real API/database flows. + +### 2. Signatures + +- Read-only drift check: `cd server && npx prisma migrate status`. +- Apply committed migrations after explicit confirmation: + `cd server && npx prisma migrate deploy`. +- Local service smoke for attached E2E: + `HOST=127.0.0.1 PORT=3000 JOBS_ENABLED=false SERVE_CLIENT_DIST=true npm run dev`. +- Server entrypoints that call `getConfig()` before `buildApp()` must load + `dotenv/config` before reading config. + +### 3. Contracts + +- `migrate status` is safe and read-only; it may be run before asking the user. +- `migrate deploy` writes database schema state and requires explicit user + confirmation unless the current task already approved that exact local DB. +- Do not use `prisma migrate reset` for E2E recovery without separate backup + and destructive-action approval. +- Attached local E2E should disable background jobs with `JOBS_ENABLED=false` + unless the test explicitly validates job behavior. + +### 4. Validation & Error Matrix + +- `migrate status` reports pending migrations -> record the migration names, + inspect whether they are additive/destructive, then ask before applying. +- Missing `JWT_SECRET` during `npm run dev` while `.env` exists -> verify the + entrypoint loads `dotenv/config` before `getConfig()`. +- Missing column after `migrate deploy` -> inspect actual columns and migration + history; do not patch tests to hide schema drift. +- E2E selector failure after schema is fixed -> treat as page/test contract + drift and update the focused E2E assertion, not the database. + +### 5. Good/Base/Bad Cases + +- Good: run `migrate status`, apply only committed migrations after approval, + seed dev users, then rerun the focused Playwright flow. +- Base: if migrations are already current, skip DB writes and debug the test + failure directly. +- Bad: use `migrate reset` or manually add columns to unblock a test. +- Bad: start the server with an ad hoc `node -r dotenv/config` workaround and + leave `npm run dev` broken. + +### 6. Tests Required + +- `cd server && npm run prisma:validate`. +- `bash scripts/qa/check-local-migration-drift.sh` when local DB drift was the + cause. +- `cd server && node --check src/index.js` if server bootstrap changed. +- Focused Playwright flow that previously failed. +- Frontend `npm run build` when Playwright or client behavior changed. + +### 7. Wrong vs Correct + +#### Wrong + +```bash +cd server && npx prisma migrate reset +``` + +#### Correct + +```bash +cd server && npx prisma migrate status +# after explicit approval for the local regression database only +cd server && npx prisma migrate deploy +``` + ## Scenario: Portal Announcement Acknowledgement ### 1. Scope / Trigger diff --git a/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/check.jsonl b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/check.jsonl new file mode 100644 index 00000000..4b6ad472 --- /dev/null +++ b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/check.jsonl @@ -0,0 +1,3 @@ +{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Trellis 治理、提交和验证门禁"} +{"file": ".trellis/spec/hyb-oa/backend-fastify-prisma.md", "reason": "Prisma migration status、schema 校验和数据库漂移检查"} +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Playwright E2E、资产列表/详情断言和前端构建检查"} diff --git a/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/implement.jsonl b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/implement.jsonl new file mode 100644 index 00000000..047381c3 --- /dev/null +++ b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/implement.jsonl @@ -0,0 +1,3 @@ +{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Trellis 任务、审批确认、提交和数据库漂移处理边界"} +{"file": ".trellis/spec/hyb-oa/backend-fastify-prisma.md", "reason": "Prisma 迁移、回归库一致性和后端验证规则"} +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Playwright E2E 与资产页面列表/详情交互规则"} diff --git a/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/implement.md b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/implement.md new file mode 100644 index 00000000..159edb4d --- /dev/null +++ b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/implement.md @@ -0,0 +1,60 @@ +# 实施说明 + +## 只读排查结论 + +- `cd server && npx prisma migrate status` 显示本地回归库 `hyb_oa_regression` 有 12 个迁移未应用: + - `20260513214500_biz8_purchase_project_link` + - `20260517090437_cashier_payment_receipts` + - `20260517145928_attendance_unified_wecom_sync` + - `20260517200040_hr_wecom_directory_v1` + - `20260518184319_wecom_hr_master_sync_v1` + - `20260519223206_wecom_approval_archive_minimum_v1` + - `20260520124804_wecom_checkin_summary_archive_v1` + - `20260522191833_asset_governance_v1` + - `20260524193000_owner_self_fallback_role` + - `20260526211500_email_code_login` + - `20260528113000_policy_acknowledgements` + - `20260528162000_business_attachment_governance_v1` +- 实际列检查显示当前库只命中 `employees.employee_no`,缺少 `asset_cards.asset_type`、`asset_cards.sensitivity_level`、`purchase_requests.project_id`、`approval_rules.owner_self_fallback_role` 等当前代码依赖字段。 +- 缺列来源与未应用迁移一致: + - `purchase_requests.project_id` 来自 `20260513214500_biz8_purchase_project_link` + - `asset_cards.asset_type` 来自 `20260522191833_asset_governance_v1` + - `approval_rules.owner_self_fallback_role` 来自 `20260524193000_owner_self_fallback_role` + +## 风险判断 + +- 这不是前端资产页面问题,也不是 `assets-flow.spec.ts` 的测试假设问题;根因是本地回归库落后于 Prisma schema 和应用代码。 +- 未应用迁移粗查未发现 `DROP`、`TRUNCATE` 或数据库 reset;主要是新增列、表、索引、外键,以及给审批规则兜底角色补默认值。 +- 仍属于数据库结构变更,按项目规则需要明确确认后再执行。 + +## 建议处理 + +- 在本地回归库执行 `cd server && npx prisma migrate deploy`,只应用已提交迁移,不做 reset。 +- 应用后运行: + - `cd server && npm run prisma:validate` + - `cd server && npm run seed:dev-users`(如命令存在且当前测试依赖本地账号) + - 聚焦 `assets-flow.spec.ts` + - 必要前端回归:`cd client && npm run typecheck && npm run build` + - `python3 ./.trellis/scripts/task.py validate 05-31-fix-assets-e2e-schema-drift` + - `node scripts/qa/check-trellis-governance.mjs` + - `git diff --check` + +## 执行结果 + +- 已获得用户确认后执行 `cd server && npx prisma migrate deploy`,12 个待应用迁移全部成功应用到本地回归库。 +- `cd server && npx prisma migrate status` 复查显示数据库结构已是最新。 +- 关键字段复查已命中: + - `approval_rules.owner_self_fallback_role` + - `asset_cards.asset_type` + - `asset_cards.sensitivity_level` + - `purchase_requests.project_id` + - `employees.employee_no` +- `cd server && npm run seed:dev-users` 已完成,开发账号与基础资料可用。 +- `cd client && E2E_BASE_URL=http://127.0.0.1:3000 npx playwright test tests/e2e/assets-flow.spec.ts --workers=1 --reporter=list` 已通过。 +- E2E 修正点: + - 创建资产后页面已经进入详情,不再点击列表页旧的 `全范围` 按钮。 + - 资产详情同时展示流转摘要和资产摘要,归属人/位置断言改为匹配首个可见命中,避免 Playwright strict mode 误判。 +- 本地服务启动修正: + - `server/src/index.js` 在读取配置前显式加载 `dotenv/config`,避免 `npm run dev` 在本地 `.env` 存在时仍因缺少 `JWT_SECRET` 失败。 +- 已用正常 `npm run dev` 启动本地服务,并通过 `/ready` 与资产真实 E2E 复核,不再依赖 `node -r dotenv/config` 启动绕过。 +- 已更新 `.trellis/spec/hyb-oa/backend-fastify-prisma.md`,沉淀本地回归库迁移漂移和 server dotenv 启动顺序规则。 diff --git a/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/prd.md b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/prd.md new file mode 100644 index 00000000..86805118 --- /dev/null +++ b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/prd.md @@ -0,0 +1,77 @@ +# 修复资产真实 E2E 数据库结构漂移 + +## Goal + +查清并修复真实资产 E2E 被本地数据库结构漂移阻塞的问题,让资产流程可以在本地真实 API/数据库环境完成回归验证。修复必须遵守当前业务边界:hyb-oa 没有 ERP,资产管理按 OA 台账处理,不引入新的业务功能。 + +## What I Already Know + +- 上轮资产一致性改造已提交:`f299cee 修正 ERP 边界与资产页面一致性(05-30-fix-erp-boundary-asset-consistency)`。 +- Mock 浏览器检查已验证资产页面无 ERP 文案、右侧操作区、中文姓名和员工号展示。 +- 真实 `assets-flow.spec.ts` 受本地数据库结构漂移阻塞,已记录缺列包括: + - `asset_cards.asset_type` + - `purchase_requests.project_id` +- 不能擅自执行数据库重置、生产迁移或真实数据写入。 +- 用户已确认系统没有 ERP,以后也不会有 ERP。 + +## Assumptions + +- 本轮优先处理本地/测试环境验证阻塞,不扩大到公网部署。 +- 如果需要修改数据库结构,先通过只读排查确认迁移状态,再使用项目已有迁移机制;不使用破坏性 reset。 +- 如果问题只是本地库落后于迁移,优先给出清晰、安全的本地补齐路径。 + +## Scope + +- 本地回归数据库 `hyb_oa_regression` 与 Prisma 迁移目录的一致性排查和修复。 +- 资产真实 E2E 主链路 `assets-flow.spec.ts` 的执行恢复。 +- 如资产页面改造导致 E2E 断言落后,仅更新测试断言以匹配当前已确认页面规则。 +- 如本地服务启动流程阻塞 E2E,修复不改变业务行为的启动加载问题。 +- 记录本轮数据库结构漂移原因、修复方式和验证结果。 + +## Requirements + +- 排查 Prisma schema、迁移目录、本地数据库实际结构与 E2E 测试准备逻辑之间的差异。 +- 找出 `asset_cards.asset_type`、`purchase_requests.project_id` 等缺列的来源:迁移未应用、迁移缺失、测试数据准备假设错误,或代码/数据库版本不一致。 +- 修复应尽量使用已有迁移、seed、测试准备脚本和项目约定。 +- 不引入 ERP 文案、ERP 字段、ERP 同步入口或未来 ERP 接入口径。 +- 不改变资产页面当前已确定的信息架构和交互规则,除非真实 E2E 揭示必要缺陷。 + +## Test Plan + +- `cd server && npx prisma migrate status` +- `cd server && npx prisma migrate deploy`(仅在用户确认后对本地回归库执行) +- `cd server && npm run seed:dev-users` +- `cd server && npm run prisma:validate` +- `cd server && node --check src/index.js` +- `bash scripts/qa/check-local-migration-drift.sh` +- `cd client && npm run build` +- `cd client && E2E_BASE_URL=http://127.0.0.1:3000 npx playwright test tests/e2e/assets-flow.spec.ts --workers=1 --reporter=list` +- `python3 ./.trellis/scripts/task.py validate 05-31-fix-assets-e2e-schema-drift` +- `node scripts/qa/check-trellis-governance.mjs` +- `git diff --check` + +## Acceptance Criteria + +- [x] 明确记录数据库结构漂移的根因和处理方式。 +- [x] `assets-flow.spec.ts` 能进入并完成资产流程验证;如仍被外部环境阻塞,需记录新的具体阻塞点。 +- [x] 相关本地验证通过:前端必要检查、后端 Prisma 校验、Trellis 校验、`git diff --check`。 +- [x] 如产生稳定经验,更新对应 spec 或任务实施说明。 +- [ ] 所有变更以中文 commit 提交,commit message 包含本任务名。 + +## Out of Scope + +- 不做公网部署。 +- 不改生产数据库。 +- 不做数据库破坏性重置。 +- 不重新设计资产页面、报销页面、付款页面等 UI。 +- 不新增 ERP、考勤、请假等业务能力。 + +## Technical Notes + +- 相关文件预计包括: + - `server/prisma/schema.prisma` + - `server/prisma/migrations/` + - `client/tests/e2e/assets-flow.spec.ts` + - `server/src/scripts/seed-dev.js` + - `.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/implement.md` +- 先只读运行迁移状态和 schema 检查;需要写库的命令单独确认风险后执行。 diff --git a/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/task.json b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/task.json new file mode 100644 index 00000000..0d077ff5 --- /dev/null +++ b/.trellis/tasks/05-31-fix-assets-e2e-schema-drift/task.json @@ -0,0 +1,33 @@ +{ + "id": "fix-assets-e2e-schema-drift", + "name": "fix-assets-e2e-schema-drift", + "title": "修复资产真实 E2E 数据库结构漂移", + "description": "修复本地回归库迁移滞后导致资产真实 E2E 无法运行的问题,并对齐资产页面改造后的 Playwright 断言。", + "status": "in_progress", + "dev_type": null, + "scope": "local-regression-db-and-assets-e2e", + "package": null, + "priority": "P2", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-05-31", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [ + "server/prisma/migrations/", + "server/prisma/schema.prisma", + "server/src/index.js", + "client/tests/e2e/assets-flow.spec.ts", + ".trellis/tasks/05-31-fix-assets-e2e-schema-drift/prd.md", + ".trellis/tasks/05-31-fix-assets-e2e-schema-drift/implement.md" + ], + "notes": "", + "meta": {} +} diff --git a/client/tests/e2e/assets-flow.spec.ts b/client/tests/e2e/assets-flow.spec.ts index 0cfefb99..82939888 100644 --- a/client/tests/e2e/assets-flow.spec.ts +++ b/client/tests/e2e/assets-flow.spec.ts @@ -115,7 +115,6 @@ test("E2E 资产流转主链路:建卡→领用归还→调拨→盘点", asyn data: { asset: { id: string } }; }; const assetId = createAssetPayload.data.asset.id; - await page.getByRole("button", { name: "全范围" }).click(); await page.goto(`/assets?id=${assetId}`); await expect(page.getByTestId("asset-name")).toHaveValue(assetName, { timeout: 30_000 }); @@ -132,25 +131,25 @@ test("E2E 资产流转主链路:建卡→领用归还→调拨→盘点", asyn await selectOptionByText(page.getByTestId("asset-movement-assignee"), /陈工程/); await page.getByTestId("asset-movement-note").fill("Playwright 首次领用"); - if (await page.getByText(/归属人:-/).isVisible().catch(() => false)) { + if (await page.getByText(/归属人:-/).first().isVisible().catch(() => false)) { await page.getByTestId("asset-assign-submit").click(); } else { await page.getByTestId("asset-transfer-submit").click(); } - await expect(page.getByText(/归属人:陈工程/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/归属人:陈工程/).first()).toBeVisible({ timeout: 30_000 }); await page.getByTestId("asset-return-submit").click(); - await expect(page.getByText(/归属人:-/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/归属人:-/).first()).toBeVisible({ timeout: 30_000 }); await selectOptionByText(page.getByTestId("asset-movement-assignee"), /陈工程/); await page.getByTestId("asset-assign-submit").click(); - await expect(page.getByText(/归属人:陈工程/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/归属人:陈工程/).first()).toBeVisible({ timeout: 30_000 }); await selectOptionByText(page.getByTestId("asset-movement-assignee"), /钱主管/); await page.getByTestId("asset-movement-location").fill("深圳办公室 B-02"); await page.getByTestId("asset-transfer-submit").click(); - await expect(page.getByText(/归属人:钱主管/)).toBeVisible({ timeout: 30_000 }); - await expect(page.getByText(/位置:深圳办公室 B-02/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/归属人:钱主管/).first()).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/位置:深圳办公室 B-02/).first()).toBeVisible({ timeout: 30_000 }); await logout(page, ADMINISTRATION.name); diff --git a/server/src/index.js b/server/src/index.js index 460b3fc7..9d9ad192 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -1,3 +1,5 @@ +require("dotenv/config"); + const { getConfig } = require("./config"); const { initTracing } = require("./observability/tracing");