Files

23 KiB
Raw Permalink Blame History

测试报告(项目自带测试与测试数据集)

测试日期: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 用户决策

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 失败 6768 通过 / 12 失败 / 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 处理建议

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 方案落地

用户授权后创建了独立测试库,并把引导流程固化成工具:

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 干扰) 集成 6869 通过 / 01 失败

ruff check app tests alembic tools 全绿。第 8.4 节列出的"偶发失败"至此全部收敛为环境干扰, 并已被独立测试库方案消除。