diff --git a/tests/application/test_reconcile_warning_blocking.py b/tests/application/test_reconcile_warning_blocking.py index c1c5f8b..d1b348c 100644 --- a/tests/application/test_reconcile_warning_blocking.py +++ b/tests/application/test_reconcile_warning_blocking.py @@ -101,22 +101,29 @@ class DuplicateInFlightIntentStore: raise AssertionError("finalize must not be called for duplicate_in_flight") -class ContextFailingSlotRepository(InMemorySlotRepository): - """Raise only on the N+1th fetch_slot to simulate post-finalize lookup failure.""" +class PostFinalizeContextFailingSlotRepository(InMemorySlotRepository): + """Raise fetch_slot only after explicitly armed by finalize.""" - def __init__(self, *, fail_on_fetch_call_no=None): + def __init__(self): super().__init__() - self._fetch_calls = 0 - self._fail_on_fetch_call_no = fail_on_fetch_call_no + self._raise_on_fetch = False def fetch_slot(self, slot_id): - self._fetch_calls += 1 - if self._fail_on_fetch_call_no == self._fetch_calls: + if self._raise_on_fetch: raise RuntimeError("context lookup failed") return super().fetch_slot(slot_id) - def fail_on_next_fetch(self): - self._fail_on_fetch_call_no = self._fetch_calls + 1 + +class FinalizeArmingIntentStore(InMemoryIntentStore): + """Arm repo fetch failure immediately after finalize persistence.""" + + def __init__(self, repo): + super().__init__() + self._repo = repo + + def finalize(self, intent_id, status, slot_id, reason): + super().finalize(intent_id, status, slot_id, reason) + self._repo._raise_on_fetch = True def _make_holding_slot(repo, svc, instance_key="default", portfolio_id="pf-A", symbol="005930", qty=10): @@ -531,10 +538,10 @@ def test_reconcile_warned_alarm_finalize_failure_and_publish_failure_are_isolate def test_reconcile_warned_buy_context_lookup_failure_does_not_change_response(): - repo = ContextFailingSlotRepository(fail_on_fetch_call_no=2) + repo = PostFinalizeContextFailingSlotRepository() svc = SlotService(repo) warn_store = InMemoryReconcileWarningStore() - intent_store = InMemoryIntentStore() + intent_store = FinalizeArmingIntentStore(repo) alarms = RecordingAlarmEmitter() repo.reconcile_universe("default", {"pf-A": "005930"}) slot = repo.find_slot_by_portfolio("default", "pf-A") @@ -558,13 +565,12 @@ def test_reconcile_warned_buy_context_lookup_failure_does_not_change_response(): def test_reconcile_warned_sell_context_lookup_failure_does_not_change_response(): - repo = ContextFailingSlotRepository() + repo = PostFinalizeContextFailingSlotRepository() svc = SlotService(repo) warn_store = InMemoryReconcileWarningStore() - intent_store = InMemoryIntentStore() + intent_store = FinalizeArmingIntentStore(repo) alarms = RecordingAlarmEmitter() slot = _make_holding_slot(repo, svc, qty=10, portfolio_id="pf-A", symbol="005930") - repo._fail_on_fetch_call_no = repo._fetch_calls + 2 warn_store.set_warned(str(slot.slot_id), portfolio_id="pf-A", case_code="C") intent_svc = IntentService( svc, repo, intent_store, instance_key="default", warn_store=warn_store, alarm_emitter=alarms diff --git a/trex_core/application/alarm_emitter.py b/trex_core/application/alarm_emitter.py index 679b69f..4e7f285 100644 --- a/trex_core/application/alarm_emitter.py +++ b/trex_core/application/alarm_emitter.py @@ -17,7 +17,6 @@ where no broker connection exists. In that case publish_alarm() logs and skips. from __future__ import annotations import logging -import sys from datetime import datetime, timedelta, timezone from trex_core.adapters.messaging.topology import alarm_routing_key @@ -91,11 +90,9 @@ class AlarmEmitter: code, title, ) return False - except Exception: # noqa: BLE001 — alarm must never crash the caller - exc = sys.exc_info()[1] - exc_name = type(exc).__name__ if exc is not None else "Exception" + except Exception as exc: # noqa: BLE001 — alarm must never crash the caller _log.error( "alarm publish failed: code=%s title=%s error_type=%s", - code, title, exc_name, + code, title, type(exc).__name__, ) return False