修复资产 E2E 数据库漂移(05-31-fix-assets-e2e-schema-drift)

这个提交包含在:
2026-05-31 09:47:52 +08:00
父节点 f299ceeb74
当前提交 dafd2eb275
共修改 8 个文件,包含 261 行新增和 7 行删除
@@ -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
@@ -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、资产列表/详情断言和前端构建检查"}
@@ -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 与资产页面列表/详情交互规则"}
@@ -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 启动顺序规则。
@@ -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 检查;需要写库的命令单独确认风险后执行。
@@ -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": {}
}
+6 -7
查看文件
@@ -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);
+2
查看文件
@@ -1,3 +1,5 @@
require("dotenv/config");
const { getConfig } = require("./config");
const { initTracing } = require("./observability/tracing");