test(sandbox): make prewarm clock regression deterministic (#7513)

This commit is contained in:
Willem Jiang 2026-10-25 22:29:14 +08:00 committed by GitHub
parent 885062e197
commit 00678f060b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 16 additions and 24 deletions

View File

@ -18,6 +18,10 @@ ownership and capture cleanup. Retain the real XLSX conversion coverage.
Backend tests must preserve the runtime invariants they exercise without changing production execution topology.
`test_bench_sandbox_prewarm.py` controls observation timestamps through the
benchmark module's clock binding. Keep asyncio's clock real, initialize
`Trace.started` from the controlled clock, and advance ticks at event gates.
`test_doctor_script.py` runs a copied doctor from a temporary checkout with an
empty-model config. Drop inherited `DEER_FLOW_*`, `PYTHONPATH` and `PYTHONHOME`
from child processes; cover help, invalid flags, normal reporting and an

View File

@ -4,9 +4,7 @@ from __future__ import annotations
import asyncio
import json
import math
import threading
import time
from types import SimpleNamespace
import pytest
@ -141,43 +139,33 @@ async def test_real_graph_prewarm_starts_during_model_and_is_claimed_once(observ
@pytest.mark.anyio
async def test_prewarm_reused_survives_a_coarse_clock_tie(observed, monkeypatch):
"""Pin #7044: a same-tick acquire/tool timestamp must not unclassify a reused lease.
Coarse monotonic clocks (Windows, ~15.6 ms granularity) can quantize the
prewarm acquire and the tool request into the same tick; the former strict
`<` classification then dropped the genuine prewarm from the speculative
set. Event-state capture must keep it classified as speculative.
"""
@pytest.mark.parametrize("tool_tick_offset", [0.0, 0.015], ids=["same-tick", "next-tick"])
async def test_prewarm_reused_uses_event_order_with_coarse_clock(observed, monkeypatch, tool_tick_offset: float):
"""Pin #7044: prewarm classification follows event order, even when timestamps tie."""
provider, config = observed
coarse_s = 0.015
real_monotonic = time.monotonic
def coarse_monotonic():
return math.floor(real_monotonic() / coarse_s) * coarse_s
monkeypatch.setattr(bench.time, "monotonic", coarse_monotonic)
# Spin until a fresh quantized bucket starts, so the whole overlap scenario
# lands inside the same tick deterministically.
current_bucket = coarse_monotonic()
while coarse_monotonic() == current_bucket:
await asyncio.sleep(0.001)
tick = 100.0
# Isolate the benchmark's observation clock; asyncio timeouts keep real time.
monkeypatch.setattr(bench, "time", SimpleNamespace(monotonic=lambda: tick))
model_gate = asyncio.Event()
trace = bench.Trace()
trace = bench.Trace(started=tick)
task = asyncio.create_task(bench.run_case(provider, config, case="tool", prewarm=True, model_delay=0, thread_id="coarse-tick", user_id="alice", trace=trace, model_gate=model_gate))
try:
await asyncio.wait_for(trace.model_started.wait(), 2)
await asyncio.wait_for(provider.started.wait(), 2)
provider.finish.set()
await asyncio.wait_for(trace.acquire_ready.wait(), 2)
# Advance only at this event boundary, independently of scheduler delays.
tick += tool_tick_offset
model_gate.set()
row = await asyncio.wait_for(task, 3)
finally:
provider.finish.set()
model_gate.set()
await asyncio.gather(task, return_exceptions=True)
assert trace.acquisitions[0]["started"] == trace.timestamps["tool_requested"], "precondition: the acquire and the tool request must share one quantized tick"
assert trace.acquisitions[0]["started"] == trace.started
assert trace.timestamps["tool_requested"] == trace.started + tool_tick_offset
assert trace.acquisitions[0]["precedes_tool_request"] is True
assert row["success"], json.dumps(row)
assert row["acquire_calls"] == 1
assert row["prewarm_reused"] is True