docs/test(P18A-5,P18A-6): 收口资产矩阵文档与回归

这个提交包含在:
2026-03-07 22:33:17 +08:00
父节点 447ffa66d2
当前提交 865acb63aa
共修改 7 个文件,包含 348 行新增和 21 行删除
+11 -3
查看文件
@@ -1,8 +1,8 @@
# hyb-oa 实现状态矩阵(P5-P17B) # hyb-oa 实现状态矩阵(P5-P18A)
**快照日期**:2026-03-07 **快照日期**:2026-03-07
**范围**:`F1-F42`(含 `F19b/F19c/F19d`)+ `R1-S2` + `P9A1-P9D1` + `P10E1-P10G1` + `G0-G6` + `H1` + `M1-M4` + `P13-0-P13-8` + `P14-0-P14-8` + `P15A-0-P15A-5` + `P15B-0-P15B-6` + `P16A-0-P16A-6` + `P16B-0-P16B-6` + `P16C-0-P16C-6` + `R9-0-R9-3` + `G2-0-G2-6` + `P17A-0-P17A-6`,共 164 项。 **范围**:`F1-F42`(含 `F19b/F19c/F19d`)+ `R1-S2` + `P9A1-P9D1` + `P10E1-P10G1` + `G0-G6` + `H1` + `M1-M4` + `P13-0-P13-8` + `P14-0-P14-8` + `P15A-0-P15A-5` + `P15B-0-P15B-6` + `P16A-0-P16A-6` + `P16B-0-P16B-6` + `P16C-0-P16C-6` + `R9-0-R9-3` + `G2-0-G2-6` + `P17A-0-P17A-6` + `P17B-0-P17B-6` + `P18A-0-P18A-6`,共 178 项。
**事实源**:`issues/2026-02-09_13-39-02-p5-blueprint-v3.csv` + `issues/2026-02-09_23-30-00-p6-p8-remaining-work-master.csv` + `issues/2026-02-10_09-42-31-p9-maintenance-performance-matrix.csv` + `issues/2026-02-10_10-35-30-p10-production-hardening.csv` + `issues/2026-02-11-p11-p12-governance-mobile.csv` + `issues/2026-02-12_01-09-41-p13-l5-governance.csv` + `issues/2026-02-13_00-11-56-p14-l5-operationalization.csv` + `issues/2026-03-06_14-30-00-p15a-l5-permission-decouple.csv` + `issues/2026-03-06_14-30-00-p15b-l5-leave-v1.csv` + `issues/2026-03-07_13-47-05-p16a-l5-master-data-core.csv` + `issues/2026-03-07_16-22-07-p16b-l5-holiday-calendar.csv` + `issues/2026-03-07_17-14-39-p16c-l5-suppliers.csv` + `issues/2026-03-07_18-15-03-r9-g2-p19-roadmap-refresh.csv` + `issues/2026-03-07_18-16-00-g2-l5-cross-domain-bridge.csv` + `issues/2026-03-07_19-33-09-p17a-l5-procurement-core.csv` + `issues/2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv`(执行合同) **事实源**:`issues/2026-02-09_13-39-02-p5-blueprint-v3.csv` + `issues/2026-02-09_23-30-00-p6-p8-remaining-work-master.csv` + `issues/2026-02-10_09-42-31-p9-maintenance-performance-matrix.csv` + `issues/2026-02-10_10-35-30-p10-production-hardening.csv` + `issues/2026-02-11-p11-p12-governance-mobile.csv` + `issues/2026-02-12_01-09-41-p13-l5-governance.csv` + `issues/2026-02-13_00-11-56-p14-l5-operationalization.csv` + `issues/2026-03-06_14-30-00-p15a-l5-permission-decouple.csv` + `issues/2026-03-06_14-30-00-p15b-l5-leave-v1.csv` + `issues/2026-03-07_13-47-05-p16a-l5-master-data-core.csv` + `issues/2026-03-07_16-22-07-p16b-l5-holiday-calendar.csv` + `issues/2026-03-07_17-14-39-p16c-l5-suppliers.csv` + `issues/2026-03-07_18-15-03-r9-g2-p19-roadmap-refresh.csv` + `issues/2026-03-07_18-16-00-g2-l5-cross-domain-bridge.csv` + `issues/2026-03-07_19-33-09-p17a-l5-procurement-core.csv` + `issues/2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv` + `issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv`(执行合同)
**说明**: **说明**:
- 本矩阵用于全局读图(审计/交接/回归覆盖追踪),不替代 `issues/*.csv` 的执行合同职责。 - 本矩阵用于全局读图(审计/交接/回归覆盖追踪),不替代 `issues/*.csv` 的执行合同职责。
@@ -197,3 +197,11 @@
| P17B-4 | 前端归档入口与采购E2E | N/A | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07 | DONE | client:typecheck/build + playwright | `PurchaseRequestsPage` 新增归档区块,采购主链路 E2E 覆盖员工提单、经理审批、财务归档 | | P17B-4 | 前端归档入口与采购E2E | N/A | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07 | DONE | client:typecheck/build + playwright | `PurchaseRequestsPage` 新增归档区块,采购主链路 E2E 覆盖员工提单、经理审批、财务归档 |
| P17B-5 | 安全文档与接口说明 | N/A | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07 | DONE | node --check + validate-feature-matrix | 采购预算闭环、归档权限边界与矩阵事实源同步完成,主数据读取越权缺口已补齐 | | P17B-5 | 安全文档与接口说明 | N/A | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07 | DONE | node --check + validate-feature-matrix | 采购预算闭环、归档权限边界与矩阵事实源同步完成,主数据读取越权缺口已补齐 |
| P17B-6 | P17B回归与安全收口 | N/A | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07 | DONE | regression-all + client:typecheck/build + playwright | 采购预算闭环、PO归档与全量回归通过,报销/请假/主数据/采购主流程零回归 | | P17B-6 | P17B回归与安全收口 | N/A | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07 | DONE | regression-all + client:typecheck/build + playwright | 采购预算闭环、PO归档与全量回归通过,报销/请假/主数据/采购主流程零回归 |
| P18A-0 | P18A执行合同建档 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | CSV 校验 | 固资台账核心执行合同建档并冻结仅做资产分类与资产卡片 |
| P18A-1 | 资产Prisma模型与迁移 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | prisma:validate + prisma:migrate + prisma:generate | `AssetCategory`、`AssetCard` 与法体/成本中心/采购来源关系落地,且不破坏既有主流程 |
| P18A-2 | 资产权限与系统种子 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | server:test + seed:config | `asset.*` 权限映射与 `ASSET_CARD_DEFAULT` 系统单据分类补齐完成 |
| P18A-3 | 资产分类与卡片后端API | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | server:test + regression-assets | `/api/asset-categories` 与 `/api/assets` 列表详情创建更新接口可用,采购来源引用校验正确 |
| P18A-4 | 资产台账前端入口与页面 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | client:typecheck/build | `AssetRegistryPage`、菜单、路由与资产分类/卡片编辑界面可用 |
| P18A-5 | OpenAPI与矩阵同步 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | node --check + validate-feature-matrix | 资产接口文档标签、合同锚点与矩阵事实源同步完成 |
| P18A-6 | P18A回归与验收收口 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | regression-all + client:typecheck/build | 资产分类、资产卡片与既有报销/请假/采购/主数据回归全部通过 |
@@ -1,8 +1,8 @@
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
P18A-0,P18A执行合同建档,创建并校验P18A固资台账核心执行合同CSV,CSV通过校验且字段完整,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,manual,TODO,TODO,TODO,issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,[],P18A仅交付资产分类与资产卡片不进入折旧凭证ERP联动 P18A-0,P18A执行合同建档,创建并校验P18A固资台账核心执行合同CSV,CSV通过校验且字段完整,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,manual,DONE,DONE,DONE,issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,[],P18A仅交付资产分类与资产卡片不进入折旧凭证ERP联动
P18A-1,资产Prisma模型与迁移,新增AssetCategory与AssetCard并补齐法体成本中心采购来源等关系,Prisma schema与迁移通过校验且不破坏报销请假采购主流程,cd server && npm run prisma:validate && npx prisma migrate dev --name p18a_asset_registry_core && npm run prisma:generate,manual,TODO,TODO,TODO,server/prisma/schema.prisma | server/prisma/migrations | server/src/prismaClient.js,"[""P18A-0""]",asset_no创建时生成 purchase_request_id仅允许引用approved采购单 P18A-1,资产Prisma模型与迁移,新增AssetCategory与AssetCard并补齐法体成本中心采购来源等关系,Prisma schema与迁移通过校验且不破坏报销请假采购主流程,cd server && npm run prisma:validate && npx prisma migrate dev --name p18a_asset_registry_core && npm run prisma:generate,manual,DONE,DONE,DONE,server/prisma/schema.prisma | server/prisma/migrations | server/src/prismaClient.js,"[""P18A-0""]",asset_no创建时生成 purchase_request_id仅允许引用approved采购单
P18A-2,资产权限与系统种子,新增asset权限域角色映射并补齐系统单据分类默认配置ASSET_CARD_DEFAULT,默认角色权限与文档分类种子一致且不误开员工管理权限,cd server && npm run test,manual,TODO,TODO,TODO,server/src/constants/permissions.js | server/src/services/documentSequence.js | server/src/scripts/seed-config.js,"[""P18A-1""]",资产域不接workflow employee仅保留view_own P18A-2,资产权限与系统种子,新增asset权限域角色映射并补齐系统单据分类默认配置ASSET_CARD_DEFAULT,默认角色权限与文档分类种子一致且不误开员工管理权限,cd server && npm run test,manual,DONE,DONE,DONE,server/src/constants/permissions.js | server/src/services/documentSequence.js | server/src/scripts/seed-config.js,"[""P18A-1""]",资产域不接workflow employee仅保留view_own
P18A-3,资产分类与卡片后端API,实现资产分类CRUD与资产卡片列表详情创建更新接口并补齐编号生成校验,资产分类和资产卡片接口可用且purchase_request引用校验正确,cd server && npm run test,manual,TODO,TODO,TODO,server/src/routes/assets.js | server/src/app.js | server/tests,"[""P18A-2""]",PATCH不允许修改asset_no与purchase_request_id P18A-3,资产分类与卡片后端API,实现资产分类CRUD与资产卡片列表详情创建更新接口并补齐编号生成校验,资产分类和资产卡片接口可用且purchase_request引用校验正确,cd server && npm run test,manual,DONE,DONE,DONE,server/src/routes/assets.js | server/src/app.js | server/tests,"[""P18A-2""]",PATCH不允许修改asset_no与purchase_request_id
P18A-4,资产台账前端入口与页面,新增AssetRegistryPage API封装 菜单与路由并支持分类管理资产列表创建编辑,资产台账页面可用且菜单显隐与asset权限一致,cd client && npm run typecheck && npm run build,manual,TODO,TODO,TODO,client/src/pages/AssetRegistryPage.tsx | client/src/api/assets.ts | client/src/router/index.tsx | client/src/components/navigation/NavLinks.tsx,"[""P18A-3""]",页面包含分类管理区资产列表区创建编辑抽屉 P18A-4,资产台账前端入口与页面,新增AssetRegistryPage API封装 菜单与路由并支持分类管理资产列表创建编辑,资产台账页面可用且菜单显隐与asset权限一致,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/pages/AssetRegistryPage.tsx | client/src/api/assets.ts | client/src/router/index.tsx | client/src/components/navigation/NavLinks.tsx,"[""P18A-3""]",页面包含分类管理区资产列表区创建编辑抽屉
P18A-5,OpenAPI与矩阵同步,补齐资产接口文档 路由标签 合同锚点并同步Feature Matrix事实源,/api/docs/json可见资产接口且矩阵事实源与合同锚点一致,cd server && npm run test && python3 scripts/validate-feature-matrix.py,manual,TODO,TODO,TODO,server/src/routes/assets.js | docs/feature-matrix.md | issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,"[""P18A-4""]",只冻结资产台账边界不提前进入流转盘点 P18A-5,OpenAPI与矩阵同步,补齐资产接口文档 路由标签 合同锚点并同步Feature Matrix事实源,/api/docs/json可见资产接口且矩阵事实源与合同锚点一致,cd server && npm run test && python3 scripts/validate-feature-matrix.py,manual,DONE,DONE,DONE,server/src/routes/assets.js | docs/feature-matrix.md | issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,"[""P18A-4""]",只冻结资产台账边界不提前进入流转盘点
P18A-6,P18A回归与验收收口,执行后端前端回归并确认报销请假主数据采购零回归后再允许进入P18B,资产分类资产卡片可维护且既有报销请假采购主流程零回归,cd server && npm run test && OCR_PROVIDER=mock JOBS_ENABLED=false ./scripts/regression-all.sh && cd ../client && npm run typecheck && npm run build,manual,TODO,TODO,TODO,issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | server/src/routes/assets.js | client/src/pages/AssetRegistryPage.tsx,"[""P18A-5""]",P18A不做资产审批流不做折旧凭证ERP过账 P18A-6,P18A回归与验收收口,执行后端前端回归并确认报销请假主数据采购零回归后再允许进入P18B,资产分类资产卡片可维护且既有报销请假采购主流程零回归,cd server && npm run test && OCR_PROVIDER=mock JOBS_ENABLED=false ./scripts/regression-all.sh && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | server/src/routes/assets.js | client/src/pages/AssetRegistryPage.tsx,"[""P18A-5""]",P18A不做资产审批流不做折旧凭证ERP过账
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 P18A-0 P18A执行合同建档 创建并校验P18A固资台账核心执行合同CSV CSV通过校验且字段完整 python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv manual TODO DONE TODO DONE TODO DONE issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv [] P18A仅交付资产分类与资产卡片不进入折旧凭证ERP联动
3 P18A-1 资产Prisma模型与迁移 新增AssetCategory与AssetCard并补齐法体成本中心采购来源等关系 Prisma schema与迁移通过校验且不破坏报销请假采购主流程 cd server && npm run prisma:validate && npx prisma migrate dev --name p18a_asset_registry_core && npm run prisma:generate manual TODO DONE TODO DONE TODO DONE server/prisma/schema.prisma | server/prisma/migrations | server/src/prismaClient.js ["P18A-0"] asset_no创建时生成 purchase_request_id仅允许引用approved采购单
4 P18A-2 资产权限与系统种子 新增asset权限域角色映射并补齐系统单据分类默认配置ASSET_CARD_DEFAULT 默认角色权限与文档分类种子一致且不误开员工管理权限 cd server && npm run test manual TODO DONE TODO DONE TODO DONE server/src/constants/permissions.js | server/src/services/documentSequence.js | server/src/scripts/seed-config.js ["P18A-1"] 资产域不接workflow employee仅保留view_own
5 P18A-3 资产分类与卡片后端API 实现资产分类CRUD与资产卡片列表详情创建更新接口并补齐编号生成校验 资产分类和资产卡片接口可用且purchase_request引用校验正确 cd server && npm run test manual TODO DONE TODO DONE TODO DONE server/src/routes/assets.js | server/src/app.js | server/tests ["P18A-2"] PATCH不允许修改asset_no与purchase_request_id
6 P18A-4 资产台账前端入口与页面 新增AssetRegistryPage API封装 菜单与路由并支持分类管理资产列表创建编辑 资产台账页面可用且菜单显隐与asset权限一致 cd client && npm run typecheck && npm run build manual TODO DONE TODO DONE TODO DONE client/src/pages/AssetRegistryPage.tsx | client/src/api/assets.ts | client/src/router/index.tsx | client/src/components/navigation/NavLinks.tsx ["P18A-3"] 页面包含分类管理区资产列表区创建编辑抽屉
7 P18A-5 OpenAPI与矩阵同步 补齐资产接口文档 路由标签 合同锚点并同步Feature Matrix事实源 /api/docs/json可见资产接口且矩阵事实源与合同锚点一致 cd server && npm run test && python3 scripts/validate-feature-matrix.py manual TODO DONE TODO DONE TODO DONE server/src/routes/assets.js | docs/feature-matrix.md | issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv ["P18A-4"] 只冻结资产台账边界不提前进入流转盘点
8 P18A-6 P18A回归与验收收口 执行后端前端回归并确认报销请假主数据采购零回归后再允许进入P18B 资产分类资产卡片可维护且既有报销请假采购主流程零回归 cd server && npm run test && OCR_PROVIDER=mock JOBS_ENABLED=false ./scripts/regression-all.sh && cd ../client && npm run typecheck && npm run build manual TODO DONE TODO DONE TODO DONE issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | server/src/routes/assets.js | client/src/pages/AssetRegistryPage.tsx ["P18A-5"] P18A不做资产审批流不做折旧凭证ERP过账
+1
查看文件
@@ -35,6 +35,7 @@ CSV_FACT_SOURCES = [
ROOT / "issues" / "2026-03-07_18-16-00-g2-l5-cross-domain-bridge.csv", ROOT / "issues" / "2026-03-07_18-16-00-g2-l5-cross-domain-bridge.csv",
ROOT / "issues" / "2026-03-07_19-33-09-p17a-l5-procurement-core.csv", ROOT / "issues" / "2026-03-07_19-33-09-p17a-l5-procurement-core.csv",
ROOT / "issues" / "2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv", ROOT / "issues" / "2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv",
ROOT / "issues" / "2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv",
] ]
ID_PATTERN = re.compile(r"^[A-Z][0-9][0-9A-Za-z-]*$") ID_PATTERN = re.compile(r"^[A-Z][0-9][0-9A-Za-z-]*$")
+11 -10
查看文件
@@ -57,7 +57,7 @@ if [[ -n "$JSON_REPORT_PATH" ]]; then
trap rr_cleanup EXIT trap rr_cleanup EXIT
fi fi
TOTAL_STEPS=13 TOTAL_STEPS=14
PASSED_STEPS=0 PASSED_STEPS=0
FAILED_STEPS=0 FAILED_STEPS=0
SKIPPED_STEPS=0 SKIPPED_STEPS=0
@@ -125,15 +125,16 @@ run_step 1 "smoke" "Smoke Test" "1" '"$SCRIPT_DIR"/smoke-test.sh'
run_step 2 "dedup" "Dedup Regression" "1" '"$SCRIPT_DIR"/regression-dedup.sh' run_step 2 "dedup" "Dedup Regression" "1" '"$SCRIPT_DIR"/regression-dedup.sh'
run_step 3 "workflow_actions" "Workflow Actions Regression" "1" '"$SCRIPT_DIR"/regression-workflow-actions.sh' run_step 3 "workflow_actions" "Workflow Actions Regression" "1" '"$SCRIPT_DIR"/regression-workflow-actions.sh'
run_step 4 "procurement" "Procurement Regression" "1" '"$SCRIPT_DIR"/regression-procurement.sh' run_step 4 "procurement" "Procurement Regression" "1" '"$SCRIPT_DIR"/regression-procurement.sh'
run_step 5 "audit" "Audit Regression" "$RUN_AUDIT_REGRESSION" '"$SCRIPT_DIR"/regression-audit.sh' "RUN_AUDIT_REGRESSION=$RUN_AUDIT_REGRESSION" run_step 5 "asset" "Asset Regression" "1" '"$SCRIPT_DIR"/regression-assets.sh'
run_step 6 "ocr" "OCR Regression" "$RUN_OCR_REGRESSION" '"$SCRIPT_DIR"/regression-ocr.sh' "RUN_OCR_REGRESSION=$RUN_OCR_REGRESSION" run_step 6 "audit" "Audit Regression" "$RUN_AUDIT_REGRESSION" '"$SCRIPT_DIR"/regression-audit.sh' "RUN_AUDIT_REGRESSION=$RUN_AUDIT_REGRESSION"
run_step 7 "org_settings" "Org + Settings Regression" "$RUN_ORG_SETTINGS_REGRESSION" '"$SCRIPT_DIR"/regression-org-settings.sh' "RUN_ORG_SETTINGS_REGRESSION=$RUN_ORG_SETTINGS_REGRESSION" run_step 7 "ocr" "OCR Regression" "$RUN_OCR_REGRESSION" '"$SCRIPT_DIR"/regression-ocr.sh' "RUN_OCR_REGRESSION=$RUN_OCR_REGRESSION"
run_step 8 "auth_security" "Auth Security Regression" "$RUN_AUTH_SECURITY_REGRESSION" '"$SCRIPT_DIR"/regression-auth-security.sh' "RUN_AUTH_SECURITY_REGRESSION=$RUN_AUTH_SECURITY_REGRESSION" run_step 8 "org_settings" "Org + Settings Regression" "$RUN_ORG_SETTINGS_REGRESSION" '"$SCRIPT_DIR"/regression-org-settings.sh' "RUN_ORG_SETTINGS_REGRESSION=$RUN_ORG_SETTINGS_REGRESSION"
run_step 9 "verification" "Verification Regression" "$RUN_VERIFICATION_REGRESSION" '"$SCRIPT_DIR"/regression-verification.sh' "RUN_VERIFICATION_REGRESSION=$RUN_VERIFICATION_REGRESSION" run_step 9 "auth_security" "Auth Security Regression" "$RUN_AUTH_SECURITY_REGRESSION" '"$SCRIPT_DIR"/regression-auth-security.sh' "RUN_AUTH_SECURITY_REGRESSION=$RUN_AUTH_SECURITY_REGRESSION"
run_step 10 "dashboard" "Dashboard Regression" "$RUN_DASHBOARD_REGRESSION" '"$SCRIPT_DIR"/regression-dashboard.sh' "RUN_DASHBOARD_REGRESSION=$RUN_DASHBOARD_REGRESSION" run_step 10 "verification" "Verification Regression" "$RUN_VERIFICATION_REGRESSION" '"$SCRIPT_DIR"/regression-verification.sh' "RUN_VERIFICATION_REGRESSION=$RUN_VERIFICATION_REGRESSION"
run_step 11 "budget" "Budget Regression" "$RUN_BUDGET_REGRESSION" '"$SCRIPT_DIR"/regression-budget.sh' "RUN_BUDGET_REGRESSION=$RUN_BUDGET_REGRESSION" run_step 11 "dashboard" "Dashboard Regression" "$RUN_DASHBOARD_REGRESSION" '"$SCRIPT_DIR"/regression-dashboard.sh' "RUN_DASHBOARD_REGRESSION=$RUN_DASHBOARD_REGRESSION"
run_step 12 "integrations" "Integrations Regression" "$RUN_INTEGRATIONS_REGRESSION" '"$SCRIPT_DIR"/regression-integrations.sh' "RUN_INTEGRATIONS_REGRESSION=$RUN_INTEGRATIONS_REGRESSION" run_step 12 "budget" "Budget Regression" "$RUN_BUDGET_REGRESSION" '"$SCRIPT_DIR"/regression-budget.sh' "RUN_BUDGET_REGRESSION=$RUN_BUDGET_REGRESSION"
run_step 13 "feature_flags" "Feature Flags Regression" "$RUN_FEATURE_FLAGS_REGRESSION" '"$SCRIPT_DIR"/regression-feature-flags.sh' "RUN_FEATURE_FLAGS_REGRESSION=$RUN_FEATURE_FLAGS_REGRESSION" run_step 13 "integrations" "Integrations Regression" "$RUN_INTEGRATIONS_REGRESSION" '"$SCRIPT_DIR"/regression-integrations.sh' "RUN_INTEGRATIONS_REGRESSION=$RUN_INTEGRATIONS_REGRESSION"
run_step 14 "feature_flags" "Feature Flags Regression" "$RUN_FEATURE_FLAGS_REGRESSION" '"$SCRIPT_DIR"/regression-feature-flags.sh' "RUN_FEATURE_FLAGS_REGRESSION=$RUN_FEATURE_FLAGS_REGRESSION"
RUN_ENDED_MS="$(rr_now_ms)" RUN_ENDED_MS="$(rr_now_ms)"
OVERALL_DURATION_MS="$((RUN_ENDED_MS - RUN_STARTED_MS))" OVERALL_DURATION_MS="$((RUN_ENDED_MS - RUN_STARTED_MS))"
+283
查看文件
@@ -0,0 +1,283 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-assets}"
EMPLOYEE_IDENTIFIER="${EMPLOYEE_IDENTIFIER:-employee@hyinda.com}"
EMPLOYEE_PASSWORD="${EMPLOYEE_PASSWORD:-EmployeePassword1234}"
FINANCE_MGR_IDENTIFIER="${FINANCE_MGR_IDENTIFIER:-finance_mgr@hyinda.com}"
FINANCE_MGR_PASSWORD="${FINANCE_MGR_PASSWORD:-FinanceMgrPassword1234}"
if ! command -v curl >/dev/null 2>&1; then
echo "[ERROR] curl not found" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "[ERROR] jq not found" >&2
exit 1
fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
if [[ -z "${DATABASE_URL:-}" && -f ".env" ]]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
fi
if [[ -z "${DATABASE_URL:-}" ]]; then
echo "[ERROR] DATABASE_URL is required (export it or provide server/.env)" >&2
exit 1
fi
TMP_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
assert_status() {
local actual="$1"
local expected="$2"
local name="$3"
local body_file="${4:-}"
if [[ "$actual" != "$expected" ]]; then
echo "[ERROR] $name expected HTTP $expected but got $actual" >&2
[[ -n "$body_file" && -f "$body_file" ]] && cat "$body_file" >&2
exit 1
fi
}
assert_jq() {
local file_path="$1"
local jq_expr="$2"
local name="$3"
if ! jq -e "$jq_expr" "$file_path" >/dev/null; then
echo "[ERROR] $name assertion failed: $jq_expr" >&2
cat "$file_path" >&2
exit 1
fi
}
login_user() {
local identifier="$1"
local password="$2"
local out_file="$3"
local status
status="$(curl -sS -o "$out_file" -w "%{http_code}" \
-X POST "$BASE_URL/api/auth/login" \
-H "content-type: application/json" \
-H "x-device-fingerprint: $DEVICE_FINGERPRINT" \
-d "{\"identifier\":\"$identifier\",\"password\":\"$password\"}")"
assert_status "$status" "200" "login $identifier" "$out_file"
jq -er '.data.accessToken' "$out_file"
}
api_json_auth() {
local method="$1"
local url="$2"
local token="$3"
local out_file="$4"
local body="${5:-}"
if [[ -n "$body" ]]; then
curl -sS -o "$out_file" -w "%{http_code}" \
-X "$method" "$url" \
-H "authorization: Bearer $token" \
-H "content-type: application/json" \
-d "$body"
else
curl -sS -o "$out_file" -w "%{http_code}" \
-X "$method" "$url" \
-H "authorization: Bearer $token"
fi
}
resolve_fixture_ids() {
DATABASE_URL="$DATABASE_URL" node - <<'NODE'
const { createPrismaClient } = require('./src/prismaClient');
(async () => {
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
try {
const employee = await prisma.employee.findUnique({
where: { email: process.env.SEED_EMPLOYEE_EMAIL ?? '[email protected]' },
select: { id: true, legal_entity_id: true },
});
const costCenter = await prisma.costCenter.findFirst({
where: {
legal_entity_id: employee.legal_entity_id,
enabled: true,
archived_at: null,
},
orderBy: { created_at: 'asc' },
select: { id: true },
});
const documentCategory = await prisma.documentCategory.findFirst({
where: {
entity_type: 'asset_card',
enabled: true,
archived_at: null,
code: 'ASSET_CARD_DEFAULT',
},
orderBy: { created_at: 'asc' },
select: { id: true },
}) ?? await prisma.documentCategory.findFirst({
where: {
entity_type: 'asset_card',
enabled: true,
archived_at: null,
},
orderBy: { created_at: 'asc' },
select: { id: true },
});
const approvedPurchase = await prisma.purchaseRequest.findFirst({
where: { status: 'approved' },
orderBy: { updated_at: 'desc' },
select: { id: true },
});
process.stdout.write([
employee.id,
employee.legal_entity_id ?? '',
costCenter?.id ?? '',
documentCategory?.id ?? '',
approvedPurchase?.id ?? '',
].join('\n'));
} finally {
await prisma.$disconnect();
await pool.end();
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
}
assert_asset_state() {
local asset_id="$1"
local category_id="$2"
local employee_id="$3"
local purchase_request_id="$4"
DATABASE_URL="$DATABASE_URL" node - "$asset_id" "$category_id" "$employee_id" "$purchase_request_id" <<'NODE'
const [assetId, categoryId, employeeId, purchaseRequestId] = process.argv.slice(2);
const { createPrismaClient } = require('./src/prismaClient');
(async () => {
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
try {
const category = await prisma.assetCategory.findUnique({
where: { id: categoryId },
select: { name: true, document_category: { select: { code: true } } },
});
const asset = await prisma.assetCard.findUnique({
where: { id: assetId },
select: {
asset_no: true,
status: true,
assignee_employee_id: true,
purchase_request_id: true,
location: true,
},
});
if (!category || category.name !== '回归资产分类-已更新' || category.document_category?.code !== 'ASSET_CARD_DEFAULT') {
console.error('[ERROR] asset category state mismatch', category);
process.exit(1);
}
if (!asset || !asset.asset_no.startsWith('ZC-') || asset.status !== 'in_use' || asset.assignee_employee_id !== employeeId) {
console.error('[ERROR] asset card state mismatch', asset);
process.exit(1);
}
if (purchaseRequestId && asset.purchase_request_id !== purchaseRequestId) {
console.error('[ERROR] asset purchase source mismatch', asset);
process.exit(1);
}
if (asset.location !== '深圳办公室 A-01') {
console.error('[ERROR] asset location mismatch', asset);
process.exit(1);
}
} finally {
await prisma.$disconnect();
await pool.end();
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
}
echo "[1/8] login finance manager and employee"
FINANCE_MGR_TOKEN="$(login_user "$FINANCE_MGR_IDENTIFIER" "$FINANCE_MGR_PASSWORD" "$TMP_DIR/finance-mgr-login.json")"
EMPLOYEE_TOKEN="$(login_user "$EMPLOYEE_IDENTIFIER" "$EMPLOYEE_PASSWORD" "$TMP_DIR/employee-login.json")"
mapfile -t FIXTURE_IDS < <(resolve_fixture_ids)
EMPLOYEE_ID="${FIXTURE_IDS[0]}"
LEGAL_ENTITY_ID="${FIXTURE_IDS[1]}"
COST_CENTER_ID="${FIXTURE_IDS[2]}"
DOCUMENT_CATEGORY_ID="${FIXTURE_IDS[3]}"
APPROVED_PURCHASE_ID="${FIXTURE_IDS[4]}"
if [[ -z "$LEGAL_ENTITY_ID" || -z "$COST_CENTER_ID" || -z "$DOCUMENT_CATEGORY_ID" ]]; then
echo "[ERROR] missing asset fixtures" >&2
exit 1
fi
echo "[2/8] create asset category"
CATEGORY_CODE="ASSET_REG_$(date +%s)"
CREATE_CATEGORY_BODY="$(jq -nc --arg code "$CATEGORY_CODE" --arg dc "$DOCUMENT_CATEGORY_ID" '{code:$code,name:"回归资产分类",documentCategoryId:$dc,enabled:true}')"
create_category_status="$(api_json_auth POST "$BASE_URL/api/asset-categories" "$FINANCE_MGR_TOKEN" "$TMP_DIR/create-category.json" "$CREATE_CATEGORY_BODY")"
assert_status "$create_category_status" "200" "create asset category" "$TMP_DIR/create-category.json"
CATEGORY_ID="$(jq -er '.data.assetCategory.id' "$TMP_DIR/create-category.json")"
assert_jq "$TMP_DIR/create-category.json" '.data.assetCategory.documentCategory.code == "ASSET_CARD_DEFAULT"' "asset category document category"
echo "[3/8] update asset category"
PATCH_CATEGORY_BODY='{"name":"回归资产分类-已更新","enabled":true}'
patch_category_status="$(api_json_auth PATCH "$BASE_URL/api/asset-categories/$CATEGORY_ID" "$FINANCE_MGR_TOKEN" "$TMP_DIR/patch-category.json" "$PATCH_CATEGORY_BODY")"
assert_status "$patch_category_status" "200" "patch asset category" "$TMP_DIR/patch-category.json"
assert_jq "$TMP_DIR/patch-category.json" '.data.assetCategory.name == "回归资产分类-已更新"' "patched asset category name"
echo "[4/8] create asset card with approved purchase source"
CREATE_ASSET_BODY="$(jq -nc --arg name "回归资产卡片" --arg categoryId "$CATEGORY_ID" --arg legalEntityId "$LEGAL_ENTITY_ID" --arg costCenterId "$COST_CENTER_ID" --arg purchaseRequestId "$APPROVED_PURCHASE_ID" --arg assigneeEmployeeId "$EMPLOYEE_ID" '{name:$name,categoryId:$categoryId,legalEntityId:$legalEntityId,costCenterId:$costCenterId,purchaseRequestId:($purchaseRequestId|select(length>0)),status:"in_stock",assigneeEmployeeId:$assigneeEmployeeId}')"
create_asset_status="$(api_json_auth POST "$BASE_URL/api/assets" "$FINANCE_MGR_TOKEN" "$TMP_DIR/create-asset.json" "$CREATE_ASSET_BODY")"
assert_status "$create_asset_status" "200" "create asset" "$TMP_DIR/create-asset.json"
ASSET_ID="$(jq -er '.data.asset.id' "$TMP_DIR/create-asset.json")"
assert_jq "$TMP_DIR/create-asset.json" '.data.asset.assetNo | test("^ZC-")' "asset no generated"
assert_jq "$TMP_DIR/create-asset.json" '.data.asset.assignee.employeeNo == "EMP001" or .data.asset.assignee.name == "Employee"' "asset assignee set"
echo "[5/8] employee can only see own assigned asset"
list_own_status="$(api_json_auth GET "$BASE_URL/api/assets?mine=1" "$EMPLOYEE_TOKEN" "$TMP_DIR/list-own.json")"
assert_status "$list_own_status" "200" "employee list own assets" "$TMP_DIR/list-own.json"
assert_jq "$TMP_DIR/list-own.json" ".data.mine == true and (.data.items | map(.id) | index(\"$ASSET_ID\") != null)" "employee owns asset"
echo "[6/8] finance manager updates asset status and location"
PATCH_ASSET_BODY='{"status":"in_use","location":"深圳办公室 A-01"}'
patch_asset_status="$(api_json_auth PATCH "$BASE_URL/api/assets/$ASSET_ID" "$FINANCE_MGR_TOKEN" "$TMP_DIR/patch-asset.json" "$PATCH_ASSET_BODY")"
assert_status "$patch_asset_status" "200" "patch asset" "$TMP_DIR/patch-asset.json"
assert_jq "$TMP_DIR/patch-asset.json" '.data.asset.status == "in_use" and .data.asset.location == "深圳办公室 A-01"' "asset status/location updated"
echo "[7/8] purchase source is immutable after create"
INVALID_PATCH_BODY="$(jq -nc --arg purchaseRequestId "$APPROVED_PURCHASE_ID" '{purchaseRequestId:$purchaseRequestId}')"
invalid_patch_status="$(api_json_auth PATCH "$BASE_URL/api/assets/$ASSET_ID" "$FINANCE_MGR_TOKEN" "$TMP_DIR/invalid-patch.json" "$INVALID_PATCH_BODY")"
assert_status "$invalid_patch_status" "400" "patch asset purchase source forbidden" "$TMP_DIR/invalid-patch.json"
assert_jq "$TMP_DIR/invalid-patch.json" '.error.code == "VALID_001"' "invalid purchase source patch code"
echo "[8/8] verify category and asset db state"
assert_asset_state "$ASSET_ID" "$CATEGORY_ID" "$EMPLOYEE_ID" "$APPROVED_PURCHASE_ID"
echo "[OK] Asset regression passed"
echo "- assetCategoryId: $CATEGORY_ID"
echo "- assetId: $ASSET_ID"
@@ -44,6 +44,17 @@ describe("Permission Constants", () => {
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "procurement.manage")).toBe(true); expect(hasPermission(resolvePermissionsForRoles(["administration"]), "procurement.manage")).toBe(true);
}); });
it("grants asset permissions to configured roles", () => {
expect(hasPermission(resolvePermissionsForRoles(["employee"]), "asset.view_own")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["manager"]), "asset.view_all")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["finance"]), "asset.view_all")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "asset.manage")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "asset.transfer")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["finance_mgr"]), "asset.inventory")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["employee"]), "asset.manage")).toBe(false);
});
it("maps workflow entities to correct permission codes", () => { it("maps workflow entities to correct permission codes", () => {
expect(resolveEntityPermission("reimbursement", "approve")).toBe("expense.approve"); expect(resolveEntityPermission("reimbursement", "approve")).toBe("expense.approve");
expect(resolveEntityPermission("reimbursement", "transfer")).toBe("expense.transfer"); expect(resolveEntityPermission("reimbursement", "transfer")).toBe("expense.transfer");
@@ -130,4 +130,27 @@ describe("documentSequence service", () => {
expect(issued.config.source).toBe("document_category"); expect(issued.config.source).toBe("document_category");
expect(issued.documentNumber).toMatch(/^CG-\d{8}-0001$/); expect(issued.documentNumber).toMatch(/^CG-\d{8}-0001$/);
}); });
it("falls back to default asset category code for asset cards", async () => {
const prisma = buildPrisma({
categoryMap: {
ASSET_CARD_DEFAULT: {
code: "ASSET_CARD_DEFAULT",
entity_type: "asset_card",
sequence_prefix: "ZC",
number_length: 4,
reset_period: "yearly",
enabled: true,
archived_at: null,
},
},
});
const config = await resolveDocumentSequenceConfig(prisma, { documentType: "asset_card" });
expect(config.source).toBe("document_category");
expect(config.documentCategoryCode).toBe("ASSET_CARD_DEFAULT");
expect(config.prefix).toBe("ZC");
});
}); });