test(control-plane): pin the resume_at transport case to an injected clock - #4421
Conversation
3a6b900 to
469c8c9
Compare
…clock The parametrized resume_at case built its canonical Todo summary without an evaluated_at, so summary construction re-derived resume readiness from the wall clock. Once real time passed the hard-coded 2026-09-15T00:00:00Z instant the case failed deterministically. - thread evaluated_at through quota_todo_summary into compact_todo_group - evaluate and summarize the case against one shared EVALUATED_AT constant - restore the scheduled-instant literal now that it is pinned to that clock - add a boundary regression covering before, at, and after the instant The boundary regression fails on the pre-fix fixture for both instants that precede the scheduled time and passes once the clock is injected. Fixes loopx-project#4413 Signed-off-by: Yue <108062406+Yue021130@users.noreply.github.com>
469c8c9 to
edb92e0
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
动机
resume_at 的传输契约用例原本用一个"足够远的未来"字面量(resume_at:2099-01-01T00:00:00Z)保证条件不满足,但 canonical summary 的构造路径没有注入求值时刻,apply_resume_conditions → evaluate_todo_resume_conditions 在 evaluated_at 缺省时会退回 now_utc_iso()。也就是说这条用例其实依赖挂钟:真实时间一旦越过所写字面量(作者在描述里说这个日期已经过去,会确定性变红),用例就会失败。修复方向是对的:把时钟注入补齐,而不是把字面量再往后推。
改动思路
生产侧的 owner 早就支持注入时钟:compact_todo_group(evaluated_at=...)(loopx/control_plane/todos/todo_summary.py:1077)和 evaluate_todo_resume_conditions(..., evaluated_at=...)(同文件 784 行,缺省才用挂钟)。真正缺的是测试夹具包装层:loopx/control_plane/testing/quota_fixtures.py 的 quota_todo_summary 没有把这个参数转发下去。因此本 PR 只在夹具上加一个可选参数并转发,再把测试模块里的时钟统一成 EVALUATED_AT/RESUME_AT 常量,并补一条覆盖边界两侧的用例。没有引入 fake clock 框架、没有 monkeypatch datetime、也没有在生产语义上做任何改动。
具体改动
loopx/control_plane/testing/quota_fixtures.py:70,79:quota_todo_summary(..., evaluated_at=None)透传给compact_todo_group;默认 None,保持所有既有调用方行为不变。tests/control_plane/test_delivery_response.py:19-20:新增EVALUATED_AT = 2026-09-14T00:00:00Z与RESUME_AT = 2026-09-15T00:00:00Z(后者比前者晚一天,因此不受 CI 运行日期影响)。- 同文件
:107,110:原参数化传输用例改用这两个常量,evaluate_todo_resume_conditions与quota_todo_summary都收到注入时刻。 - 同文件
:120,148:新增_resume_at_projection与边界用例,覆盖 2026-09-14T00:00:00Z / 23:59:59Z(未满足 →canonical_todo_wait)与 2026-09-15T00:00:00Z / +1s / +1y(已满足 →history_supervision),并断言被篡改 clock_provider 的未满足等待仍让位给history_supervision。
对主干的风险
- 生产行为零变化:唯一非测试改动是一个带默认值的可选关键字参数,且该夹具模块只被测试引用。
- 我做了两组对照实验,确认这条修复不是"看起来更整洁"而是承重的:在 head 上不注入时钟时,
resume_at:2026-09-15T00:00:00Z(当前已是过去时刻)会被总结成resume_ready=True、satisfied=True,注入2026-09-14T00:00:00Z才是False;把 head 的测试文件放到 base(c979cf11c)上跑则是 9 failed / 8 passed(该参数在 base 不存在)。 - 残留脆弱点:夹具参数仍是可选的,将来在本模块给某条
resume_when加时钟敏感条件却忘记注入时,挂钟依赖会悄悄回来。
验证(exact head edb92e042):
pytest tests/control_plane/test_delivery_response.py -q→ 17 passed(base 同文件 12 passed;head 文件在 base 上 9 failed/8 passed)。- 自建挂钟对照探针(同上,两个方向都跑了真实 summary 与
project_delivery_response)。 - 远端检查:该分支(
codex/4413-resume-at-clock)当前没有任何 CI 记录,head 的验证来自上列本地运行,合并前建议先触发一次 CI。
我的整体评价
根因定位准确、修法最小、证据可复现:它在正确的层次(测试夹具)补上了既有生产参数的透传,并把"日期一到就变红"的隐藏依赖换成显式注入时钟的边界用例。给出 APPROVE。
非阻塞建议:如果希望把这条不变式变成机械约束,可以让夹具在遇到时钟敏感的 resume_when 而未显式传入 evaluated_at 时直接报错,这样第三个调用点不会再默默退回挂钟。
English verdict: APPROVE at exact head edb92e0. The case only passed because summary construction fell back to the wall clock; quota_todo_summary now forwards the already-supported evaluated_at, and the new boundary test pins both sides of the instant (17 passed at head, 9 failed/8 passed when the head test file runs against origin/main, and a direct probe shows the un-injected clock reports the already-past instant as satisfied). One non-blocking note: the branch reports no remote CI checks yet, and the fixture parameter stays optional so a future clock-sensitive case could silently regress to wall-clock evaluation.
Summary / 概要
Fixes #4413. The parametrized
resume_atcase built its canonical Todo summary without anevaluated_at, so summary construction re-derived resume readiness from the wall clock. Once real time passed the hard-coded2026-09-15T00:00:00Zinstant, the case failed deterministically.loopx/control_plane/testing/quota_fixtures.py:quota_todo_summarynow acceptsevaluated_atand passes it tocompact_todo_group, which already supported the parameter.tests/control_plane/test_delivery_response.py: one sharedEVALUATED_ATconstant drives bothevaluate_todo_resume_conditionsand summary construction.test_resume_at_boundary_projects_from_the_injected_clock_not_the_wall_clockcovers before / at / after the scheduled instant (5 parametrized instants) and keeps the tampered-clock_providerhalf of the original contract.Note on #4411 / 与 #4411 的关系
#4411 (merged today) changed the literal to
2099-01-01T00:00:00Zas a stopgap, with a comment saying a near-future literal makes the case start failing the day it passes. That constraint no longer exists once the clock is injected, so this PR restores the2026-09-15T00:00:00Zliteral — one day ahead of the injected2026-09-14T00:00:00Zclock — so the case actually exercises a near boundary and stays deterministic forever. Happy to keep the 2099 literal instead if you prefer.Validation / 验证
evaluated_atinjection reverted, 3 cases fail — the original[resume_at:2026-09-15T00:00:00Z-patch3]plus the two boundary instants that precede the scheduled time. All pass with the fix.uv run python -m pytest tests/control_plane/test_delivery_response.py -q→ 17 passed.uv run python -m pytest tests/control_plane -q -p no:randomly→ 27 failed, 2050 passed, 5 skipped, 1918 errors; the cleanorigin/mainbaseline gives the identical 27 failed / 1918 errors with 2045 passed. The delta is exactly the 5 new boundary cases. The pre-existing failures and errors are unrelated (for exampletest_empty_canonical_ownership_never_revives_display_claim_or_local_lease[sqlite], which also fails on the clean tree).uv run ruff checkon both changed files: clean.git diff --check: clean.loopx canary premerge --from-git-diff: 3 direct checks, 4 catalog canaries, 8 risk-profile smokes — 15/15 passed, no manual holds.Scope / 范围
Test fixture and test only. No runtime behavior change, no Todo authority change, no live goal-state migration, and no default-provider or public-contract change.