fix: 修复全量审查确认的 3 Critical + 5 Required,并修复 13 个腐化用例 #8

Merged
shangfangjian merged 7 commits from fix-review-report-critical into main 2026-09-21 18:04:54 +08:00
Collaborator

依据 Profeto-代码审查报告.md 完成修复。全程 TDD(先测后改、红绿循环),并对关键守卫做了变异测试验证其确实能捕获回归。

Critical(3)

C1 定时任务永不触发 —— 实际 4 层缺陷,报告只列了 2 层

  • 最深一层:scheduler.py:42 写的是 croniter(self.cron, datetime.now) —— 漏了调用括号,传的是方法对象本身,croniterTypeError 被吞 → next_run 恒为 None。即使 3 处调用点全传对 cron,任务依然永不触发
  • _calc_next 非法 cron 静默吞异常 → 改为构造期抛 ValueError
  • _run_loop 单任务异常会终止整个调度循环 → 增加异常隔离
  • sleep(min(wait,60)) 使运行期改 cron 最长 60s 才生效 → 改固定 SLEEP_TICK 轮询
  • 运行期(已 start)新注册任务从不 create_task → 新增 _ensure_loop
  • 3 处 register() 第 2 参误传 task 类型名 → 改传 cron

C2 bzzoiro events 采集管线整体缺失

  • 重构提交 6a49940 同时删掉 fetch_bzzoiro_eventsclass BzzoiroSource 却未替代;sources.pyexcept Exception: pass 吞掉 ImportError → _SOURCES 恒空 → get_source("bzzoiro") 恒抛错
  • 已从 f05dc1a 恢复两符号;静默吞异常改为记日志

C3 EvalPage.tsx 引用未导入的 EmptyState → 改 EmptyText;tsc --noEmit 由 1 error 归零

Required(5)

# 修复
R1 _km 未定义 → 复用 key_ring._mask
R2 ingest_bzzoiro_standings 是截断函数(零 upsert)→ 从 f05dc1a 移植完整 upsert
R3 orchestrator 日志 %d 传了 list → 改 len(ok_reports)
R4 mode="multi" 丢弃 model → 透传
R5 backtest 字符串日期直比 timestamptz → 新增 _parse_date_bound

报告的两处误判(已核实)

  • R6 不成立:迁移 0009 只加 3 个 nullable 列,无 0-0 伪造比分逻辑
  • R7 不成立:限流已完整实现(IP 防伪造 + 10/min + 429),backtest 有 Semaphore(8)

收尾复核新增发现

  • R4 首版实现引入并发串味:用模块级变量中转 model override,而 backtest asyncio.gather 并发 8 场预测会互相覆盖 → A 场用上 B 场的模型。已改为形参显式下传
  • 测试基线真相:报告假设的「全绿」不成立,实测 14 failed / 190 passed。13 个残留全部是测试腐化 + 跨测试污染,无源码缺陷

验证

  • 后端 pytest:240 passed, 1 skipped, 0 failed(基线 14 failed)
  • 前端 tsc --noEmit:0 error;vite build 成功
  • 套件顺序无关性已验证;关键守卫经变异测试确认有守护力

遗留

  • O1-O7 架构问题(orchestrator.py 等超大文件)未处理
  • CI 防线仍未建立 —— 多个「静默失效」缺陷能长期潜伏,正因无 CI。建议补 pytest + tsc + build 流水线
依据 `Profeto-代码审查报告.md` 完成修复。全程 TDD(先测后改、红绿循环),并对关键守卫做了**变异测试**验证其确实能捕获回归。 ## Critical(3) **C1 定时任务永不触发 —— 实际 4 层缺陷,报告只列了 2 层** - 最深一层:`scheduler.py:42` 写的是 `croniter(self.cron, datetime.now)` —— 漏了调用括号,传的是方法对象本身,`croniter` 抛 `TypeError` 被吞 → `next_run` 恒为 None。**即使 3 处调用点全传对 cron,任务依然永不触发** - `_calc_next` 非法 cron 静默吞异常 → 改为构造期抛 `ValueError` - `_run_loop` 单任务异常会终止整个调度循环 → 增加异常隔离 - `sleep(min(wait,60))` 使运行期改 cron 最长 60s 才生效 → 改固定 `SLEEP_TICK` 轮询 - 运行期(已 start)新注册任务从不 `create_task` → 新增 `_ensure_loop` - 3 处 `register()` 第 2 参误传 task 类型名 → 改传 `cron` **C2 bzzoiro events 采集管线整体缺失** - 重构提交 `6a49940` 同时删掉 `fetch_bzzoiro_events` 与 `class BzzoiroSource` 却未替代;`sources.py` 的 `except Exception: pass` 吞掉 ImportError → `_SOURCES` 恒空 → `get_source("bzzoiro")` 恒抛错 - 已从 `f05dc1a` 恢复两符号;静默吞异常改为记日志 **C3 EvalPage.tsx** 引用未导入的 `EmptyState` → 改 `EmptyText`;`tsc --noEmit` 由 1 error 归零 ## Required(5) | # | 修复 | |---|---| | R1 | `_km` 未定义 → 复用 `key_ring._mask` | | R2 | `ingest_bzzoiro_standings` 是截断函数(零 upsert)→ 从 `f05dc1a` 移植完整 upsert | | R3 | orchestrator 日志 `%d` 传了 list → 改 `len(ok_reports)` | | R4 | `mode="multi"` 丢弃 `model` → 透传 | | R5 | backtest 字符串日期直比 `timestamptz` → 新增 `_parse_date_bound` | ## 报告的两处误判(已核实) - **R6 不成立**:迁移 0009 只加 3 个 nullable 列,无 0-0 伪造比分逻辑 - **R7 不成立**:限流已完整实现(IP 防伪造 + 10/min + 429),backtest 有 Semaphore(8) ## 收尾复核新增发现 - **R4 首版实现引入并发串味**:用模块级变量中转 model override,而 backtest `asyncio.gather` 并发 8 场预测会互相覆盖 → A 场用上 B 场的模型。已改为形参显式下传 - 测试基线真相:报告假设的「全绿」不成立,实测 **14 failed / 190 passed**。13 个残留全部是测试腐化 + 跨测试污染,无源码缺陷 ## 验证 - 后端 pytest:**240 passed, 1 skipped, 0 failed**(基线 14 failed) - 前端 `tsc --noEmit`:**0 error**;**vite build 成功** - 套件顺序无关性已验证;关键守卫经变异测试确认有守护力 ## 遗留 - O1-O7 架构问题(`orchestrator.py` 等超大文件)未处理 - **CI 防线仍未建立** —— 多个「静默失效」缺陷能长期潜伏,正因无 CI。建议补 pytest + tsc + build 流水线
WorkBuddy added 7 commits 2026-09-21 17:56:43 +08:00
C1 定时任务静默失效(4 层缺陷):
  - scheduler.py: croniter(expr, datetime.now) 未调用 now(),
    导致 TypeError 被吞、next_run 恒为 None(最深一层)
  - scheduler.py: _calc_next 非法 cron 静默吞异常 -> 改为构造期抛 ValueError
  - scheduler.py: _run_loop 一次异常即永久停摆 -> 增加异常隔离
  - scheduler.py: sleep(min(wait,60)) 使运行期 cron 变更最长 60s 才生效
    -> 改为固定 SLEEP_TICK 轮询
  - app.py / schedules.py: 3 处 register() 第 2 参误传 task 类型名

C3 EvalPage.tsx: 引用未导入的 EmptyState -> 改为已导入的 EmptyText
  (tsc --noEmit 由 1 error 变为 0 error)

新增 tests/test_scheduler_registration.py: 9 项行为回归测试
C2 数据丢失修复。重构提交 6a49940 删除了 src/data/bzzoiro.py 中的
fetch_bzzoiro_events 与 BzzoiroSource 后未做替换,同时 sources.py 用
`try/except Exception: pass` 吞掉了 ImportError,导致:

  - _SOURCES 注册表恒为空
  - get_source("bzzoiro") 恒抛 ValueError("未知数据源: bzzoiro")
  - src/api/routes/ingest.py 与 schedules.py 运行时全线失效,
    且日志中无任何导入错误痕迹

修复内容:
1. 从 f05dc1a 恢复并移植 events 管线:
   - fetch_bzzoiro_events():分页抓取 /events/(纯异步,复用现有 _fetch_json_async)
   - @register class BzzoiroSource + ingest():保留原签名
     (db, *, leagues, date_from, date_to, status),保持「不 commit,
     事务由调用方 UnitOfWork 控制」的契约,并保留单联赛抓取失败的
     错误隔离(记录 error 后 continue)
   - 复用现有 _to_date / _to_int_or_none / _match_key,未重复定义
   - 血缘字段继续使用与 nm 配对的 raw(避免 P0-3 回归)
2. sources.py:导入失败改为 logger.exception 记录,新增 _loaded 标记
   保证 _load_sources() 至多执行一次;失败时不置位以便后续重试。
   get_source() 对真正未知的名字仍抛 ValueError。

验证:
   - 新增 tests/test_bzzoiro_source_registry.py(8 项):注册表非空、
     get_source/list_sources 可用、ingest 签名契约、不再静默吞异常
   - test_regressions.py:3 failed -> 2 failed
   - 全量:14 failed -> 13 failed,通过数 190 -> 199

注意事项:未改动任何 caller(routes/ingest.py、routes/schedules.py 原样
可用,证明签名保持正确)。
test_bzzoiro_source_registry.py 增至 11 项:
- 新增 TestImportOrderSelfHeal:用独立子进程验证两种真实导入次序
  (sources 先 / bzzoiro 先)下注册表均可用。该缺陷只在真实模块初始化
  时序下成立,mock 照不出来,故不 mock。
- 新增未知名字仍抛 ValueError 的契约守卫。

src/data/sources.py:补全 _load_sources 文档,说明首次装载失败属正常的
环状导入(bzzoiro 仍在初始化),失败时保持 _loaded=False 使后续
get_source()/list_sources() 调用可重试装载(自愈)。

验证:
- tests/test_bzzoiro_source_registry.py:11 passed
- 两种导入次序实测均输出 bzzoiro ['bzzoiro']
R1 429 key 轮换路径调用不存在的 _km → NameError(且位于凭证脱敏日志行):
   改用 src/data/key_ring._mask,全树不再有 _km 调用。
   [注: 该改动已随另一工作流提交 9ccda4b 一并入库]
R2 ingest_bzzoiro_standings 被截断(return 前无 upsert 逻辑,standings 表
   永不写入、total_upserted 恒为 0):从 f05dc1a 移植完整实现,按
   (league_id, season, team_id) upsert,保留逐联赛错误隔离,不加 db.commit()。
   [注: 同上,已随 9ccda4b 入库]
R3 src/llm/agents/orchestrator.py 完成日志把 list 喂给 %d → logging TypeError:
   改为 len(ok_reports),与同文件降级日志行写法一致。
R4 mode=multi 静默丢弃调用方传入的 model:predict_match_multi 新增
   model 关键字参数,经 _ACTIVE_MODEL_OVERRIDE 下传至 _agent_provider,
   显式 model 优先级最高;override 生效时跳过 provider 缓存读写以免串味,
   并在 predict.py 派发点透传。
R5 回测把字符串日期直接与 timestamptz 列比较:新增 _parse_date_bound 助手,
   支持 YYYY-MM-DD / 完整 ISO / datetime / None,裸日期按 UTC 锚定,
   结束日取当天末刻(闭区间,避免最后一天被静默排除),非法输入抛 ValueError。

新增 tests/test_review_required_fixes.py 覆盖 R1-R5(R2/R4 为行为测试),
20 项全通过。
按「测试腐化(a)/ 源码缺陷(b)/ 测试污染(c)」逐项定性,仅改 tests/:

- 外机绝对路径(4): 迁移文件路径改为相对仓库根解析(沿用
  test_regressions._read 约定),断言内容保持不变。
- cutoff 用例(4+1): mock 未生效的真因是 _agent_provider 被换成同步
  lambda,await 抛 TypeError 被生产代码吞掉后 IndexError;改为 async
  mock 并按 run_specialists/load_match_header/_agent_provider 的真实契约
  打补丁。degraded 用例再加 _upsert_prediction 顶层 kwargs(model)采集。
- 陈旧断言(3): agent 键按现契约断言中文映射;h2h mock 改 async;
  Match.stats 按设计为 lazy="select",从 MATCH_RELATIONS 移出并单独
  固化该设计决定。
- P0-3 守卫(1): seg 越界扫到下游 stats 管线导致误报,改为按缩进收口;
  合法形状含经 raw 派生变量中转的写法,并补元测试确保守卫仍能抓到回归。
- 交叉污染(6): test_multi_agent_cutoff 用 patch.object 精确还原,消除
  裸赋值泄漏的同步 mock;现已验证顺序无关。
原断言对「关系声明到下一注解」的整段(含注释)做 lazy="selectin"
子串匹配;而 Match.league 声明正下方的注释里恰好也写着该字样,
导致移除真实 kwarg 后测试依旧通过(变异测试确认)。

改为只匹配 relationship(...) 括号内内容,并加注释说明该假阳性。
验证: 干净代码通过 → 移除 league 的 lazy 后失败 → 还原后又通过。
全套 238 passed / 1 skipped。
R4 首版实现用模块级 _ACTIVE_MODEL_OVERRIDE 中转 model override,
理由是不想改动 run_specialists 的签名(既有测试会 mock 它)。

但 backtest.py:225 会 asyncio.gather 并发 8 场预测(Semaphore(8)),
每场都调用 predict_match_multi —— 模块级变量会被并发调用互相覆盖,
导致 A 场的预测用上 B 场的模型。这是静默的正确性缺陷。

改为: run_specialists 新增 model_override 形参,一路显式下传;
删除模块级变量。同步更新 6 处 mock 签名。

新增 3 个守卫并做变异验证:
  - test_r4_no_module_level_model_override_global (源码级,可判别)
  - test_r4_run_specialists_accepts_model_override_parameter
  - test_r4_dispatch_passes_override_to_specialists
变异测试: 重新引入全局变量方案 → 两个守卫变红;还原 → 全绿。

注: 曾尝试写并发行为测试,但真实 run_specialists 会访问数据库,
测试环境下不稳定(ConnectionRefusedError),会是 flaky 的假证据,
故改用清晰的源码级判别守卫,并在注释中说明原因。

240 passed / 1 skipped; tsc 0 error; vite build 成功。
shangfangjian merged commit b5f7e682f0 into main 2026-09-21 18:04:54 +08:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: shangfangjian/Profeto#8