diff --git a/backend/tests/AGENTS.md b/backend/tests/AGENTS.md index fa3975c415..3b7d5721e9 100644 --- a/backend/tests/AGENTS.md +++ b/backend/tests/AGENTS.md @@ -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 diff --git a/backend/tests/test_bench_sandbox_prewarm.py b/backend/tests/test_bench_sandbox_prewarm.py index 7258bb907e..bb0565b510 100644 --- a/backend/tests/test_bench_sandbox_prewarm.py +++ b/backend/tests/test_bench_sandbox_prewarm.py @@ -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