Skip to content

Commit 25e68f9

Browse files
jkennedyvzDeep Agent
andauthored
test(talon): make history backoff coverage deterministic (#6142)
The backoff test raced three SQLite appends against a 100 ms retry window. Control the worker clock and synchronize on its waits to verify appends preserve the retry deadline, pending work stays queued, and all four messages are eventually indexed. Validation: `test_history_profiles.py` — 48 passed, 3 skipped; Ruff passed. An injected early-retry regression makes the test fail. Co-authored-by: Deep Agent <agent@deepagents.dev>
1 parent 90fe4f4 commit 25e68f9

1 file changed

Lines changed: 33 additions & 14 deletions

File tree

libs/talon/tests/unit_tests/test_history_profiles.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
from dataclasses import replace
66
from types import SimpleNamespace
7+
from typing import TYPE_CHECKING
78

89
import httpx
910
import pytest
@@ -17,6 +18,10 @@
1718
from deepagents_talon.history_vector_backends import vector_backend
1819
from tests.unit_tests.test_history_vectors import SCOPE, settled
1920

21+
if TYPE_CHECKING:
22+
from collections.abc import Awaitable
23+
from pathlib import Path
24+
2025
PREFIX = "DEEPAGENTS_TALON_HISTORY_EMBED_"
2126

2227

@@ -519,43 +524,57 @@ def driver(module, _extra):
519524
assert type(index["embed"]).__name__ == "AutoEmbeddings"
520525

521526

522-
async def test_rate_limit_backoff_survives_new_appends(tmp_path, monkeypatch):
523-
from deepagents_talon import history_vectors # noqa: PLC0415
527+
async def test_rate_limit_backoff_survives_new_appends(
528+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
529+
) -> None:
530+
now = 0.0
531+
waits: asyncio.Queue[float] = asyncio.Queue()
532+
533+
async def wait_for(wake: Awaitable[object], timeout: float) -> None: # noqa: ASYNC109 # Match asyncio.wait_for's interface.
534+
waits.put_nowait(timeout)
535+
await asyncio.wait_for(wake, 5)
536+
537+
# Control only the worker's clock; real asyncio deadlines still bound the test.
538+
worker_asyncio = SimpleNamespace(**vars(asyncio))
539+
worker_asyncio.get_running_loop = lambda: SimpleNamespace(time=lambda: now)
540+
worker_asyncio.wait_for = wait_for
541+
monkeypatch.setattr(history_vectors, "asyncio", worker_asyncio)
524542

525543
class RateLimited(RecordingEmbeddings):
526544
attempts = 0
527-
failed = asyncio.Event()
528-
succeeded = asyncio.Event()
529545

530-
async def aembed_documents(self, texts):
546+
async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
531547
self.attempts += 1
532548
if self.attempts == 1:
533-
self.failed.set()
534-
response = httpx.Response(429, headers={"retry-after": "0.1"})
549+
response = httpx.Response(429, headers={"retry-after": "120"})
535550
msg = "rate limited"
536551
raise httpx.HTTPStatusError(
537552
msg,
538553
request=httpx.Request("POST", "https://test.invalid"),
539554
response=response,
540555
)
541-
self.succeeded.set()
542556
return await super().aembed_documents(texts)
543557

544558
raw = RateLimited()
545559
fake_adapter(monkeypatch, raw)
546-
monkeypatch.setattr(history_vectors, "_RETRY_SECONDS", 0.001)
547560
monkeypatch.setattr(history_vectors.secrets, "randbelow", lambda _bound: 0)
548561
config = configuration(tmp_path)
549562
async with open_history(config) as archive:
563+
assert await asyncio.wait_for(waits.get(), 5) == history_vectors._RETRY_SECONDS
550564
await archive.append(SCOPE, "first", "time", [HumanMessage("first")])
551-
await asyncio.wait_for(raw.failed.wait(), 1)
565+
assert await asyncio.wait_for(waits.get(), 5) == 120
552566
for i in range(3):
553-
await archive.append(SCOPE, f"later-{i}", "time", [HumanMessage("later")])
554-
assert not raw.succeeded.is_set()
555-
assert await archive.vectors.archive.pending(SCOPE)
556-
await asyncio.wait_for(raw.succeeded.wait(), 1)
567+
now += 20
568+
await archive.append(SCOPE, f"later-{i}", "time", [HumanMessage(f"later-{i}")])
569+
assert await asyncio.wait_for(waits.get(), 5) == 120 - now
570+
assert raw.attempts == 1
571+
assert await archive.vectors.archive.pending(SCOPE)
572+
now = 120
573+
archive.vectors.wake.set()
557574
await settled(archive)
558575
assert not await archive.vectors.archive.pending(SCOPE)
576+
stored = await archive.vectors.store.asearch(archive.vectors.namespace("whatsapp", "one"))
577+
assert {item.value["text"] for item in stored} == {"first", "later-0", "later-1", "later-2"}
559578

560579

561580
async def test_threaded_store_uses_native_async_embeddings(tmp_path):

0 commit comments

Comments
 (0)