Fix unmanaged reconciliation review blockers
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
f9284d1de8
commit
591db7adb7
@@ -1,7 +1,8 @@
|
||||
"""Snapshot scheduler tests (REQ-030 AC-1; REQ-114 AC-1~9; REQ-115 AC-1~6)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from datetime import UTC, datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from trex_core.adapters.broker.paper import PaperBroker
|
||||
@@ -44,10 +45,42 @@ def test_run_once_failure_sets_degraded_signal():
|
||||
store = SnapshotStore()
|
||||
sched = SnapshotScheduler(read_port=BadRead(), store=store, broker=PaperBroker())
|
||||
assert sched.run_once() is False
|
||||
reason, msg = store.degraded_state()
|
||||
reason, _msg = store.degraded_state()
|
||||
assert reason is not None and reason.value == "portfolio_hub_unavailable"
|
||||
|
||||
|
||||
def test_run_once_uses_authoritative_core_holdings_broker(monkeypatch):
|
||||
read = StubPortfolioRead(
|
||||
daemon_id="d-1",
|
||||
items=[PortfolioItem(portfolio_id="pf-X", symbol="X", cash_balance=100, asset_quantity=0)],
|
||||
)
|
||||
store = SnapshotStore()
|
||||
projected_broker = object()
|
||||
authoritative_broker = object()
|
||||
captured = {}
|
||||
|
||||
def _capture(**kwargs):
|
||||
captured["broker"] = kwargs["broker"]
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"trex_core.application.reconciliation.reconcile_core_holdings",
|
||||
_capture,
|
||||
)
|
||||
sched = SnapshotScheduler(
|
||||
read_port=read,
|
||||
store=store,
|
||||
broker=projected_broker,
|
||||
core_holdings_broker=authoritative_broker,
|
||||
slot_repo=object(),
|
||||
slot_service=SimpleNamespace(instance_key="inst-1"),
|
||||
)
|
||||
sched._reconcile_slots = lambda _items: None
|
||||
|
||||
assert sched.run_once() is True
|
||||
assert captured["broker"] is authoritative_broker
|
||||
|
||||
|
||||
def test_swap_preserves_warning_for_carried_over_portfolio():
|
||||
store = SnapshotStore()
|
||||
store.swap([PortfolioItem(portfolio_id="pf-X", symbol="X",
|
||||
@@ -99,7 +132,7 @@ def _fixed_clock(kst_hour=8, kst_minute=0):
|
||||
"""Return a clock callable fixed at a given KST time today."""
|
||||
now_kst = datetime.now(tz=_KST).replace(hour=kst_hour, minute=kst_minute,
|
||||
second=0, microsecond=0)
|
||||
return lambda: now_kst.astimezone(timezone.utc)
|
||||
return lambda: now_kst.astimezone(UTC)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -616,8 +649,8 @@ def test_kis_broker_reinitialize_preserves_base_url_and_flags():
|
||||
def test_build_kis_reinit_callback_returns_none_for_paper():
|
||||
"""REQ-113 AC-2: broker=paper이면 callback=None."""
|
||||
from trex_core.adapters.broker.paper import PaperBroker as PB
|
||||
from trex_core.runtime.daemon import _build_kis_reinit_callback
|
||||
from trex_core.runtime.config import DaemonConfig
|
||||
from trex_core.runtime.daemon import _build_kis_reinit_callback
|
||||
|
||||
# minimal config: paper broker, credential_fetch_enabled=True
|
||||
cfg = DaemonConfig(dsn="x", broker="paper", execution_interval_sec=1.0,
|
||||
@@ -629,8 +662,8 @@ def test_build_kis_reinit_callback_returns_none_for_paper():
|
||||
def test_build_kis_reinit_callback_returns_none_when_disabled():
|
||||
"""REQ-113 AC-1: credential_fetch_enabled=False이면 callback=None."""
|
||||
from trex_core.adapters.broker.paper import PaperBroker as PB
|
||||
from trex_core.runtime.daemon import _build_kis_reinit_callback
|
||||
from trex_core.runtime.config import DaemonConfig
|
||||
from trex_core.runtime.daemon import _build_kis_reinit_callback
|
||||
|
||||
cfg = DaemonConfig(dsn="x", broker="kis", execution_interval_sec=1.0,
|
||||
credential_fetch_enabled=False)
|
||||
@@ -642,8 +675,8 @@ def test_build_kis_reinit_callback_returns_callable_for_kis():
|
||||
"""REQ-113 AC-2 / REQ-115 AC-1: broker=kis + fetch enabled → callable 반환 + 실제 reinitialize 호출됨."""
|
||||
from trex_core.adapters.broker.kis import KISBroker
|
||||
from trex_core.application.portfolio import BrokerCredentialPayload
|
||||
from trex_core.runtime.daemon import _build_kis_reinit_callback
|
||||
from trex_core.runtime.config import DaemonConfig
|
||||
from trex_core.runtime.daemon import _build_kis_reinit_callback
|
||||
|
||||
cfg = DaemonConfig(dsn="x", broker="kis", execution_interval_sec=1.0,
|
||||
credential_fetch_enabled=True)
|
||||
|
||||
@@ -35,6 +35,11 @@ class RaisingBroker(FakeBroker):
|
||||
raise BrokerBalanceError("transport_failed")
|
||||
|
||||
|
||||
class AuthoritativeBroker(FakeBroker):
|
||||
def exposes_full_balance_snapshot(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class FilteredBroker(FakeBroker):
|
||||
def exposes_full_balance_snapshot(self) -> bool:
|
||||
return False
|
||||
@@ -116,6 +121,13 @@ def test_in_memory_store_preserves_first_seen_when_resolved_symbol_reappears():
|
||||
t3 = t2 + timedelta(minutes=1)
|
||||
|
||||
store.observe(instance_key=INSTANCE, symbol="008110", quantity=3, observed_at=t1)
|
||||
store.record_operator_action(
|
||||
instance_key=INSTANCE,
|
||||
symbol="008110",
|
||||
action="investigating",
|
||||
evidence_ref="kis-balance:2026-09-07",
|
||||
note="before resolve",
|
||||
)
|
||||
store.resolve_absent(instance_key=INSTANCE, observed_symbols=set(), observed_at=t2)
|
||||
reopened = store.observe(instance_key=INSTANCE, symbol="008110", quantity=5, observed_at=t3)
|
||||
|
||||
@@ -123,6 +135,8 @@ def test_in_memory_store_preserves_first_seen_when_resolved_symbol_reappears():
|
||||
assert reopened["state"]["first_seen_at"] == t1
|
||||
assert reopened["state"]["last_seen_at"] == t3
|
||||
assert reopened["state"]["quantity_changed_at"] == t3
|
||||
assert reopened["state"]["operator_action"] is None
|
||||
assert reopened["state"]["evidence_ref"] is None
|
||||
|
||||
|
||||
def test_reconcile_core_holdings_suppresses_repeated_unmanaged_alarm_spam():
|
||||
@@ -134,7 +148,7 @@ def test_reconcile_core_holdings_suppresses_repeated_unmanaged_alarm_spam():
|
||||
decisions = reconcile_core_holdings(
|
||||
slot_repo=repo,
|
||||
slot_service=svc,
|
||||
broker=FakeBroker({"008110": 3}),
|
||||
broker=AuthoritativeBroker({"008110": 3}),
|
||||
instance_key=INSTANCE,
|
||||
alarm_emitter=emitter,
|
||||
unmanaged_position_store=store,
|
||||
@@ -166,7 +180,7 @@ def test_reconcile_core_holdings_tracks_change_and_resolution():
|
||||
reconcile_core_holdings(
|
||||
slot_repo=repo,
|
||||
slot_service=svc,
|
||||
broker=FakeBroker({"008110": 5}),
|
||||
broker=AuthoritativeBroker({"008110": 5}),
|
||||
instance_key=INSTANCE,
|
||||
alarm_emitter=emitter,
|
||||
unmanaged_position_store=store,
|
||||
@@ -174,7 +188,7 @@ def test_reconcile_core_holdings_tracks_change_and_resolution():
|
||||
reconcile_core_holdings(
|
||||
slot_repo=repo,
|
||||
slot_service=svc,
|
||||
broker=FakeBroker({}),
|
||||
broker=AuthoritativeBroker({}),
|
||||
instance_key=INSTANCE,
|
||||
alarm_emitter=emitter,
|
||||
unmanaged_position_store=store,
|
||||
@@ -211,6 +225,25 @@ def test_broker_failure_does_not_resolve_unmanaged_state():
|
||||
assert state["lifecycle_state"] == "active"
|
||||
|
||||
|
||||
def test_missing_authoritative_marker_does_not_resolve_unmanaged_state():
|
||||
repo, svc = _wire()
|
||||
store = InMemoryUnmanagedPositionStore()
|
||||
store.observe(instance_key=INSTANCE, symbol="008110", quantity=3)
|
||||
|
||||
decisions = reconcile_core_holdings(
|
||||
slot_repo=repo,
|
||||
slot_service=svc,
|
||||
broker=FakeBroker({}),
|
||||
instance_key=INSTANCE,
|
||||
unmanaged_position_store=store,
|
||||
)
|
||||
|
||||
assert decisions == {}
|
||||
state = store.get_state(instance_key=INSTANCE, symbol="008110")
|
||||
assert state is not None
|
||||
assert state["lifecycle_state"] == "active"
|
||||
|
||||
|
||||
def test_non_authoritative_broker_view_does_not_resolve_unmanaged_state():
|
||||
repo, svc = _wire()
|
||||
store = InMemoryUnmanagedPositionStore()
|
||||
|
||||
Reference in New Issue
Block a user