11from __future__ import annotations
22
33import json
4+ import logging
45from contextlib import contextmanager
56from pathlib import Path
67from typing import Any
@@ -297,15 +298,24 @@ def close(self):
297298 assert "Historian delivery failed" in caplog .text
298299
299300
300- def test_unexpected_sink_error_propagates_instead_of_being_swallowed (
301- settings : Settings , tmp_path : Path
301+ def test_unexpected_sink_error_is_logged_and_does_not_break_operations (
302+ settings : Settings , tmp_path : Path , caplog
302303) -> None :
303304 """An unexpected (non-HistorianDeliveryError) failure from the historian
304- sink must propagate rather than be swallowed by the best-effort handler.
305-
306- Regression guard for issue #47: the bare ``except Exception`` in
307- ``CiderAgentService._emit`` used to mask any sink failure as a routine
308- delivery problem. It now narrows to ``HistorianDeliveryError``.
305+ sink must not fail the surrounding operation.
306+
307+ Historian delivery is best-effort (issue #90): any exception from the sink
308+ -- whether ``HistorianDeliveryError``, an ``httpx.HTTPStatusError`` on a 4xx
309+ response, or a genuinely unexpected error from a corrupted sink -- is caught
310+ in ``EventEmitter.emit``, logged, and never allowed to propagate out of the
311+ call path. ``HistorianDeliveryError`` is logged at warning level; anything
312+ else is logged at error level with a traceback so genuine sink bugs remain
313+ visible and actionable.
314+
315+ This supersedes the issue #47 regression guard, which asserted that
316+ unexpected sink errors propagated. #90 makes the best-effort contract a hard
317+ boundary so a Historian outage or bug can never break a user-facing music
318+ operation.
309319 """
310320
311321 class PoisonedSink :
@@ -320,8 +330,55 @@ def close(self):
320330
321331 service = _service (settings , PoisonedSink (), tmp_path )
322332
323- with pytest .raises (RuntimeError , match = "sink is corrupted" ):
324- service .pause ()
333+ with caplog .at_level (logging .ERROR , logger = "vesper.events" ):
334+ result = service .pause ()
335+
336+ # The user-facing operation succeeds despite the sink failure.
337+ assert result ["status" ] == "ok"
338+ # The unexpected sink error was logged at error level with a traceback.
339+ error_records = [
340+ record for record in caplog .records
341+ if record .levelno == logging .ERROR and "Unexpected Historian sink error" in record .message
342+ ]
343+ assert error_records , "expected an error-level log for the unexpected sink failure"
344+ assert "sink is corrupted" in error_records [0 ].message
345+ assert error_records [0 ].exc_info is not None
346+ assert error_records [0 ].exc_info [0 ] is RuntimeError
347+
348+
349+ def test_http_status_error_from_sink_does_not_break_operations (
350+ settings : Settings , tmp_path : Path , caplog
351+ ) -> None :
352+ """A non-delivery exception the real HttpHistorianSink can raise -- here an
353+ ``httpx.HTTPStatusError`` from a 4xx response -- must not escape
354+ ``EventEmitter.emit`` and fail the user-facing operation (issue #90).
355+
356+ ``HttpHistorianSink._request`` re-raises ``httpx.HTTPStatusError`` for 4xx
357+ responses rather than wrapping it in ``HistorianDeliveryError``. Before #90
358+ that propagated out of ``emit``; it is now caught and logged at error level.
359+ """
360+
361+ class HttpStatusErrorSink :
362+ def emit (self , event ):
363+ raise httpx .HTTPStatusError (
364+ "Historian returned HTTP 403" ,
365+ request = httpx .Request ("POST" , "https://historian.test/v1/events" ),
366+ response = httpx .Response (403 , request = httpx .Request ("POST" , "https://historian.test/v1/events" )),
367+ )
368+
369+ def emit_batch (self , events ):
370+ self .emit (events [0 ] if events else {"id" : "noop" })
371+
372+ def close (self ):
373+ return None
374+
375+ service = _service (settings , HttpStatusErrorSink (), tmp_path )
376+
377+ with caplog .at_level (logging .ERROR , logger = "vesper.events" ):
378+ result = service .pause ()
379+
380+ assert result ["status" ] == "ok"
381+ assert "Unexpected Historian sink error" in caplog .text
325382
326383
327384def test_rpc_failures_emit_sanitized_event (settings : Settings , tmp_path : Path ) -> None :
0 commit comments