From 39e6dae0413c5c51ec71cce5c739304b387d29b8 Mon Sep 17 00:00:00 2001 From: Stefan Jansen Date: Tue, 11 Aug 2026 16:24:50 -0400 Subject: [PATCH] fix: wait through early soak wakeups Closes #58 --- scripts/qualification/qualify_paper.py | 9 +++++++-- tests/unit/test_paper_qualification.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/scripts/qualification/qualify_paper.py b/scripts/qualification/qualify_paper.py index de0fc12..f1d741f 100644 --- a/scripts/qualification/qualify_paper.py +++ b/scripts/qualification/qualify_paper.py @@ -729,6 +729,11 @@ async def _soak_snapshot(provider: str, broker: Any, *, started_monotonic: float } +async def _sleep_until(deadline: float) -> None: + while (remaining := deadline - time.monotonic()) > 0: + await asyncio.sleep(remaining) + + async def run_provider_soak( *, provider: str, candidate: dict[str, Any], checkout_root: Path ) -> dict[str, Any]: @@ -774,7 +779,7 @@ async def run_provider_soak( deadline, ) if not reconnected and reconnect_at <= snapshot_at: - await asyncio.sleep(max(0.0, reconnect_at - time.monotonic())) + await _sleep_until(reconnect_at) if not broker.is_connected: unexpected_disconnect_count += 1 raise PaperQualificationError("paper provider disconnected during the soak") @@ -788,7 +793,7 @@ async def run_provider_soak( broker.assert_paper_trading() reconnect_count += 1 reconnected = True - await asyncio.sleep(max(0.0, snapshot_at - time.monotonic())) + await _sleep_until(snapshot_at) if not broker.is_connected: unexpected_disconnect_count += 1 raise PaperQualificationError("paper provider disconnected during the soak") diff --git a/tests/unit/test_paper_qualification.py b/tests/unit/test_paper_qualification.py index 50a3f8a..54ccbf1 100644 --- a/tests/unit/test_paper_qualification.py +++ b/tests/unit/test_paper_qualification.py @@ -407,6 +407,17 @@ def test_provider_soak_rejects_incomplete_or_failed_evidence(field: str, value: async def test_provider_soak_runs_continuously_and_reconnects_once( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: + monotonic_time = 0.0 + woke_early = False + + async def fake_sleep(delay: float) -> None: + nonlocal monotonic_time, woke_early + if not woke_early and monotonic_time >= 0.3 and delay > 0.01: + monotonic_time += delay - 0.01 + woke_early = True + else: + monotonic_time += delay + class FakeBroker: def __init__(self) -> None: self.is_connected = False @@ -433,6 +444,8 @@ async def fake_snapshot(provider: str, captured_broker: object) -> dict: monkeypatch.setattr(paper_qualification, "SOAK_DURATION_SECONDS", 0.4) monkeypatch.setattr(paper_qualification, "SOAK_SNAPSHOT_INTERVAL_SECONDS", 0.1) + monkeypatch.setattr(paper_qualification.time, "monotonic", lambda: monotonic_time) + monkeypatch.setattr(paper_qualification.asyncio, "sleep", fake_sleep) monkeypatch.setattr(paper_qualification, "_verify_installed_candidate", lambda *_args: None) monkeypatch.setattr(paper_qualification, "_build_broker", lambda _provider: broker) monkeypatch.setattr(paper_qualification, "_snapshot", fake_snapshot) @@ -445,6 +458,8 @@ async def fake_snapshot(provider: str, captured_broker: object) -> dict: assert report["passed"] is True, { key: value for key, value in report.items() if key not in {"candidate", "snapshots"} } + assert woke_early is True + assert report["duration_seconds"] >= 0.4 assert report["reconnect_count"] == 1 assert len(report["snapshots"]) >= 5 assert broker.connect_count == 2