记录一次源码级测试被绕过的交叉验证(2026-10-19)

测试能过,不代表代码是对的——它只代表字符串还在。

这天我同时在跑三条线:上午是 defi_dashboard 的第 13 轮交叉验证,下午给视频监控系统加了三个需求,晚上还接了个图片合成的活。但真正让我后背发凉的是上午——两个测试在我把代码改回原 bug 形态后,居然是绿的。

一、xverify13 的背景与前提

工作目录是 C:\Users\Administrator\Documents\craw_app1_wz\defi_dashboard。第 12 轮报了 2 处确证 bug:采集器币集漏了 INJ/OP/SUI,检查与合并放在同一个 try 里导致失败时会销毁历史。这轮要复核这些修复。

前提假设没变:我的错误率不低于前 12 轮,第 12 轮的数字全对而机制全错。

实际 CSV 是 23 币、504 天,窗口 2025-06-02 到 2026-10-18。历史面板里 20 个币全覆盖,多收的 ONDO/TAO/ZEC 是注释里明确标注的新增。INJ/OP/SUI 现在 lag=0,漏币修复已生效。

二、注入验证:源码级测试形同虚设

这轮的核心是复核项 4.1——测试的脆弱性。我做了 6 处注入,前两处直接引爆了问题。

注入 1:把 daily = old.combine_first(daily) 移回 except 块内,也就是复现原 bug 形态。结果 test_consistency_check_failure_never_skips_merge 仍然通过。

为什么?测试的断言是源码级字符串顺序:

1
2
seg = src[src.index("一致性检查失败"):src.index("daily = old.combine_first(daily)")]
assert "combine_first" not in seg

注入后 src.index("daily = old.combine_first(daily)") 定位的还是同一行,因为字符串本身还存在,只是多了缩进和注释。seg 恰好不包含 combine_first,断言就过了。

注入 2:把 daily = old.combine_first(daily) 改成 daily = daily,真正删掉合并逻辑。 结果 test_collector_source_contains_unconditional_merge 仍然通过。

原因更离谱——第 101 行的注释 combine_first 是【旧值优先】 还在 collect() 函数体内,inspect.getsource(FC.collect) 里照样搜得到 "combine_first" 字符串。测试被注释里的字符满足了,真正的逻辑删掉了它也不知道。

这是确证 bug:源码级断言完全无法区分「注释提到」和「代码真的调用」。

对比一下,注入 3(去掉 INJ)被正确抓到,注入 5(CSV 阈值 144000→21600 秒)被正确抓到,注入 6(NOW() 替换 UTC_TIMESTAMP)也被正确抓到。只有前两个源码级测试是虚的,恰好它们守的是最关键的语义逻辑。

三、Spearman 复算:0.13~0.53 无法复现

SYNTHESIS 文件里写了三信号之间的 Spearman 区间 0.13~0.53,来自 xverify12 实测。这轮我尝试独立复算。

三个信号定义各不相同:DVOL 残差是 dvol_resid_z30(市场级单序列),持仓结构是 top_ls_*(逐币横截面),资金费率是 fund_level(市场均值或横截面)。我先统一到市场级逐日信号值,在留出段对齐后算两两 Spearman。

留出段 1112 天,三信号非空分别是:dvol_resid 993 / fund_level 501 / pos_level 1109。缺 scipy,用纯 numpy 实现 Spearman。结果:

  • dvol_resid vs fund_level: +0.025(n=501)
  • dvol_resid vs pos_level: +0.010(n=990)
  • fund_level vs pos_level: +0.685(n=498)

与 0.130.53 区间明显不符。关键差异在于口径:我算的是信号值之间的相关,xverify12 的 0.130.53 很可能是算的逐日 IC 贡献序列之间的相关,那是完全不同的量。复算无法复现,且该区间本身口径不明。 这正是「数字全对而机制全错」的典型形态。

四、其他发现

两个表述问题和一个防御性缺口。

市场均值不是用全部 23 列。 用户 1.4 说「市场均值用全部 23 列」,但实际 mkt = F.mean(axis=1) 里的 F 是 _funding_frame(px.index, px.columns),已经 reindex 到价格面板里的协议 10 币——BTC, ETH, SOL, AVAX, LINK, LTC, BCH, XRP, UNI, AAVE。所以市场均值只用了这 10 币子集,不是全部 23 列。

_check_csv 的时区异常未被捕获。 try/except 只包裹 read_csv(第 58-62 行),第 63 行的 age 计算 (utcnow().tz_localize(None) - d.index[-1]) 在 try 之外。若 CSV index 带时区,会抛 TypeError 导致整个 check() 崩溃。但实际的 CSV 是采集器用 to_csv(index_label="ts") 写的 naive index,永远不会带时区,所以这是防御性问题,不是当前故障。

DVOL 日柱 UTC 00:00 生成,40 小时阈值充足。 Deribit DVOL 日柱时间戳 1792108800000 对应 2026-10-16 00:00:00 UTC。当前时间 10-18 16:31 UTC,DVOL 最后一行是 10-18 00:00,age 已经 16.5 小时。采集器 30 分钟跑一次,最坏到 UTC 00:30 拉到当天柱,age 最大 24.5h,远低于 40h,有 16 小时余量。

五、下午的视频监控三需求

切换战场。视频监控系统提了三个需求:回放倍速 1-16 倍、日历选择时分秒范围、计划出勤配置界面。

倍速播放有个坑:换录像窗口后浏览器会把 playbackRate 重置回 1,所以每次换源后要重新套用,倍速记在 localStorage。

时间范围选择里我最警惕的是分母失真。计划出勤必须按范围小时数同步折算——否则选 2 小时范围,实际出勤只有 2 小时的量,却拿 8 小时计划出勤当分母,利用率会凭空变成 1/4,而且不报任何错,最难发现。那天下午主要就是修这个。

计划出勤配置存到 recordings/_plan_config.json,改坏删掉自动回退代码默认值,非法输入一律 400。

六、遗留

  1. 注入 1 和 2 的两个测试必须改成行为级测试——实际 mock 一个抛异常的输入,断言合并逻辑仍执行,而不是查源码字符串。
  2. _check_csv 的时区问题要加兜底,哪怕当前 CSV 不会触发,也不能让异常裸奔。
  3. Spearman 复算的口径必须明确:xverify12 的 0.13~0.53 到底算的是信号值还是 IC 序列,这个不搞清楚,SYNTHESIS 里那个区间就是悬案。
  4. 图片合成任务还没做完——个人照只有 173×291,放进 4000×3000 的集体照后排,面部清晰度是硬伤,后面得用超分处理。
  5. 今晚知乎那篇特斯拉的文章发出去之后反响一般,飞行汽车「版本差」的切口没跑起来,回头看看是不是标题太绕了。