Bun 旧 canary 冻结时间后 Date.now 又读到真实时钟,检查 JIT 路径
旧 Bun 1.4 canary 的 Date.now 热调用可能绕过模拟时间。区分 JIT 路径与测试清理,核对正式 1.4.0 引擎处理及热调用验收。
测试调用 setSystemTime() 后,少量断言能通过,较大的测试集却出现有效 TOTP 被判无效或时间断言偶发失败。若环境是旧 Bun 1.4.0-canary.1+05895486d,还要检查 Date.now() 的热调用路径是否忽略了模拟时间,不能只归因于文件之间忘了清理 mock。
这条已定位的故障发生在 JavaScriptCore 的 JIT 优化路径。正式版 Bun 1.4.0 采用的引擎源码已包含相应处理,并带有针对热调用的回归用例;旧 canary 的现象不代表当前所有 Bun 1.4 版本都受影响。
同一个 Date.now 调用点可能在运行中换路径
Date.now() 的调用足够热时,会被引擎优化成专门的操作。受影响实现的解释执行路径会读取模拟时间,优化后的路径却直接读取真实墙钟。测试前半程与后半程于是可能观察到不同时间来源。
这一条件解释了为什么只跑一个小文件看似正常、完整测试集却失败,也解释了为什么每文件隔离可能暂时隐藏现象:新的执行环境让相关代码没有那么快进入原来的热路径。它不证明测试文件顺序本身就是根因。
普通的 mock 状态泄漏仍须单独修正。任何改变测试时间的用例,都应在原有清理钩子中恢复时钟,例如:
import { afterEach, setSystemTime } from "bun:test";
afterEach(() => {
setSystemTime();
});
这段合并到负责改变时钟的测试文件中,不替代对引擎版本的核对。若当前用例本来就需要冻结时间,单纯在每次断言之前重置成真实时间,会改变被测条件。
修正需要落到实际 Bun 引擎
已核对的正式 Bun 1.4.0 所固定的 JavaScriptCore 实现,让 JIT 的 operationDateNow 通过当前全局对象读取统一的时间入口。Bun 侧还配合调整了恢复真实时间时的状态表示,不能只把引擎补丁与任意旧 Bun 源码拼在一起。
仍使用上述旧 canary 的项目,应通过原来的运行时管理方式切换到包含修正的正式版本,并确认 CI、容器及本地测试实际运行的是同一组预期版本。只更新测试依赖,不会替换 Bun 自带的引擎。
暂时无法切换时,恢复每个测试自己改变的时钟,并在该版本支持的情况下评估 bun test --isolate。隔离会改变文件之间的全局状态共享,可能增加执行开销;它是针对受影响环境的规避方式,不是证明 JIT 已修复。
检查结果要覆盖足够多次读取同一时钟调用点,让原先只在热路径出现的差异有机会暴露,同时验证恢复真实时间后不再保留冻结值。不要认定固定第几次调用必然触发优化,这取决于版本、构建和执行情况。
原有 TOTP 或时间窗口测试也应保留。通过扩大令牌有效期、跳过鉴权断言来让测试变绿,会掩盖本来需要验证的行为;这一引擎问题也不意味着所有验证码失败都与模拟时间有关。


