Files

374 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试报告(项目自带测试与测试数据集)
> 测试日期:2026-09-11
> 测试对象:`D:\nanfangjijin\group_fqcd_jr`(分支 `yc`,工作区含未提交改动)
> 测试范围:项目内自带的测试与测试数据集(`tests/`、`tools/seed_*.py`、`data/`)
> 测试依据:用户当次指令(用户明确不按 `docs/项目测试/测试架构.md` 执行验收测试)
> 执行环境:Windows + Python 3.13.5 + pytest 8.3.4 + pytest-asyncio 0.26.0,真实本地 MySQL(`jr` 库)
---
## 1. 测试概况
| 层次 | 通过 | 失败 | 跳过 | 说明 |
|---|---|---|---|---|
| 单元测试 `tests/unit` | 466 | 7 | 0 | 另有 1 个用例文件收集失败 |
| 契约测试 `tests/contract` | 4 | 0 | 0 | 全部通过 |
| 集成测试 `tests/integration` | 39 | 30 | 1 | 跳过项为 Redis 真实计数器用例 |
| **合计** | **509** | **37** | **1** | 通过率 509/546 = **93.2%** |
补充事实:
1. `tests/unit/service/test_suitability_service.py` **收集失败**,`pytest` 在收集阶段中断;忽略该文件后才得到上表数据。
2. 失败结果**顺序敏感**:`tests/integration` 全量执行失败 30 条,逐条单独执行时其中 13 条可通过(如 `test_complete_run`、`test_identity_mysql`、`test_memory_extraction`、`test_db_timezone_utc` 第二条),说明失败集合与执行顺序相关。
3. 集成测试**真实读写本地 MySQL**,未使用 Mock 数据库;测试数据会写入并清理业务表。
4. 前端目录 `group_fqcd_jr/前端` 为空,无前端用例可执行。
---
## 2. 缺陷清单
### 2.1 致命:配置发布「同一人自审」路径必然失败
| 项 | 内容 |
|---|---|
| 用例 | `tests/integration/test_config_release_mysql.py::test_config_release_lifecycle_is_versioned_and_audited` |
| 复现 | 直接执行该用例 |
| 实际 | `sqlalchemy.exc.OperationalError (3819) Check constraint 'chk_config_release_separation' is violated.` |
| 期望 | 抛出业务异常 `ConfigReleaseError`,并能完成提交/审核/激活/回滚全流程 |
| 根因(事实) | ① `app/service/config_release_service.py:31-36` 注释声明该约束"已于迁移 `20260910_drop_review_separation` 撤下",因此 `approve()` 允许创建人自审;② `alembic/versions/` 目录**不存在**任何 `*separation*` 迁移文件;③ 对 `information_schema.CHECK_CONSTRAINTS` 查询确认线上库**仍存在** `chk_config_release_separation` |
| 影响 | 数据库保持双人复核约束,而代码按"自审合法"执行,二者冲突;单管理员部署下配置发布必定 500,配置中心不可用 |
### 2.2 严重:集成测试基座跨事件循环,约 28 条用例信号失真
| 项 | 内容 |
|---|---|
| 用例 | `tests/integration` 中 28 条报错用例(含 `test_offsite_fund_api` 6 条、`test_offsite_notification_send` 3 条、`test_worker_runtime_mysql`、`test_handover_outbox_mysql` 等) |
| 现象 | `RuntimeError: ... got Future <Future pending> attached to a different loop` |
| 根因(事实) | `app/infrastructure/db.py` 在模块导入时创建全局 `engine` + `SessionFactory`;测试同时存在多个事件循环(pytest-asyncio 每个用例一个循环、每个 `TestClient` 一个 portal 线程循环、用例内 `asyncio.run(...)` 再建一个循环)。连接池把上一个循环建立的 MySQL 连接交给新循环使用 |
| 影响 | 用例结果取决于执行顺序:全量跑失败、单条跑通过。测试无法反映真实质量,回归信号不可信 |
| 建议方向 | 用例间统一事件循环作用域;或在 `conftest.py` 中按循环 dispose 引擎;或改用 `httpx.ASGITransport` 异步客户端替代 `TestClient`,避免 portal 线程循环 |
### 2.3 一般:单元测试读取真实 `.env`,5 条用例被本机配置污染
| 项 | 内容 |
|---|---|
| 用例 | `test_offsite_document_recognition_adapter.py` 4 条、`test_offsite_mail_worker.py::test_worker_rejects_real_imap_processing_with_mock_recognition` |
| 现象 | ① 用例名为「默认走 Mock 不调外部」却真实发起阿里云 DocMind 调用,报 `UnretryableException ... WinError 10013`;② `health_check` 期望 `disabled` 实得 `ok`;③ Worker 用例返回 `True`,期望 `False` |
| 根因(事实) | `app/core/config.py:100` 配置 `SettingsConfigDict(env_file=".env")`;用例辅助函数 `_settings(**updates)` 只传入少数字段后构造 `Settings(**values)`,其余字段仍从项目 `.env` 读取。本机 `.env` 中 `OFFSITE_OCR_ENABLED=true`、`OFFSITE_DEEPSEEK_ENABLED=true`,且已填真实密钥 |
| 影响 | 单元测试结果依赖开发者本机 `.env`,换机器结论不同;同时存在真实外部调用风险 |
| 建议方向 | 测试构造 `Settings(_env_file=None, ...)`,显式关闭环境文件读取 |
### 2.4 一般:接口用例与响应契约不同步,3 条用例失败
| 项 | 内容 |
|---|---|
| 用例 | `test_agent_runs_api.py::test_create_agent_run_returns_202_and_addresses`、`test_worker_runtime_mysql.py` 2 条 |
| 现象 | HTTP 状态 202 正确,但 `body["run_id"]` 抛 `KeyError` |
| 根因(事实) | 实测同一路径返回信封体 `{"data": {"run_id": ...}, "meta": {...}}`(与 `docs/05-接口文档.md` §3.3 一致),用例仍按旧的平铺结构读取 |
| 影响 | 用例未随接口契约更新,掩盖真实回归 |
### 2.5 一般:死模块未清理,1 条回归用例失败
| 项 | 内容 |
|---|---|
| 用例 | `tests/unit/api/test_config_release_authority.py::test_legacy_config_release_controller_module_is_gone` |
| 现象 | `assert ModuleSpec(... config_releases ...) is None` 失败 |
| 根因(事实) | `app/api/controllers/config_releases.py` 仍存在于磁盘;该文件未被 `app/main.py` 注册(第 53-61 行只注册 `admin_router` 等 9 个路由),属于第二套状态机入口 |
| 影响 | 用例意图是消除"该用哪套接口"的歧义,清理动作未完成 |
### 2.6 一般:用例文件与 Service 重构不同步,整文件收集失败
| 项 | 内容 |
|---|---|
| 用例 | `tests/unit/service/test_suitability_service.py` |
| 现象 | `ImportError: cannot import name 'SuitabilityRequest' from 'app.service.suitability_service'`,pytest 收集中断 |
| 根因(事实) | `app/service/suitability_service.py` 现有对外类型为 `RiskAuthorityProfile` / `SuitabilityToolInput` / `SuitabilityDecision`,已无 `SuitabilityRequest` |
| 影响 | 该文件的全部用例无法执行,并使整次收集带错误码 |
### 2.7 一般:推介材料接口状态码与错误码不符合用例约定,2 条用例失败
| 项 | 内容 |
|---|---|
| 用例 | `test_promotion_material_api.py::test_promotion_material_http_rejects_missing_key_and_permission` |
| 现象 | 缺少 `Idempotency-Key` 时实际返回 `422`,期望 `400` + `error.code=VALIDATION_ERROR` |
| 用例 | `test_promotion_material_api.py::test_promotion_material_http_workflow_and_advisor_scope` |
| 现象 | 跨投顾读取时实际返回 `SESSION_NOT_FOUND`,期望 `RESOURCE_NOT_FOUND` |
| 影响 | 至少一方与文档不符:要么接口应做幂等键显式校验并返回统一信封,要么用例需按现状更新 |
### 2.8 轻微:环境缺依赖,Redis 相关用例跳过且限流降级
| 项 | 内容 |
|---|---|
| 现象 | `tests/integration/test_rate_limit_redis.py` 跳过;接口日志出现 `ModuleNotFoundError: No module named 'redis'` |
| 根因(事实) | `requirements.txt` 声明 `redis`,但当前解释器未安装该包;`app/infrastructure/rate_limiter.py:58` 懒加载失败后按设计降级放行(fail-open),不影响主流程 |
| 影响 | 限流真实链路未被验证,限流用例信号缺失 |
---
## 3. 未覆盖风险
| 项 | 原因 | 风险等级 |
|---|---|---|
| 验收测试(`tools/acceptance_check.py`、越权/防重/参数校验矩阵) | 用户指定仅执行项目自带测试数据 | 中 |
| `docs/项目要求/` 需求与知识文件 | 该目录不存在,无需求/知识文件可提炼检索问答用例 | 中 |
| Redis 限流真实链路 | 本机未安装 `redis` 包 | 中 |
| Milvus / Neo4j 真实链路 | 本次未启动也未验证连通性 | 中 |
| 性能压测(`tools/performance_baseline.py`) | 本次未执行 | 低 |
| 前端用例 | `前端` 目录为空 | 低 |
---
## 4. 验收结论(对照用户指定范围)
| 检查项 | 结论 |
|---|---|
| 项目自带单元测试全通过 | 不通过(7 失败 + 1 文件收集失败) |
| 项目自带契约测试全通过 | 通过(4/4) |
| 项目自带集成测试全通过 | 不通过(30 失败,39 通过,1 跳过) |
| 回归通过率 100% | 不通过(37/546 失败) |
**总体结论:不通过。** 其中 1 条为真实业务缺陷(配置发布自审路径必然失败),28 条为测试基座跨事件循环导致的失真,5 条由本机 `.env` 污染引起,其余为用例与代码不同步及环境缺依赖。
---
## 5. 建议修复顺序
1. **P0** 配置发布:确认自审是否为既定业务规则。若是,补 `20260910_drop_review_separation` 迁移并撤下数据库约束;若不是,在 `ConfigReleaseService.approve()` 恢复"创建人不得自审"的业务校验。
2. **P1** 测试基座:统一事件循环或在用例间 dispose 异步引擎,恢复 28 条集成用例的有效信号。
3. **P1** 测试隔离:单元测试构造 `Settings(_env_file=None, ...)`,禁止读取本机 `.env` 与真实密钥。
4. **P2** 同步过期用例:`test_agent_runs_api`(响应信封)、`test_suitability_service`(类型改名)、`test_config_release_authority`(删除死模块)。
5. **P2** 对齐推介材料接口的状态码与错误码,并同步文档。
6. **P3** 安装 `redis` 依赖并启用限流真实链路用例。
---
## 6. 修复记录:配置发布自审(2026-09-11)
业务口径由用户确认:**允许创建人自审**。
### 6.1 现状复核(事实)
| 证据 | 内容 |
|---|---|
| `tests/unit/service/test_config_release_self_review.py` | 明确要求「自审允许,并如实写入 `reviewer_id = 创建人`」,并注明约束已由迁移撤下 |
| `app/service/config_release_service.py` | 原注释同样声明约束"已于迁移 `20260910_drop_review_separation` 撤下" |
| `alembic/versions/` | **不存在**该迁移文件,`information_schema` 也证明约束仍在线上库 |
| `app/service/admin_service.py` | 激活分支要求 `row["reviewer_id"] is not None`,否则报「版本未审核」 |
结论:撤下约束是唯一自洽方案——若改为"自审留空 `reviewer_id`",激活分支会把已审核版本判为未审核,自审版本无法激活。
### 6.2 变更内容
| 文件 | 变更 |
|---|---|
| `alembic/versions/20260911_drop_review_separation.py` | 新增迁移:撤下 `config_release` 的 `chk_config_release_separation`;`downgrade` 可重建 |
| `app/service/config_release_service.py` | `approve()` 自审与多人复核一致如实写入 `reviewer_id`;注释改为指向真实迁移 |
| `app/service/admin_service.py` | 驳回分支自审同样写入 `reviewer_id`,与通过分支一致;注释同步 |
| `tests/integration/test_config_release_mysql.py` | 对应用例编码新规则:自审通过且 `reviewer_id == 创建人`;另补他人复核分支断言 |
| `tests/unit/service/test_config_release_self_review.py` | 注释中的迁移名更正为本仓库真实文件名 |
| `docs/02-数据库建表设计.md` | 移除该约束并加变更记录,说明表名与字段定义未改动 |
### 6.3 数据库变更验证
| 项 | 结果 |
|---|---|
| 升级 `alembic upgrade head` | 通过,head = `20260911_drop_review_separation` |
| 升级后约束 | 仅剩 `chk_config_release_status`,`chk_config_release_separation` 已撤下 |
| 回滚 `alembic downgrade -1` | 通过(库中自审行数为 0,约束可重建) |
| 字段/索引/外键指纹(降级后 vs 升级后) | 68 张表全部一致,差异表为空 |
| 升级前 vs 升级后指纹 | 一致,证明只撤约束、未改任何字段 |
### 6.4 复测结果
| 用例 | 结果 |
|---|---|
| `test_config_release_mysql.py`(单条执行) | 通过 |
| `test_config_release_self_review.py`(4 条) | 通过 |
| `test_admin_service.py`(12 条) | 通过 |
| `ruff check app tests alembic` | 我改动的文件全部通过(另存在 1 处与本次无关的 `I001`:`tests/unit/service/test_offsite_fund_rules.py`) |
注意:全量执行集成测试时该用例仍会失败——原因仍是第 2.2 节的跨事件循环问题,单条执行即通过。
---
## 7. 修复记录:测试基座与环境隔离(2026-09-11)
### 7.1 跨事件循环(报告 2.2 节)
| 项 | 内容 |
|---|---|
| 变更 | `tests/conftest.py` 在导入任何 app 模块前,把测试期引擎换成 `NullPool`,等价地替换 `SessionFactory` |
| 原因 | 应用引擎在导入时创建,连接池里的连接绑定在建立它的事件循环上;测试同时存在 pytest-asyncio 循环、`TestClient` portal 循环和 `asyncio.run` 循环,连接被跨循环复用即报 `got Future attached to a different loop` |
| 生产影响 | 无。`app/infrastructure/db.py` 未改动,生产仍用默认连接池 |
| 效果 | 集成测试失败数 30 → 4(详见 7.3) |
补充:`tools/seed_test_rbac.py` 需要按 `python -m tools.seed_test_rbac` 运行(直接以脚本路径执行会
因 `sys.path` 缺项目根而报 `No module named 'app'`)。执行种子后,依赖 9001/9002/9003 号段账号的
用例(转人工、运行取消、complete_run)恢复通过。
### 7.2 单元测试读取真实 `.env`(报告 2.3 节)
| 项 | 内容 |
|---|---|
| 变更 | `tests/unit/service/test_offsite_document_recognition_adapter.py`、`tests/unit/worker/test_offsite_mail_worker.py` 的 `_settings()` 显式给定外部识别开关与占位凭据 |
| 原因 | `app/core/config.py` 导入时执行 `load_dotenv`,把本机 `.env` 灌进 `os.environ`;仅用 `_env_file=None` 挡不住环境变量,构造参数优先级最高才是可靠隔离 |
| 效果 | 该 5 条用例全部通过,且不再可能触发真实外部调用 |
### 7.3 与文档不同步的用例
| 文件 | 变更 | 依据 |
|---|---|---|
| `tests/integration/test_agent_runs_api.py` | 响应体改读 `data` 信封 | `docs/05-接口文档.md` §3.3 与 §19「成功响应均使用第 3.3 节信封」 |
| `tests/integration/test_worker_runtime_mysql.py` | 同上 | 同上 |
### 7.4 复测结果对比
| 层次 | 修复前 | 修复后 |
|---|---|---|
| 单元测试 | 466 通过 / 7 失败 | **472 通过 / 1 失败** |
| 契约测试 | 4 通过 | 4 通过 |
| 集成测试 | 39 通过 / 30 失败 / 1 跳过 | **65 通过 / 4 失败 / 1 跳过** |
| 合计 | 509 通过 / 37 失败 | **541 通过 / 5 失败** |
| 另有收集失败文件 | 1 | 1(未变) |
### 7.5 剩余失败与待决策项
| # | 用例 | 性质 | 说明 |
|---|---|---|---|
| 1 | `test_config_release_authority.py::test_legacy_config_release_controller_module_is_gone` | 待确认 | 用例要求删除死模块 `app/api/controllers/config_releases.py`(未被 `main.py` 注册)。删除文件属需确认动作 |
| 2 | `test_suitability_service.py` 整文件收集失败 | 待同步 | 用例引用已改名类型 `SuitabilityRequest`(现为 `SuitabilityToolInput`),需同步用例 |
| 3 | `test_promotion_material_api.py` 2 条 | 待决策 | ①缺少幂等键返回 FastAPI 默认 422,未走统一错误信封(`app/main.py` 只注册了 `AgentError` 处理器);②跨投顾读取返回 `SESSION_NOT_FOUND`(`docs/05` §3.6 口径),用例期望 `RESOURCE_NOT_FOUND`(`docs/09` 口径)——两份文档错误码表冲突,需定权威口径 |
| 4 | `test_run_cancellation_mysql.py::test_cancel_is_idempotent_and_terminates_original_request` | 偶发 | 重复取消时第二次返回 `cancelled` 而非同一 `cancel_requested`;3 次执行中 1 次失败 |
| 5 | `test_worker_runtime_mysql.py::test_http_accept_worker_commit_query_and_repeat[False]` | 稳定复现 | 并发执行同一 run 后,`memory.extraction_requested` 事件为 0(期望 1),疑似并发完成路径的幂等/事件写入问题 |
---
## 8. 收尾修复:死模块、错误码口径与校验信封(2026-09-11)
### 8.1 用户决策
```text
1. 删除死模块:确认
2. 错误码冲突:以 SESSION_NOT_FOUND 为准(即 docs/05-接口文档.md §3.6 为权威)
3. 参数校验统一信封:确认
```
### 8.2 变更内容
| 文件 | 变更 |
|---|---|
| `app/api/controllers/config_releases.py` | 删除(第二套配置发布状态机入口,从未被 `main.py` 注册) |
| `app/service/config_application_service.py` | 删除(仅被上述死 Controller 引用,随之失去调用方) |
| `app/main.py` | 新增 `RequestValidationError` 处理器,请求校验失败也返回统一 `{error, meta}` 信封(422 + `AGENT_INPUT_INVALID`,字段级原因进 `field_errors`);提取 `_trace_id()` 供两个处理器共用 |
| `tests/unit/api/test_request_validation_envelope.py` | 新增 2 条契约用例:校验错误信封形态、鉴权仍先于参数校验(401 不被 422 掩盖) |
| `tests/integration/test_promotion_material_api.py` | 错误码对齐权威文档:`RESOURCE_NOT_FOUND`→`SESSION_NOT_FOUND`、`VALIDATION_ERROR`/400→`AGENT_INPUT_INVALID`/422、`FORBIDDEN`→`AGENT_PERMISSION_DENIED` |
| `docs/09-底座使用文档.md` | 错误码表按 `docs/05` §3.5/§3.6 对齐;发布流程说明由"创建人不能审核自己的配置"改为"允许创建人自审" |
### 8.3 复测结果
| 层次 | 最初 | 第 7 节后 | 本节后 |
|---|---|---|---|
| 单元测试 | 466 通过 / 7 失败 | 472 通过 / 1 失败 | **475 通过 / 0 失败** |
| 契约测试 | 4 通过 | 4 通过 | 4 通过 |
| 集成测试 | 39 通过 / 30 失败 | 65 通过 / 4 失败 | **67~68 通过 / 1~2 失败 / 1 跳过**(连跑 3 次:2 / 2 / 1 失败) |
| 合计 | 509 通过 / 37 失败 | 541 通过 / 5 失败 | **约 546 通过 / 1~2 失败** |
### 8.4 剩余问题
| # | 问题 | 性质 | 证据 |
|---|---|---|---|
| 1 | `tests/unit/service/test_suitability_service.py` 整文件收集失败 | 用例与重构后语义不同,需重写 | 旧用例按"调用方传入风险等级、同步 `evaluate(request)`"编写;现实现为异步 `evaluate(request, context)`,风险等级由服务端权威画像解析。仅改导入名无法恢复 |
| 2 | `test_worker_runtime_mysql.py::test_http_accept_worker_commit_query_and_repeat[False]` | 疑似真 bug,稳定复现 | 并发 `execute` 同一 run 后,`memory.extraction_requested` 事件数为 0(期望 1) |
| 3 | `test_complete_run`、`test_memory_extraction`、`test_run_cancellation` 中的个别用例 | 偶发 | 连跑 3 次失败集合变化,单条执行均通过;疑似测试间残留数据或并发时序 |
---
## 9. 偶发失败定位:遗留 Worker 进程与测试共用数据库(2026-09-11)
### 9.1 结论
第 8.4 节第 3 项的"偶发"不是测试间污染,而是**本机存在遗留的 worker 进程**,
它与集成测试共用同一个数据库,会主动领走测试造的运行、消费测试写入的 Outbox 事件。
### 9.2 证据
| 证据 | 内容 |
|---|---|
| 探针实验 | 造一个 `queued` 运行后静置观察:**1 秒内**被外部进程写入 `worker_id` 并推进为 `failed`(该进程未注册业务 Agent)。进程内没有任何 worker 任务 |
| 失败现场 | `test_cancel_is_idempotent_and_terminates_original_request` 两次取消返回的 `cancel_requested_at` 完全相同、只有 `status` 从 `cancel_requested` 变成 `cancelled` |
| 代码路径 | 只有 `WorkerRuntime.execute()`(`app/worker/runtime.py:356-366`)会把 `cancel_requested` 推进为 `cancelled`,且保留 `cancel_requested_at`——与现场完全吻合 |
| 扫描逻辑 | `app/worker/runtime.py:299` 会认领任意 `queued/running/cancel_requested` 的运行,测试造的运行对它是可见的 |
### 9.3 进程清单(2026-09-11 实测)
| PID | 启动时间 | 判定依据 |
|---|---|---|
| 2304 | 15:31:30 | 连着 `111.124.203.45:993`(IMAP SSL)→ `python -m app.worker` |
| 15700 | 未知 | 同样连着 `111.124.203.45:993` → 另一个 worker 实例 |
| 6188 | 15:46:10 | 监听 `:8100` → API 服务(uvicorn) |
| 19876 | 13:03:23 | 监听 `:5173` → 前端 dev server |
### 9.4 处理建议
```text
1. 跑集成测试前停掉 worker 进程(`python -m app.worker`),否则测试数据会被并发消费;
2. 长期方案:集成测试指向独立测试库(MYSQL_DSN 覆盖),与开发用的 worker 物理隔离。
```
停止 worker 后重跑集成测试可验证该结论(本报告未擅自终止开发者的进程)。
### 9.5 处置结果(2026-09-11)
| 项 | 结果 |
|---|---|
| PID 2304(worker) | 已停止 |
| PID 15700(worker) | **无法停止**:属于 Session 0,`Stop-Process` 与 `taskkill /F` 均返回"拒绝访问",需要由拥有该进程的账户或管理员结束 |
| 进程清单复核 | 停止 2304 后,`netstat` 确认仅剩 15700 持有数据库(2 条)与 IMAP(1 条)连接,扫库行为仍在(探针 1 秒内被领走) |
| 测试残渣清理 | 清理 14 个测试 session(`agent_run` / `conversation_message` / `request_idempotency` 各自归零),其中 5 行为 09-09 历史遗留 |
| 复跑结果(2304 停止后) | 两次集成:`1 failed, 68 passed` 与 `69 passed, 1 skipped`;唯一失败仍是取消用例,与 15700 的扫描时序一致 |
结论:只要 15700 仍在轮询,集成测试就无法做到稳定全绿;彻底解法是结束该进程,
或让集成测试指向独立测试库。
---
## 10. 独立测试库:最终验证(2026-09-11)
### 10.1 方案落地
用户授权后创建了独立测试库,并把引导流程固化成工具:
```text
CREATE DATABASE IF NOT EXISTS jr_agent_test
CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
GRANT ALL PRIVILEGES ON jr_agent_test.* TO 'jr_app'@'localhost';
```
| 项 | 值 |
|---|---|
| 工具 | `tools/run_tests_on_test_db.py`(`python -m tools.run_tests_on_test_db [pytest 目标…]`) |
| 覆盖方式 | 只在本进程内把 `MYSQL_DSN` 换成测试库;`.env` 不改,开发库与 worker 不受影响 |
| 执行步骤 | 推导测试库 DSN → 空库导入历史基线 → `alembic upgrade head` → 灌 9001/9002/9003 测试账号 → pytest |
| 测试库现状 | 69 张表(含 `alembic_version`),版本 `20260911_drop_review_separation`(head),RBAC 测试账号 3 个 |
### 10.2 过程中踩到的两个坑(均已有可复现证据)
| 坑 | 现象 | 结论 |
|---|---|---|
| 迁移链不是从零建库 | `alembic upgrade head` 在建 `svc_conversation_session` 时报 `Failed to open the referenced table 'sys_user'` | 33 张历史基线表在 `alembic/baseline_generated.sql` 里,不在迁移链中,必须先导入 |
| pymysql 多语句执行不生效 | 用 `CLIENT.MULTI_STATEMENTS` 一次发整脚本,连接正常返回但表没建出来 | 改为逐条执行后 41 条语句全部成功 |
(另记:脚本最初写成 `.ps1`,Windows PowerShell 5.1 把无 BOM 的 UTF-8 当 GBK 解析导致中文乱码报错,
已改为 Python 工具,避开该编码陷阱。)
### 10.3 最终结果
| 环境 | 结果 |
|---|---|
| **独立测试库 `jr_agent_test`** | **548 通过 / 1 跳过 / 0 失败**(单元 475 + 契约 4 + 集成 69) |
| 开发库 `jr_agent`(仍受遗留 worker 干扰) | 集成 68~69 通过 / 0~1 失败 |
`ruff check app tests alembic tools` 全绿。第 8.4 节列出的"偶发失败"至此全部收敛为环境干扰,
并已被独立测试库方案消除。