test(manager): keep the evidence-window fixture off the UTC date rollover - #4612
Conversation
…over `test_turn_context_reads_a_bounded_window_and_declares_sources` built six days of receipts at `datetime.now(timezone.utc)` minus `(days_ago, minutes=index)`. When the shard runs between 00:00 and 00:11 UTC the oldest row lands on a seventh UTC date, so a correct 7-day window reports seven day buckets and the assertion `len(matched_by_day) == 6` fails. PR #4591's test-shard (1) failed exactly that way at 2026-09-17T00:08:46Z. Anchor the newest receipt to 18:00 UTC on the previous UTC day, which keeps every row in the past and at least eleven minutes inside its own UTC date, so the fixture exercises the per-day and total bounds instead of the wall clock. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
动机
tests/test_chat_manager_context.py::test_turn_context_reads_a_bounded_window_and_declares_sources 用挂钟时间构造六天回执:datetime.now(timezone.utc) - timedelta(days=days_ago, minutes=index)。当测试运行在 00:00–00:11 UTC 之间时,最早的几条回执会跨到当天 UTC 日期之前,六天的 fixture 实际横跨七个日历日,于是正确的 7 天窗口返回七个 day bucket,assert len(window["matched_by_day"]) == 6 失败。PR #4591 的 test-shard (1)(job 105021863085,2026-09-17T00:08:46Z)正是这样红的:AssertionError: assert 7 == 6,bucket 为 {'2026-09-17': 6, '2026-09-16': 12, ...}。这是每天固定出现 11 分钟的必现窗口,不是随机 flake,且拦的是与本 PR 无关的改动。
改动思路
断言本身没错,错的是 fixture 让「六天」依赖挂钟。不放松断言、不改产品代码,只把 fixture 的锚点固定在「前一天 18:00 UTC」:所有回执都在过去,且每条都距离自己 UTC 日期边界至少 11 分钟,因此无论测试在当天什么时刻运行,day bucket 恒为 6。
具体改动
新增 newest = (now - timedelta(days=1)).replace(hour=18, minute=0, second=0, microsecond=0),回执改为 newest - timedelta(days=days_ago, minutes=index),并补上说明为何不能用挂钟锚点的注释。产品侧 read_manager_delivery_history 未改。
对主干的风险
仅测试 fixture,单文件、可回滚,不改产品、运行时、控制面、权限与公开面。用注入时钟(now = 2026-09-17T00:05:46Z,lookback_days=7、limit=8、total_limit=48)直接调用真实读取器核对:旧锚点 buckets=7 matched=72 included=48,新锚点 buckets=6 matched=72 included=48,窗口内命中与总限额行为不变。三分种逐分钟抽样(4320 个采样)旧锚点有 33 分钟为 7 bucket,新锚点恒为 6。本地 pytest tests/test_chat_manager_context.py tests/test_chat_manager_details.py tests/test_chat_manager_report.py -q 30 passed;ruff check 通过。同一文件也被 #4593 修改,但位于无关 hunk。
我的整体评价
修复的是必现的测试脆弱点,证据链完整(CI 日志 + 注入时钟的真实读取器对比 + 4320 点抽样),改动面最小且不放松任何断言。建议在该 head 的 required checks 变绿后合并。
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Head: 962db4495b8e0bf99a9be8480dc85f6f6dc78feb
English verdict: APPROVE
What changed
tests/test_chat_manager_context.py::test_turn_context_reads_a_bounded_window_and_declares_sourcesbuilds six days of receipts fromdatetime.now(timezone.utc)minus(days_ago, minutes=index). Twelve minute offsets push the oldest row past its own UTC date whenever the suite runs between 00:00 and 00:11 UTC, so the six-day fixture spans seven calendar dates. A correct seven-day window then reports seven day buckets, andassert len(window["matched_by_day"]) == 6fails.This PR anchors the newest fixture receipt to 18:00 UTC on the previous UTC day. Every row stays in the past, and every row sits at least eleven minutes inside its own UTC date, so the row set is date-stable at any wall-clock time and still exercises the per-day limit, the total limit, and the fully readable newest receipt.
Evidence
Failing required check on an unrelated PR (no test file touched by it):
The real reader called with an injected clock inside that window (
now = 2026-09-17T00:05:46Z,lookback_days=7,limit=8,total_limit=48):The product window is unchanged: all 72 rows stay inside the seven-day window and inside the past, and the same 48 receipts are included. Only the fixture's day span becomes deterministic. A sweep of every minute across three days (4320 samples) reports 7 buckets for 33 minutes with the old anchor and 6 buckets for all samples with the new one.
Validation
ruff format --checkreports four pre-existing hunks in this file (for example_write_delivery_index) that this PR leaves untouched; the changed lines are format-clean.Boundary
Test-only, single-purpose, reversible: one fixture anchor in one test file, no product code, runtime, control-plane, permission, scoring, or public-surface change. No credentials, private state, raw evidence, or local paths. The file is also touched by #4593, but in an unrelated hunk.