Skip to content

Commit 815d99f

Browse files
test(worker): cover retry-capped retain extraction failures (#2418)
1 parent 2452f72 commit 815d99f

1 file changed

Lines changed: 28 additions & 3 deletions

File tree

hindsight-api-slim/tests/test_retain_transient_extraction_failure.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def _count_memory_units(memory, bank_id: str) -> int:
5353
)
5454

5555

56-
async def _run_retain_through_worker(memory, extraction_error: Exception):
56+
async def _run_retain_through_worker(memory, extraction_error: Exception, *, retry_count: int = 0):
5757
"""Enqueue a one-item retain and run it through the real poller + executor.
5858
5959
``extraction_error`` is raised by the mock LLM on the ``retain_extract_facts``
@@ -90,16 +90,18 @@ async def call_raising_on_extract(self, *args, **kwargs):
9090
"operation_id": str(operation_id),
9191
"bank_id": bank_id,
9292
"contents": [{"content": "Alice moved to Berlin in March 2024 and joined Acme as a staff engineer."}],
93+
"_retry_count": retry_count,
9394
}
9495
await pool.execute(
9596
"""
9697
INSERT INTO async_operations
97-
(operation_id, bank_id, operation_type, status, task_payload, worker_id, claimed_at)
98-
VALUES ($1, $2, 'retain', 'processing', $3::jsonb, 'test-worker-1', now())
98+
(operation_id, bank_id, operation_type, status, task_payload, retry_count, worker_id, claimed_at)
99+
VALUES ($1, $2, 'retain', 'processing', $3::jsonb, $4, 'test-worker-1', now())
99100
""",
100101
operation_id,
101102
bank_id,
102103
json.dumps(task_payload),
104+
retry_count,
103105
)
104106

105107
poller = WorkerPoller(
@@ -150,3 +152,26 @@ async def test_extraction_failure_is_retried_never_silently_completed(memory, ex
150152
)
151153
assert final["status"] == "pending", f"expected task reset to 'pending' for retry, got {final['status']!r}"
152154
assert final["retry_count"] == 1, f"expected retry_count bumped to 1, got {final['retry_count']}"
155+
156+
157+
@pytest.mark.asyncio
158+
async def test_extraction_failure_at_retry_cap_fails_terminally(memory):
159+
"""Once the worker retry cap is reached, extraction failures must fail terminally.
160+
161+
This guards the recovered-worker path seen in vectorize-io/hindsight#2413:
162+
repeated structured-output parse failures should eventually release the
163+
worker slot and leave an inspectable failed operation, not remain pending or
164+
processing indefinitely.
165+
"""
166+
final, bank_id = await _run_retain_through_worker(
167+
memory,
168+
RuntimeError("structured JSON parse failed after all retain_extract_facts attempts"),
169+
retry_count=3,
170+
)
171+
unit_count = await _count_memory_units(memory, bank_id)
172+
173+
assert final["status"] == "failed", f"expected retry-capped task to fail, got {final['status']!r}"
174+
assert final["retry_count"] == 3
175+
assert final["error_message"] is not None
176+
assert "structured JSON parse failed" in final["error_message"]
177+
assert unit_count == 0

0 commit comments

Comments
 (0)