From 79441a8a87bae981e089d1a6a88b71f3c72bc33a Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Wed, 23 Sep 2026 09:59:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=B9=E6=AC=A16=EF=BC=9AD4=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=B2=BB=E7=90=86=E6=94=B6=E5=8F=A3=20+=20D13?= =?UTF-8?q?=E9=94=81=E6=96=87=E4=BB=B6=E6=B5=81=E7=A8=8B=E5=9B=BA=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D4 文档/规划/历史混放收口: - docs/TECH_DEBT.md §2 由'按批次回顾'精简为'按主题摘要', 与§3重复内容(批次0-4详细展开)整体迁入 docs/archive/2026-09_governance_batches.md - docs/STATUS.md 顶部 2026-09-17 之前条目迁入 docs/archive/2026-09_status_history.md,仅保留指针 - D2 历史口径补齐为 2026-09-18 批次4后续专项清偿 - D4 标已清偿 - AGENTS.md / docs/archive/README.md 同步导航 D13 锁文件流程固化(镜像引入主体已清偿,仅剩锁文件落盘): - 新增 deploy/generate_lockfiles.{sh,bat}:在 moldinsight conda 环境(仅项目依赖)执行 pip freeze --exclude pythonocc-core, 产出 deploy/requirements-{base,moldinsight}.lock.txt - deploy/Dockerfile.moldinsight 注释改为指向生成脚本 - docs/OPERATIONS.md §2.1 增加完整流程说明 - tests/test_lockfile_generation.py 加锁文件存在性+体积契约; tests/conftest.py 注册 --run-lockfile-check 选项, 默认 skip(仓库单测不阻塞),CI 镜像构建 job 显式启用 fail-fast 遗留:锁文件本身尚未落盘(本机Miniforge跨项目开发栈混装, 污染严重不能直接 pip freeze);待 CI / 生产首次构建时按流程落锁。 测试基线:126 passed, 9 skipped(默认4原有skip + D13新增5skip; 启用 --run-lockfile-check 时严格断言2项锁文件契约) Co-Authored-By: Claude Code --- AGENTS.md | 3 +- deploy/Dockerfile.moldinsight | 6 +- deploy/generate_lockfiles.bat | 38 ++++++++ deploy/generate_lockfiles.sh | 50 ++++++++++ docs/OPERATIONS.md | 16 ++- docs/STATUS.md | 5 + docs/TECH_DEBT.md | 107 +++++++-------------- docs/archive/2026-09_governance_batches.md | 100 +++++++++++++++++++ docs/archive/2026-09_status_history.md | 10 ++ docs/archive/README.md | 2 + tests/conftest.py | 22 +++++ tests/test_lockfile_generation.py | 105 ++++++++++++++++++++ 12 files changed, 390 insertions(+), 74 deletions(-) create mode 100644 deploy/generate_lockfiles.bat create mode 100644 deploy/generate_lockfiles.sh create mode 100644 docs/archive/2026-09_governance_batches.md create mode 100644 docs/archive/2026-09_status_history.md create mode 100644 tests/test_lockfile_generation.py diff --git a/AGENTS.md b/AGENTS.md index 3fd625d..3204f3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,7 @@ - **单数据库是刻意设计**:moldinsight 与 inventory 共享同一 PostgreSQL(如 `STPFile.product_id -> Product.id` 桥接),不拆库。 - **接口变更三件套**:优先用 Pydantic 请求模型(少用手写 `request.json()` 解析)→ 重新导出根目录 `openapi.json` → 前端 `npm run gen:api` 重新生成类型。三步缺一即契约漂移。 - **历史材料统一进 [docs/archive/](docs/archive/README.md)**,不与当前权威文档混放。 +- **历史批次详细流水账 / 早段 STATUS**:见 [docs/archive/2026-09_governance_batches.md](docs/archive/2026-09_governance_batches.md) 与 [docs/archive/2026-09_status_history.md](docs/archive/2026-09_status_history.md);主骨架权威文档(TECH_DEBT §2 / STATUS 顶部)只保留摘要。 - **配置只走 `.env`**(参照 [.env.example](.env.example) 全键说明):`DB_*`、`SECRET_KEY` 等关键项不设代码兜底(惰性校验,缺失即报),不在代码里给 localhost/弱口令默认值。 ## 3. 代码地图 @@ -120,7 +121,7 @@ frontend/ # Vue 3 独立工程:src/modules 按域组织 migrations/ # 数据库迁移 scripts/ # 一次性迁移与工具脚本(migrations/ 数据迁移、db/ 索引与审计 SQL、tools/ 检查工具),非运行时代码 tests/ # pytest:sqlite+aiosqlite 临时库;pythonocc 缺失时 OCC 契约测试自动 skip -deploy/ # Dockerfile.* / nginx / build 脚本 +deploy/ # Dockerfile.* / nginx / build 脚本 / generate_lockfiles.{sh,bat}(D13 锁文件生成入口) docs/ # 权威文档(本文件 §5 导航) ``` diff --git a/deploy/Dockerfile.moldinsight b/deploy/Dockerfile.moldinsight index fd43ec6..167f59a 100644 --- a/deploy/Dockerfile.moldinsight +++ b/deploy/Dockerfile.moldinsight @@ -4,8 +4,10 @@ # conda 运行时作为最终镜像的执行环境,自带全部动态库。 FROM continuumio/miniconda3:24.7.1-0 -# 锁定几何栈核心版本;pip 侧全量版本锁待首次镜像构建成功后由 -# `pip freeze > deploy/requirements-moldinsight.lock.txt` 生成(D13 遗留项) +# 锁定几何栈核心版本;pip 侧全量版本锁由 +# `bash deploy/generate_lockfiles.sh` 在 moldinsight conda 环境内执行后生成 +# (D13,参见 deploy/generate_lockfiles.sh / .bat 与 docs/OPERATIONS.md §2) +# 落盘产物为 deploy/requirements-{base,moldinsight}.lock.txt,CI / 离线构建可直接锁定安装 RUN conda create -n moldinsight -c conda-forge -y \ python=3.12 \ pythonocc-core=7.9.0 \ diff --git a/deploy/generate_lockfiles.bat b/deploy/generate_lockfiles.bat new file mode 100644 index 0000000..4b4c84a --- /dev/null +++ b/deploy/generate_lockfiles.bat @@ -0,0 +1,38 @@ +@echo off +REM ============================================================ +REM geMoldInsight pip 锁文件生成脚本(TECH_DEBT D13,Windows 版) +REM ============================================================ +REM +REM 用法:在 gemold conda 环境内执行 deploy\generate_lockfiles.bat +REM +REM 产物: +REM deploy\requirements-base.lock.txt +REM deploy\requirements-moldinsight.lock.txt +REM ============================================================ + +setlocal enabledelayedexpansion +cd /d "%~dp0\.." + +where conda >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] conda 未安装或不在 PATH,请先激活 conda 环境 ^(推荐 moldinsight^) 1>&2 + exit /b 1 +) + +if not "%CONDA_DEFAULT_ENV%"=="moldinsight" if not "%CONDA_DEFAULT_ENV%"=="gemold" ( + echo [WARN] 当前 conda 环境为 '%CONDA_DEFAULT_ENV%',推荐在 'moldinsight' 内执行 +) + +echo ==^> 生成 base 锁文件 +pip freeze --exclude pythonocc-core > deploy\requirements-base.lock.txt + +echo ==^> 生成 moldinsight 锁文件 +pip freeze --exclude pythonocc-core > deploy\requirements-moldinsight.lock.txt + +echo. +echo 已生成: +echo deploy\requirements-base.lock.txt +echo deploy\requirements-moldinsight.lock.txt +echo. +echo 下一步:提交两个 lock.txt,并按团队策略同步更新 requirements-*.txt 下限。 +endlocal diff --git a/deploy/generate_lockfiles.sh b/deploy/generate_lockfiles.sh new file mode 100644 index 0000000..3b3720b --- /dev/null +++ b/deploy/generate_lockfiles.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# ============================================================ +# geMoldInsight pip 锁文件生成脚本(TECH_DEBT D13) +# ============================================================ +# +# 设计要点: +# - 锁文件必须在构建产出的 conda/minimal 环境里生成(只有 geMoldInsight 依赖 + 基础库), +# 本机开发环境的 `pip freeze` 会污染(全开发栈混装),不能直接落锁。 +# - 仅在带 moldinsight 的 conda 环境内运行 `pip freeze > deploy/requirements-.lock.txt` +# 才有意义。 +# - 锁文件落盘后即可被 CI / 离线构建 / 复现部署直接 `pip install -r` 锁定版本, +# 而不再依赖 >= 下限解析。 +# +# 用法(必须在 `gemold` conda 环境内执行): +# bash deploy/generate_lockfiles.sh +# +# 产物: +# deploy/requirements-base.lock.txt +# deploy/requirements-moldinsight.lock.txt +# ============================================================ + +set -euo pipefail + +cd "$(dirname "$0")/.." + +if ! command -v conda >/dev/null 2>&1; then + echo "[ERROR] conda 未安装或不在 PATH,请先激活 conda 环境(推荐环境名 moldinsight)" >&2 + exit 1 +fi + +if [[ "${CONDA_DEFAULT_ENV:-}" != "moldinsight" && "${CONDA_DEFAULT_ENV:-}" != "gemold" ]]; then + echo "[WARN] 当前 conda 环境为 '${CONDA_DEFAULT_ENV:-<未激活>}'," \ + "推荐在 'moldinsight' conda 环境内运行(否则锁文件将含宿主污染)" >&2 +fi + +echo "==> 生成 base 锁文件" +pip freeze --exclude pythonocc-core > deploy/requirements-base.lock.txt + +echo "==> 生成 moldinsight 锁文件" +pip freeze --exclude pythonocc-core > deploy/requirements-moldinsight.lock.txt + +echo +echo "已生成:" +echo " deploy/requirements-base.lock.txt ($(wc -l < deploy/requirements-base.lock.txt) 行)" +echo " deploy/requirements-moldinsight.lock.txt ($(wc -l < deploy/requirements-moldinsight.lock.txt) 行)" +echo +echo "下一步:" +echo " 1. 提交这两个 lock.txt(仅含项目直接依赖 + conda-minimal 环境产出)" +echo " 2. 同步更新 deploy/requirements-base.txt / requirements-moldinsight.txt 的版本下限" \ + "为 lock 中的实际版本(或保留 >=,按团队策略)" diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 2b9303e..3aed0dc 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -26,10 +26,24 @@ ## 2. 安装与环境 - 后端依赖:`pip install -r requirements.txt`。 -- **OCC 几何能力**:PythonOCC 不走 pip 主路径,通过 conda 环境提供(本项目实践环境名 `gemold`)。无 OCC 环境时项目可启动,但几何分析契约测试自动 skip。 +- **OCC 几何能力**:PythonOCC 不走 pip 主路径,通过 conda 环境提供(本项目实践环境名 `gemold` 或 `moldinsight`)。无 OCC 环境时项目可启动,但几何分析契约测试自动 skip。 - 前端:`cd frontend && npm install`。 - 数据库迁移:`migrations/`(`alembic.ini` 在仓库根;2026-09-16 由 `alembic/` 改名——原目录名与 alembic 包重名,应用内 import 会被遮蔽导致启动期迁移静默失败);数据修复类一次性脚本在 `scripts/migrations/` 与 `scripts/db/`,**不是运行时代码**,勿在服务内引用。 +### 2.1 pip 锁文件生成(D13 流程) + +`deploy/requirements-{base,moldinsight}.lock.txt` 是项目依赖的**版本锁**,由 conda 环境首次构建成功后一次性落盘: + +- **生成时机**:在 `moldinsight` / `gemold` conda 环境(仅含项目依赖 + conda 基础库,**不能**在混装全开发栈的本机 pip 环境跑)执行 `pip freeze` +- **生成命令**: + - Linux / macOS:`bash deploy/generate_lockfiles.sh` + - Windows:`deploy\generate_lockfiles.bat` +- **产物**: + - `deploy/requirements-base.lock.txt` + - `deploy/requirements-moldinsight.lock.txt` +- **消费方**:CI、离线构建、生产复现部署;`pip install -r deploy/requirements-base.lock.txt` 可直接锁定安装而不依赖 `>=` 解析 +- **提交策略**:两个 lock.txt 提交到仓库;版本下限(`requirements-{base,moldinsight}.txt`)按团队策略同步或保留 `>=` 灵活解析 + ## 3. 本地启动 后端三入口(均含 sys.path 修正,可从仓库根直接跑): diff --git a/docs/STATUS.md b/docs/STATUS.md index f22e7e6..89c3fff 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -2,9 +2,14 @@ > 文档定位:**唯一的「现在到哪了」**。README / AGENTS / 各主文档只链接到这里,不复制状态内容。 > 维护规则:每完整完成一个需求,**倒序在本文顶部加一条**(日期 + 主题 + 关键事实);其余主文档(架构 / 规划 / 技术债 / 部署)维护各自的"当前有效说法",本文只记录"什么时候做到了哪一步"。维护规则出处见根目录 [AGENTS.md](../AGENTS.md)。 +> 早期条目(2026-09-17 之前)已精简为锚点,完整流水见 [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md) 与 [archive/2026-09_status_history.md](archive/2026-09_status_history.md)。 > 2026-09-22(**Pydantic v2 schema 配置升级 + `datetime.utcnow()` 弃用清零**:① 全仓 14 处 `class Config`([src/inventory/schemas](../src/inventory/schemas/))+ [src/shared/services/auth_routes.py](../src/shared/services/auth_routes.py) 三处全部迁移到 `model_config = ConfigDict(from_attributes=True)`;② [src/shared/services/auth_service.py](../src/shared/services/auth_service.py) 中 `datetime.utcnow()` 改用 `datetime.now(timezone.utc)`,消除遗留 `DeprecationWarning`;③ 一次跑通 `pytest tests/ -q` 全量无 deprecation 警告,全仓 `from_attributes=True` 语义保持不变,未触发 OpenAPI 漂移。**测试基线**:**126 passed, 4 skipped**(与上一批次一致,无回归)。) > +> 2026-09-22(**D13 锁文件流程固化:镜像引入已清偿 + 落锁流程就绪**:① 新增 [deploy/generate_lockfiles.sh](../deploy/generate_lockfiles.sh) / [.bat](../deploy/generate_lockfiles.bat):在 moldinsight conda 环境(仅项目依赖,**不能**在混装开发栈跑)执行 `pip freeze --exclude pythonocc-core`,产出 `deploy/requirements-{base,moldinsight}.lock.txt`;② [Dockerfile.moldinsight](../deploy/Dockerfile.moldinsight) 注释改为指向生成脚本;③ [docs/OPERATIONS.md](../docs/OPERATIONS.md) §2.1 增加完整流程说明(生成时机 / 命令 / 产物 / 消费方 / 提交策略);④ [tests/test_lockfile_generation.py](../tests/test_lockfile_generation.py) 加锁文件存在性 + 体积契约,默认 skip(仓库单测不阻塞),CI 镜像构建 job 显式 `pytest --run-lockfile-check` 启用 fail-fast。**遗留**:锁文件本身尚未落盘——本机 Miniforge 装的是跨项目开发栈混装环境,污染严重不能直接用 `pip freeze`;须等 CI / 生产机器首次构建 moldinsight 镜像后按流程落锁。**测试基线**:**126 passed, 9 skipped**(默认 4 个原有 skip + D13 新增 5 个 skip;启用 `--run-lockfile-check` 时严格断言 2 项锁文件契约)。) +> +> 2026-09-02 起(含 09-17 之前基线条目)的完整内容见 [archive/2026-09_status_history.md](archive/2026-09_status_history.md)。 +> > 2026-09-21(**inventory 仪表盘聚合服务下沉完成:dashboard 薄路由化**:① 新增 [dashboard_service.py](../src/inventory/services/dashboard_service.py),将仪表盘首页所需的基础主数据统计、物料库存总量/总值、待处理采购/销售单数、低库存预警列表等聚合查询从路由层下沉到 service;② [dashboard_routes.py](../src/inventory/api/dashboard_routes.py) 改为单行委托薄路由,inventory 主要业务域路由已基本完成 service orchestration 收口;③ 新增 [test_api_dashboard_service.py](../tests/test_api_dashboard_service.py),覆盖 seeded summary 与低库存预警两条 API 回归。**接口面零变化**(无 openapi 漂移)。**测试基线**:**126 passed, 4 skipped**;新增 dashboard 回归 **2 passed**。) > > 2026-09-21(**inventory 产品域跨模块桥接收口完成:`/api/products/from-task/{task_id}` 下沉至 `product_service`**:① [product_service.py](../src/inventory/services/product_service.py) 新增 `create_product_from_task`,将 ProcessingTask / STPFile 查询、已绑定成品幂等返回、`MI{stp_file_id}` SKU 冲突递增、分析结果摘要拼装、成品创建与 `stp_files.product_id` 回写从路由层下沉到 service;② [product_routes.py](../src/inventory/api/product_routes.py) 现已全量薄路由化,产品域 CRUD / BOM / from-task 三类接口统一改为 service orchestration;③ 扩展 [test_api_product_service.py](../tests/test_api_product_service.py) 与 [tests/conftest.py](../tests/conftest.py),补 `STPFile` / `ProcessingTask` 种子及 from-task 创建、重复调用幂等、任务不存在 404 回归。**接口面零变化**(无 openapi 漂移)。**测试基线**:**124 passed, 4 skipped**;product 域回归现为 **14 passed**。) diff --git a/docs/TECH_DEBT.md b/docs/TECH_DEBT.md index 23244f1..2535e53 100644 --- a/docs/TECH_DEBT.md +++ b/docs/TECH_DEBT.md @@ -18,59 +18,33 @@ --- -## 2. 已完成的重要治理(摘要) +## 2. 已完成的重要治理(主题摘要) -以下高价值治理已完成: +按主题归类的高价值治理已完成项。每项的具体修复清单 / 迁移号 / 回归测试 / 测试基线见归档: + +- [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md):批次 0–4 + 后续专项 + 2026-09-21 inventory 服务下沉 + 2026-09-22 schema/datetime 弃用清零 的完整流水账 +- [archive/MOLDINSIGHT_TECH_DEBT_PLAN.md](archive/MOLDINSIGHT_TECH_DEBT_PLAN.md):更早的设计审查原始计划 ### 2.1 安全与权限 -- debug/history 路由补鉴权 -- 任务访问控制收紧 -- 无主数据不再默认放行 -- `/api/status/{task_id}` 补 JWT 鉴权与归属校验(原 D5,2026-09-16 清偿,见 D5 条目) -- bcrypt 创建口令超 72 字节显式拒绝、验证侧截断比较;`SECRET_KEY` / `RUSTFS_*` 缺失时明确报错,代码侧弱默认移除(D14 部分,2026-09-16) +- debug / history 路由补鉴权;任务访问控制收紧;无主数据不再默认放行 +- `/api/status/{task_id}` 补 JWT 鉴权与归属校验(原 D5 → §3 D5) +- bcrypt 超 72 字节显式拒绝 + 截断比较;`SECRET_KEY` / `RUSTFS_*` 缺失明确报错 ### 2.2 静默失败与可用性 - `detect-undercuts` 改为基于真实 shape 分析 -- OCC 超时后重建 executor,避免全队列永久堵死 +- OCC 超时后重建 executor(短期)→ D10 方案 B 进程化彻底替换 - 后台任务统一分派,补强引用与并发控制 ### 2.3 状态存储与缓存 - Redis 任务状态改为 Hash 字段级更新,兼容旧格式 -- 完成态任务视图增加缓存 -- 导出缓存与持久化链路收口,支持重启后再导出 +- 内存回退彻底删除,PG 为任务状态单一事实源(原 D7) +- 完成态任务视图缓存;导出缓存与持久化链路收口 ### 2.4 架构与代码清理 -- 删除旧单体入口与死代码 -- 设置惰性配置校验,提升可测试性 -- Generator 公共接口提取完成,补充契约测试 - -### 2.5 部署正确性(2026-09-16,批次 0/1) -- `/api/status/{task_id}` 补鉴权与归属校验(原 D5) -- 主处理链路改走 RustFS:分派入参 `stp_file_id` 化,源文件按 object_key 下载;compose 共享卷过渡兜底(原 D6) -- `AUTO_MIGRATE` 开关 + 迁移脚本随镜像分发 + `alembic/`→`migrations/` 改名修复包遮蔽(原 D12) -- OCC 镜像改 conda 运行时原生执行、基础镜像 tag 锁定(D13 主体);compose 关键项去弱默认(D14 部分) - -### 2.6 任务一致性模型(2026-09-16,批次 2) -- Redis 内存回退彻底删除,PG 为任务状态单一事实源(原 D7);批量元数据入库(`processing_tasks.batch_id`,迁移 `a3f8c2d91e47`) -- 型腔生成失败任务标 failed,不再静默 completed(原 D8) -- 持久化事务边界收口:数据本体分阶段原子提交、失败先回滚再置 failed(原 D9) -- D11(HTML 双写双读)本批未动:正确性已由共享卷兜底,RustFS 单一来源留待后续批次 - -### 2.7 API 与代码结构(2026-09-17,批次 3) -- `advanced_router` 按职责拆为 design / cost / machining / export 四个子路由,端点路径不变,请求体全量 Pydantic 化(原 D1) -- 路由装载失败显式化:`ROUTE_MODULES` 清单 + route_registry,失败经 `/api/health` 呈现 degraded(含真实 pythonocc 探测),DEBUG 下 fail fast -- 纯 Python 重计算端点(设计/加工/CAM 打包)统一 `asyncio.to_thread` 投放线程池,不再阻塞事件循环;OCC 操作仍走单线程 executor(D10 不变,批次 4) -- `StorageIntegrationService`(867 行)按职责拆为 TaskStorage / AnalysisStorage / FileHistory 三服务;无调用方的 `log_user_activity` 死代码删除 -- 配置治理收尾:`MAX_FILE_SIZE` 接线生效、celery_app 复用 `Settings.redis_url`(原 D14) -- 连带修复:管理员重置密码改 JSON body(原裸 str 参数被解析为 query param,前端发 body 必 422,功能端到端断裂);Dockerfile.celery 的 FROM tag 与 compose/build.sh 实际构建的 `gemold-backend:latest` 对齐(此前干净环境 celery 镜像必构建失败) -- 接口变更三件套随批完成:openapi.json 重导出(76 paths)+ 前端 `gen:api` - -### 2.8 架构演进(2026-09-17,批次 4) -- 共享 ORM 按模块拆分(原 D3 主体):891 行 `shared/models/database.py`(31 模型类三类同居)拆为 `shared/models/base.py`(唯一 Base + 归属约定)/ `shared/models/identity.py`(7 表)/ `moldinsight/models/`(9 表)/ `inventory/models/`(catalog/warehouse/trading/finance 15 表);**三条跨模块 ORM relationship(`User.stp_files`、`STPFile.user`、`STPFile.product`)经全仓核实均无使用方,直接删除**——跨模块桥接收敛为裸 FK 硬规则(ARCHITECTURE §5.1),单模块部署不再依赖另一侧模型注册;约 45 处 import 全量改写,无兼容 facade;全量注册点收敛为 migrations/env.py 与 tests/conftest.py;零调用方的死方法 `db_manager.create_tables` 一并删除(拆分后会静默建残缺 schema) -- OCC 泄漏治理 + 吞吐方案设计先行(原 D10):`_reset_occ_executor` 补 `cancel_futures=True`——不止卫生问题:旧实现下"慢恢复"的旧线程会继续消化旧队列,与新 executor **并发操作非线程安全的 OCC**(数据竞争);吞吐路线定稿于 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md)(短期 A:celery prefork 伸缩 + max-tasks-per-child 兜底;中期 B:run_occ 接口进程化 + kill-on-timeout 根治) -- 归属边界回归测试:[tests/test_model_ownership.py](../tests/test_model_ownership.py)(31 表全量注册、单模块独立 mapper 配置、旧模块无 facade) - -详细历史过程保留在原始技术债文档中,后续将转入归档。 +- 删除旧单体入口与死代码(`db_manager.create_tables` / `log_user_activity` / `CADExporter.export_mold_results` / `getAluminumPrice` 等) +- 惰性配置校验,提升可测试性 +- Generator 公共接口提取 + 契约测试 +- 共享 ORM 按模块拆分,跨模块桥接收敛为裸 FK 硬规则(ARCHITECTURE §5.1) --- @@ -86,24 +60,19 @@ ~~原现状 / 影响~~:导出/估算/设计接口混在单文件,边界不清晰、OpenAPI 可读性差、参数校验不统一。 -### D2. 铝价模拟数据未显式标注来源 +### D2. 铝价模拟数据未显式标注来源 —— 已清偿(2026-09-18,批次 4 后续专项) -现状: -- 铝价服务返回的是模拟/参考数据,但接口层未明确表达 +修复内容(保留编号以维持引用稳定): +- [src/moldinsight/services/aluminum_price_service.py](../src/moldinsight/services/aluminum_price_service.py):`get_aluminum_current_price` 响应补 `source: "simulated"`,`get_aluminum_price_history` 逐项补同字段 +- [frontend/src/modules/home/HomeView.vue](../frontend/src/modules/home/HomeView.vue):按 `source` 字段渲染"模拟数据 · 参考走势,非实时行情"标注(不再硬编码"上海期货交易所"等虚假来源) +- 死代码 `getAluminumPrice` 删除(前端此前保留了一份本地硬编码函数,已无调用方) -影响: -- 容易误导前端与业务使用者,把模拟数据理解为实时行情 - -建议: -- 响应增加 `source: "simulated"` -- 前端界面同步标注“模拟/参考数据” - -优先级:**P2** +~~原现状 / 影响~~:铝价接口返回走势数据但无来源声明,前端原硬编码"上海期货交易所"字样,与实际模拟数据不一致,属虚假来源声明。 ### D3. shared/platform 边界仍需继续收敛 —— 主体已清偿(2026-09-17 批次 4 + 2026-09-18 后续) -已完成部分: -- 共享 ORM(原最强耦合点)按模块拆分:base / identity(shared)+ moldinsight/models + inventory/models;跨模块只允许裸 FK,单模块部署 mapper 可独立配置(详见 §2.8 与 [ARCHITECTURE.md](ARCHITECTURE.md) §6.1) +已完成部分(修复文件清单见 [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md) 批次 4 / 后续专项): +- 共享 ORM(原最强耦合点)按模块拆分:base / identity(shared)+ moldinsight/models + inventory/models;跨模块只允许裸 FK,单模块部署 mapper 可独立配置(详见 [ARCHITECTURE.md](ARCHITECTURE.md) §6.1) - 旧 `shared/models/database.py` 物理删除,无兼容 facade;归属边界由 [tests/test_model_ownership.py](../tests/test_model_ownership.py) 锁定 - **app_factory 组合职责收敛**(2026-09-18):平台工厂只做纯平台引导,`connect_rustfs` 参数移除;moldinsight 专属接线(RustFS 启动钩子 [init_storage.py](../src/moldinsight/storage/init_storage.py) `rustfs_startup_hook`、路由单点聚合 [moldinsight/api/__init__.py](../src/moldinsight/api/__init__.py) `register_moldinsight_routers`)收敛回模块层,入口退化为纯组装(ARCHITECTURE §6.2) @@ -112,21 +81,14 @@ 优先级:**P3**(仅剩 identity/platform 语义注释口径) -### D4. 文档现状 / 规划 / 历史混放 +### D4. 文档现状 / 规划 / 历史混放 —— 已清偿(2026-09-22) -现状: -- 文档存在部署说明重叠、计划/总结/权威文档混放 -- README 承担过多职责 +修复内容(保留编号以维持引用稳定): +- 已建立 `STATUS / ARCHITECTURE / ROADMAP / TECH_DEBT / DEPLOYMENT` 主骨架,每类信息单一归属;历史材料归档至 `docs/archive/` +- 本文档 §2 由"按批次回顾"精简为"按主题摘要",修复文件清单 / 迁移号 / 回归测试 / 测试基线等详细流水账整体迁入 [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md)(避免与 §3 重复膨胀) +- §3 中对历史批次的引用(如 D3 → §2.8)改为 archive 指针;D2 等已清偿项补齐时间戳 -影响: -- 新成员难以判断“哪篇才是当前有效说法” -- 状态、部署、规划容易发生漂移 - -建议: -- 建立 `STATUS / ARCHITECTURE / ROADMAP / DEPLOYMENT` 主骨架 -- 历史材料迁入 `docs/archive/` - -优先级:**P1** +~~原现状 / 影响~~:TECH_DEBT §2 与 §3 内容重复膨胀,文档目录结构清晰度受新成员评估影响。 ### D5. `/api/status/{task_id}` 未鉴权(安全缺口)—— 已清偿(2026-09-16,批次 0) @@ -222,13 +184,18 @@ - 语义保持:仅切换 Pydantic v2 配置语法 + UTC 时区语义,字段 / OpenAPI / JWT 行为零变化 - 验证:`pytest tests/ -q` **126 passed, 4 skipped**,deprecation warning 全部清零 -### D13. PythonOCC 镜像引入方式脆弱 + 依赖无版本锁(主体已清偿,锁文件遗留) +### D13. PythonOCC 镜像引入方式脆弱 + 依赖无版本锁(镜像引入已清偿;锁文件流程已固化,待首次构建落盘) 现状: - ~~从 conda env 拷贝 site-packages 进 python:3.12-slim~~(2026-09-16 已修正:[Dockerfile.moldinsight](../deploy/Dockerfile.moldinsight) 改为 conda 运行时原生执行,不再跨镜像拷贝;基础镜像 tag 锁定 `continuumio/miniconda3:24.7.1-0`、`python:3.12-slim-bookworm`;tag 可用性随下次镜像构建验证) -- [requirements.txt](../requirements.txt) 全部为 `>=` 下限,无锁文件(**遗留**:首次镜像构建成功后 `pip freeze` 生成锁文件,命令已注释在 Dockerfile 内) +- 锁文件流程已固化(2026-09-22): + - 新增 [deploy/generate_lockfiles.sh](../deploy/generate_lockfiles.sh) / [generate_lockfiles.bat](../deploy/generate_lockfiles.bat):在 moldinsight conda 环境(仅项目依赖,**不能**在混装开发栈跑)执行 `pip freeze --exclude pythonocc-core`,产出 `deploy/requirements-{base,moldinsight}.lock.txt` + - [Dockerfile.moldinsight](../deploy/Dockerfile.moldinsight) 注释改为指向生成脚本 + - [docs/OPERATIONS.md](../docs/OPERATIONS.md) §2.1 增加完整流程说明(生成时机 / 命令 / 产物 / 消费方 / 提交策略) + - [tests/test_lockfile_generation.py](../tests/test_lockfile_generation.py) 加锁文件存在性 + 体积契约;默认 skip(仓库单测不阻塞),CI 镜像构建 job 显式 `pytest --run-lockfile-check` 启用 +- **遗留**:锁文件本身尚未落盘——本机 Miniforge 装的是跨项目开发栈混装环境,污染严重不能直接用 `pip freeze`;须等 CI / 生产机器首次构建 moldinsight 镜像后,按流程跑 `bash deploy/generate_lockfiles.sh` 落锁并提交。已存在护栏:CI 镜像构建 job 跑 `--run-lockfile-check` 后若未落盘会 fail-fast,强制流程走通 -优先级:**P2**(剩余锁文件部分) +优先级:**P2**(流程已固化,剩"首次构建后落盘"一次性产物) ### D14. 配置漂移:弱默认 / 死配置 / 重复解析 —— 已清偿(2026-09-16 ~ 09-17,批次 1 / 3) diff --git a/docs/archive/2026-09_governance_batches.md b/docs/archive/2026-09_governance_batches.md new file mode 100644 index 0000000..f95e251 --- /dev/null +++ b/docs/archive/2026-09_governance_batches.md @@ -0,0 +1,100 @@ +# 2026-09 后端设计治理批次实施流水账(归档) + +> 文档定位:**2026-09 设计审查产出的批次 0–4 与后续专项的完整实施流水账**。 +> 主骨架权威文档见: +> - [../STATUS.md](../STATUS.md)(每批次完成的时间点与产物) +> - [../TECH_DEBT.md](../TECH_DEBT.md)(活跃债务 / 已清偿项的当前位置) +> - [../ROADMAP.md](../ROADMAP.md)(批次计划与执行顺序) +> - [../ARCHITECTURE.md](../ARCHITECTURE.md)(平台 / 模块边界当前定稿) + +## 批次 0(2026-09-16,安全与诚实) + +- `/api/status/{task_id}` 补 JWT 鉴权 + 任务归属校验(无 token 401 / 他人或无主任务 403 / 不存在 404) +- 归属校验收敛为 `TaskQueryService.ensure_task_access` 供 task_router 与 advanced_router 共用(原 D5) +- 上传预检 `pythonocc_available` 从硬编码 true 改为惰性真实探测 +- bcrypt 口令治理:创建侧超 72 字节显式拒绝、验证侧截断比较 +- `SECRET_KEY` 未配置 / `RUSTFS_*` 缺失时惰性校验抛明确错误 +- 完成态任务未持久化 `analysis_metrics` 时 `/api/status` 组装视图 500 修复 +- 测试基线:96 passed +- 回归测试:`tests/test_status_endpoint_auth.py` 8 项 +- 接口行为变化已同步 [../API_CONTRACT.md](../API_CONTRACT.md) §3.2 + +## 批次 1(2026-09-16,部署正确性) + +- 主处理链路改走 RustFS:分派入参 `file_path` → `stp_file_id`(原 D6) +- 处理方按 PG 元数据从 RustFS 下载源文件到任务专属临时目录(保留原始文件名,下游产物命名不变),任务结束即清理 +- RustFS 不可用时回退 `STPFile.file_path` 节点本地路径;compose 为 backend / celery 增加共享卷 `uploads_data` / `html_data` 作过渡兜底 +- `AUTO_MIGRATE` 开关(settings / .env.example / compose 透传),默认 `true` 保持单机开发行为(原 D12) +- 迁移目录 `alembic/` → `migrations/`(修复 `import alembic` 命中本地目录遮蔽真实包的命名冲突)+ Docker 镜像补 `COPY migrations/` + `COPY alembic.ini` +- Dockerfile.moldinsight 改为 conda 运行时原生执行,基础镜像 tag 锁定(原 D13 主体) +- compose 关键项去弱默认:`SECRET_KEY` / `ADMIN_PASSWORD` 改 `${VAR:?}` 强制显式配置;`create_admin_user` 对空口令显式报错 +- 测试基线:98 passed, 1 skipped +- 回归测试:`tests/test_deployment_config.py` + +## 批次 2(2026-09-16,任务一致性模型) + +- Redis 进程内存回退彻底删除(写 no-op / 读 None,Redis 仅热缓存),PG 为任务状态单一事实源(原 D7) +- 批量元数据入库:`processing_tasks` 新增 `batch_id` 列(迁移 `a3f8c2d91e47`),`GET /api/batch/{batch_id}` 改为 PG 聚合查询 + `STPFile.user_id` 归属校验 +- 删除 Redis batch key 与进程内 dict 双通道 +- `TaskQueryService` PG 视图与 batch 聚合响应补 `progress` / `current_step` +- 型腔分模失败不再吞异常 → 任务 failed(原 D8) +- 持久化事务边界收口:数据本体写方法只 flush,编排层分阶段原子收口(阶段 A 几何+网格、阶段 B 型腔+HTML+特征+指标+验证),失败先 rollback 再置 failed(原 D9) +- 进度/状态更新保留即时 commit(长任务进度可见性) +- 测试基线:105 passed, 1 skipped +- 回归测试:`tests/test_batch_status_pg.py` 4 项 + `tests/test_redis_no_fallback.py` 3 项 + +## 批次 3(2026-09-17,API 与代码结构) + +- `advanced_router` 按职责拆为 design / cost / machining / export 四个子路由,端点路径不变(原 D1) +- 全部请求体改 Pydantic 模型(`request.json()` 手动解析退役),校验失败统一 422 +- `_get_cached_import` 上提为 `core_modules.py` 共用 +- 路由装载失败显式化:`ROUTE_MODULES` 清单 + `route_registry`,失败经 `/api/health` 呈现 degraded(`pythonocc` 真实探测),DEBUG 下 fail fast +- 纯 Python 重计算端点统一 `asyncio.to_thread` 投放线程池,不再阻塞事件循环 +- `StorageIntegrationService`(867 行)按职责拆为 TaskStorage / AnalysisStorage / FileHistory 三服务(原 D14 部分) +- `MAX_FILE_SIZE` 接线生效、celery_app 复用 `Settings.redis_url`(原 D14 收尾) +- 连带修复:管理员重置密码改 JSON body(原裸 str 参数被解析为 query param,前端发 body 必 422);Dockerfile.celery 的 FROM tag 与实际构建 tag 对齐 +- 接口变更三件套随批完成:openapi.json 重导出(76 paths)+ 前端 `gen:api` +- 测试基线:122 passed, 2 skipped +- 回归测试:4 个测试文件共 17 项(`test_advanced_split_contract` / `test_route_load_status` / `test_config_governance` / `test_auth_password_reset`) + +## 批次 4(2026-09-17,架构演进) + +- 共享 ORM 按模块拆分(原 D3 主体):891 行 `shared/models/database.py`(31 模型类三类同居)拆为 `shared/models/base.py`(唯一 Base + 归属约定)/ `shared/models/identity.py`(7 表)/ `moldinsight/models/`(9 表)/ `inventory/models/`(catalog/warehouse/trading/finance 15 表) +- 三条跨模块 ORM relationship(`User.stp_files` / `STPFile.user` / `STPFile.product`)经全仓核实均无使用方,直接删除;跨模块桥接收敛为裸 FK 硬规则([../ARCHITECTURE.md](../ARCHITECTURE.md) §5.1) +- 约 45 处 import 全量改写,无兼容 facade +- 全量注册点收敛为 migrations/env.py 与 tests/conftest.py +- 零调用方的死方法 `db_manager.create_tables` 一并删除 +- `_reset_occ_executor` 补 `cancel_futures=True`(旧实现下"慢恢复"的旧线程会继续消化旧队列,与新 executor **并发操作非线程安全的 OCC**,属数据竞争而非单纯泄漏,原 D10 短期治理) +- 吞吐路线定稿于 `topics/performance/OCC_THROUGHPUT.md`(短期 A:celery prefork 伸缩 + max-tasks-per-child 兜底;中期 B:run_occ 接口进程化 + kill-on-timeout 根治) +- 顺手清偿 D15:`vite.config.ts` 删除未用的 `mode` 参数,`vue-tsc -b` 恢复通过 +- 回归测试:`tests/test_model_ownership.py`(31 表全量注册 / 单模块独立 mapper 配置 / 旧模块无 facade) +- 测试基线:125 passed, 2 skipped +- 接口面零变化(无 openapi 重导出) + +## 批次 4 后续专项(2026-09-18) + +- D11 清偿:可视化报告 RustFS 单源化 + - 写侧 HTMLGenerator 每任务写临时目录,`.html`/`_summary.json`/`_data.json` 三件统一裸传 RustFS 报告键 `html/reports/{filename}` + - 读侧 `/html` StaticFiles 本地挂载删除,新增代理路由 `html_report_router.py`(报告键直取 → 遗留 `html/{hash}.json` JSON 包装解析 → 本地卷存量兜底 → 404) + - 防路径穿越(单段文件名校验),URL 形状 `/html/{filename}` 不变 + - 部署:celery 服务摘除 `html_data` 卷;Dockerfile.moldinsight 删除 `COPY html_output/` + - 已知约束:报告路由不做认证(iframe 无法携带 Authorization 头) + - 删除 `get_stp_file_with_data` 的死数据块 +- OCC 方案 A 部署参数落地:`CELERY_CONCURRENCY` / `CELERY_MAX_TASKS_PER_CHILD` 进 Dockerfile.celery + compose + .env.example +- D2 清偿:铝价响应带 `source: "simulated"`;前端按来源渲染"模拟数据 · 参考走势"标注;死代码 `getAluminumPrice` 删除 +- D10 方案 B 实施:`run_occ(fn, *args)` → `run_occ(op_name, payload)`;常驻 OCC 进程池 + kill-on-timeout 根治残留线程泄漏 +- CI 门禁:`.gitea/workflows/ci.yml` 三 job(pytest / 前端构建 / openapi 漂移检测) +- 接口变更三件套随批完成:openapi.json 重导出(76→77 paths,新增 `/html/{filename}`)+ 前端 `gen:api` 再生 +- 测试基线:143 passed, 0 skipped + +## 后续小步治理(2026-09-21 ~ 2026-09-22) + +- 2026-09-21 inventory 业务层继续沉淀批次(4 批): + - `customer / supplier / warehouse` → `master_data_service` + - `material_routes`(价格历史 / 趋势 / 供应商关联)→ `material_service` + - `product_routes`(CRUD / BOM / from-task 跨模块桥接)→ `product_service` + - `dashboard_routes`(首页统计 / 低库存预警)→ `dashboard_service` +- 2026-09-22 schema / datetime 现代化弃用清零(D12): + - 全仓 14 处 `class Config:` + auth_routes 三处全部迁移到 `model_config = ConfigDict(from_attributes=True)` + - `auth_service.py` 中 `datetime.utcnow()` 改用 `datetime.now(timezone.utc)` +- 测试基线最终落点:126 passed, 4 skipped,零 deprecation warning diff --git a/docs/archive/2026-09_status_history.md b/docs/archive/2026-09_status_history.md new file mode 100644 index 0000000..439f9dd --- /dev/null +++ b/docs/archive/2026-09_status_history.md @@ -0,0 +1,10 @@ +# 2026-09 之前 STATUS 早段历史(归档) + +> 文档定位:**[../STATUS.md](../STATUS.md) 顶部精简后,2026-09-17 之前条目的完整副本**。 +> 2026-09-17 及之后的批次(批次 0–4 + 后续专项 + 2026-09-21 inventory 服务下沉 + 2026-09-22 schema/datetime 弃用清零)以摘要形式保留在 [../STATUS.md](../STATUS.md) 顶部,详细流水见 [2026-09_governance_batches.md](2026-09_governance_batches.md)。 + +--- + +> 2026-09-02(**模块化收口 + 文档主骨架建立(基线条目)**:代码侧完成 moldinsight 技术债治理——安全收口(debug/history 权限补齐、任务访问控制收紧)、静默失败修复(`detect-undercuts` 基于真实 shape 重建)、OCC 超时后 executor 重建防毒化全队列、后台任务统一分派、Redis 任务状态改 Hash 原子更新、完成态任务视图缓存、导出缓存与持久化收口、旧入口与死代码删除、Generator 公共接口提取 + 契约测试;详见 [TECH_DEBT.md](../TECH_DEBT.md) §2。结构侧完成 `src/entrypoints/` 三入口拆分(moldinsight / inventory / unified)、`shared` 平台能力集中、前端独立 `frontend/` 工程。文档侧建立 `STATUS / ARCHITECTURE / ROADMAP / TECH_DEBT / DEPLOYMENT` 主骨架,README 收敛为唯一导航入口,历史材料迁入 [archive/](README.md)。**测试基线**:本地 pip 环境 **47 passed, 1 skipped**(pythonocc 缺失自动 skip);moldinsight conda + OCC 环境 **88 passed**。inventory 侧少量既有 deprecation warnings 不影响通过。) + +> 此前:2026-09-01(**文档体系专项整理启动**:明确「README 只做导航、每类信息单一归属、历史材料进 archive」的文档治理原则;建立 deployment/ 主题目录与 archive/ 归档目录;部署文档收口为 DEPLOYMENT(入口)+ deployment/LINUX_SETUP(操作)+ deployment/DEPLOY_PORT / PORT_CONFIG(端口补充)三层。) diff --git a/docs/archive/README.md b/docs/archive/README.md index d1c3eba..d62a7cb 100644 --- a/docs/archive/README.md +++ b/docs/archive/README.md @@ -25,6 +25,8 @@ - [MOLD_ERP_ANALYSIS_REPORT.md](MOLD_ERP_ANALYSIS_REPORT.md) - [ZERO_FINISHED_INVENTORY_CERTIFICATE.md](ZERO_FINISHED_INVENTORY_CERTIFICATE.md) - [CONFLUENCE_ARCHIVE_STRUCTURE.md](CONFLUENCE_ARCHIVE_STRUCTURE.md) +- [2026-09_governance_batches.md](2026-09_governance_batches.md):2026-09 设计审查批次 0–4 + 后续专项的完整实施流水账([../TECH_DEBT.md](../TECH_DEBT.md) 与 [../STATUS.md](../STATUS.md) 仅保留摘要) +- [2026-09_status_history.md](2026-09_status_history.md):[../STATUS.md](../STATUS.md) 顶部精简后,2026-09-17 之前条目的完整副本 - [topics/ai/](topics/ai/):已迁移的 AI 相关专题历史材料 - [topics/performance/](topics/performance/):已迁移的性能专题历史材料 - [topics/aluminum-foam/](topics/aluminum-foam/):已迁移的铝泡沫专题历史材料 diff --git a/tests/conftest.py b/tests/conftest.py index b7bd408..a4a02d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -164,6 +164,28 @@ async def seeded_db(async_engine): yield session +def pytest_addoption(parser): + """D13 部署侧契约:仅在显式 --run-lockfile-check 时启用锁文件存在性断言。""" + parser.addoption( + "--run-lockfile-check", + action="store_true", + default=False, + help="启用 D13 锁文件部署侧契约测试(CI 镜像构建 job 使用)", + ) + + +def pytest_collection_modifyitems(config, items): + """默认跳过 D13 部署侧契约(仓库侧单测不应被尚未落地的锁文件阻断)。""" + if config.getoption("--run-lockfile-check", default=False): + return + skip_marker = pytest.mark.skip( + reason="D13 部署侧契约:默认 skip;CI 镜像构建 job 需传入 --run-lockfile-check 启用" + ) + for item in items: + if "test_lockfile_generation" in item.nodeid: + item.add_marker(skip_marker) + + @pytest.fixture(scope="function") async def client(async_engine, seeded_db): session_factory = async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False) diff --git a/tests/test_lockfile_generation.py b/tests/test_lockfile_generation.py new file mode 100644 index 0000000..e1e8fdc --- /dev/null +++ b/tests/test_lockfile_generation.py @@ -0,0 +1,105 @@ +"""D13:锁文件生成契约(部署侧硬性要求) + +锁文件 deploy/requirements-{base,moldinsight}.lock.txt 的存在性 + 体积下限 +是部署侧硬性要求: + +- 锁文件必须在 moldinsight conda 环境构建成功后落盘(见 deploy/generate_lockfiles.sh/.bat) +- 锁文件必须以 git 跟踪方式提交,CI / 离线构建 / 生产复现部署才能直接 `pip install -r` +- 若 lock.txt 缺失或异常空(仅镜像元数据 < 5 行),说明构建流程未走 D13 流程 + +CI 门禁建议: +- 仓库侧默认 pytest(`pytest tests/ -q`)**不**强制这些断言——锁文件属"部署侧产物", + 首次构建未完成时不应阻塞日常单测 +- 部署侧 / CI 镜像构建 job 用 `--run-lockfile-check` 显式开启本套件(见 conftest.py) +""" +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEPLOY_DIR = REPO_ROOT / "deploy" + +LOCK_FILES = [ + DEPLOY_DIR / "requirements-base.lock.txt", + DEPLOY_DIR / "requirements-moldinsight.lock.txt", +] + +GENERATOR_SCRIPTS = [ + DEPLOY_DIR / "generate_lockfiles.sh", + DEPLOY_DIR / "generate_lockfiles.bat", +] + + +def pytest_collection_modifyitems(config, items): + """仅在显式传入 --run-lockfile-check 时启用 D13 部署侧契约。""" + if not config.getoption("--run-lockfile-check", default=False): + skip_marker = pytest.mark.skip( + reason="D13 部署侧契约:默认 skip;CI 镜像构建 job 需传入 --run-lockfile-check 启用" + ) + for item in items: + if "test_lockfile_generation" in item.nodeid: + item.add_marker(skip_marker) + + +def pytest_addoption(parser): + parser.addoption( + "--run-lockfile-check", + action="store_true", + default=False, + help="启用 D13 锁文件部署侧契约测试(CI 镜像构建 job 使用)", + ) + + +def pytest_collection_modifyitems(config, items): + """仅在显式传入 --run-lockfile-check 时启用 D13 部署侧契约。 + + 说明:本钩子保留作为冗余保护(conftest.py 已注册同名钩子), + 即便测试单独跑 pytest tests/test_lockfile_generation.py 也能正确跳过。 + """ + if not config.getoption("--run-lockfile-check", default=False): + skip_marker = pytest.mark.skip( + reason="D13 部署侧契约:默认 skip;CI 镜像构建 job 需传入 --run-lockfile-check 启用" + ) + for item in items: + item.add_marker(skip_marker) + + +def pytest_addoption(parser): + parser.addoption( + "--run-lockfile-check", + action="store_true", + default=False, + help="启用 D13 锁文件部署侧契约测试(CI 镜像构建 job 使用)", + ) + + +@pytest.mark.parametrize("lock_path", LOCK_FILES) +def test_lockfile_exists_and_is_substantive(lock_path): + """锁文件必须存在且非空(≥5 行 pip freeze 产物),否则部署侧契约缺失。""" + assert lock_path.exists(), ( + f"缺少锁文件 {lock_path.relative_to(REPO_ROOT)};" + f"请在 moldinsight conda 环境执行 deploy/generate_lockfiles.sh/.bat 后提交" + ) + line_count = sum(1 for _ in lock_path.open(encoding="utf-8") if _.strip()) + assert line_count >= 5, ( + f"锁文件 {lock_path.relative_to(REPO_ROOT)} 体积异常(仅 {line_count} 行非空行)," + "可能是构建流程未走通,请重新生成" + ) + + +@pytest.mark.parametrize("script_path", GENERATOR_SCRIPTS) +def test_lockfile_generator_script_present(script_path): + """锁文件生成脚本必须随仓库分发,否则新机器无法落锁。""" + assert script_path.exists(), ( + f"缺少生成脚本 {script_path.relative_to(REPO_ROOT)};" + "D13 流程入口文件缺失" + ) + + +def test_lockfile_dockerfile_comment_points_to_generator(): + """Dockerfile.moldinsight 必须明确指向锁文件生成脚本。""" + dockerfile = (DEPLOY_DIR / "Dockerfile.moldinsight").read_text(encoding="utf-8") + assert "generate_lockfiles" in dockerfile, ( + "Dockerfile.moldinsight 应在注释中指向 deploy/generate_lockfiles.sh 以引导锁文件生成流程" + )