From d4a968ee1a4e86f2aa23bf8c72822f61a843645c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:25:39 +0000 Subject: [PATCH] fix: normalize snapshot read failures to unavailable in slots RPC path Co-authored-by: genichin <4282227+genichin@users.noreply.github.com> --- tests/application/test_account_snapshot.py | 21 ++++++++ .../test_slots_handler_broker_quantity.py | 48 ++++++++++++++++++- trex_core/application/account_snapshot.py | 11 ++++- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/tests/application/test_account_snapshot.py b/tests/application/test_account_snapshot.py index 2592ec8..7a171e5 100644 --- a/tests/application/test_account_snapshot.py +++ b/tests/application/test_account_snapshot.py @@ -36,6 +36,11 @@ class FakeAccountBroker: return self.cash +class BrokenLoadStore(InMemoryAccountSnapshotStore): + def load(self, account_hash: str) -> AccountSnapshot | None: + raise RuntimeError("db read failed") + + def _wire(): now = [datetime(2026, 7, 29, tzinfo=UTC)] broker = FakeAccountBroker() @@ -177,3 +182,19 @@ def test_slots_rpc_uses_snapshot_only_with_sell_ttl_freshness(): with pytest.raises(AccountSnapshotUnavailable) as raised: service.holdings_for_slots_rpc() assert raised.value.code == "expired" + + +def test_slots_rpc_normalizes_snapshot_store_read_failure(): + broker = FakeAccountBroker() + service = AccountSnapshotService( + broker=broker, + store=BrokenLoadStore(), + buy_max_age_sec=600, + sell_max_age_sec=3600, + clock=lambda: datetime(2026, 7, 29, tzinfo=UTC), + rng=lambda: 0.5, + ) + + with pytest.raises(AccountSnapshotUnavailable) as raised: + service.holdings_for_slots_rpc() + assert raised.value.code == "read_failed" diff --git a/tests/application/test_slots_handler_broker_quantity.py b/tests/application/test_slots_handler_broker_quantity.py index cfb8e52..7a8d571 100644 --- a/tests/application/test_slots_handler_broker_quantity.py +++ b/tests/application/test_slots_handler_broker_quantity.py @@ -2,8 +2,14 @@ from __future__ import annotations from datetime import UTC, datetime +from typing import Protocol -from trex_core.application.account_snapshot import AccountSnapshotUnavailable +from trex_core.application.account_snapshot import ( + AccountSnapshot, + AccountSnapshotService, + AccountSnapshotUnavailable, + InMemoryAccountSnapshotStore, +) from trex_core.application.broker_port import BalanceItem from trex_core.application.messaging import RpcRequest from trex_core.application.messaging_handlers import make_slots_rpc_handler @@ -32,9 +38,29 @@ class _Reader: return list(self._holdings), self._captured_at +class _SlotsReader(Protocol): + def holdings_for_slots_rpc(self) -> tuple[list[BalanceItem], datetime]: ... + + +class _BrokenLoadStore(InMemoryAccountSnapshotStore): + def load(self, account_hash: str) -> AccountSnapshot | None: + raise RuntimeError("db read failed") + + +class _Broker: + def account_scope_hash(self) -> str: + return "a" * 64 + + def get_balance(self) -> list[BalanceItem]: + return [] + + def get_available_cash(self, *, price_check_code: str = "005930") -> int: + return 0 + + def _holding_handler( *, - broker_reader: _Reader, + broker_reader: _SlotsReader, instance_key: str = "default", portfolio_id: str = "pf-A", symbol: str = "104620", @@ -131,6 +157,24 @@ def test_holding_qty_stale_snapshot_is_unavailable(): assert row["broker_quantity_available"] is False +def test_holding_qty_store_read_failure_is_unavailable(): + _, handler = _holding_handler( + broker_reader=AccountSnapshotService( + broker=_Broker(), + store=_BrokenLoadStore(), + buy_max_age_sec=600, + sell_max_age_sec=3600, + clock=lambda: datetime(2026, 8, 24, 12, 0, tzinfo=UTC), + rng=lambda: 0.5, + ), + ) + row = handler(_req_holding()).payload["slots"][0] + assert row["qty"] is None + assert row["qty_source"] == "unavailable" + assert row["broker_quantity_available"] is False + assert row["broker_quantity_captured_at"] is None + + def test_broker_quantity_reader_is_instance_bound_per_handler(): repo = InMemorySlotRepository() svc = SlotService(repo) diff --git a/trex_core/application/account_snapshot.py b/trex_core/application/account_snapshot.py index aeeca08..c61c08c 100644 --- a/trex_core/application/account_snapshot.py +++ b/trex_core/application/account_snapshot.py @@ -49,6 +49,7 @@ class AccountSnapshotUnavailable(RuntimeError): "account_mismatch", "expired", "live_read_failed", + "read_failed", }) def __init__(self, code: str) -> None: @@ -189,7 +190,15 @@ class AccountSnapshotService: account_hash = self._broker.account_scope_hash() if not account_hash: raise AccountSnapshotUnavailable("account_mismatch") - snapshot = self._store.load(account_hash) + try: + snapshot = self._store.load(account_hash) + except Exception as exc: + _log.warning( + "account snapshot load failed: account_hash=%s", + account_hash, + exc_info=True, + ) + raise AccountSnapshotUnavailable("read_failed") from exc if snapshot is None or snapshot.account_hash != account_hash: raise AccountSnapshotUnavailable("account_mismatch") return snapshot