feat(bat-054): 그날-시작 KIS 인증정보 양의 경로 주입 — KISBroker reinitialize (REQ-113 AC-2/AC-4, REQ-115 AC-1)
BAT-053 스캐폴드에서 연기된 양의 경로 완성: - KISBroker.reinitialize(credential): app_key/app_secret/account_ref 교체 + L1 token 무효화 - SnapshotScheduler에 kis_reinit_callback 주입 지점 추가; 그날-시작 성공 시 callback 호출 - daemon.py에 _build_kis_reinit_callback() 헬퍼 + SnapshotScheduler 조립 수정 - 신규 테스트 12개 추가: 757 passed / 47 skipped (기존 745 대비 +12, 회귀 0) 안전 불변식 유지: - credential_fetch_enabled=False(기본) 경로 byte-for-byte 보존 - fail-closed 불변식 미파괴 (reinit 실패 시 entry_allowed 유지) - AMQP wire 계약 변경 없음 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
@@ -399,3 +399,273 @@ def test_intent_service_buy_passes_when_entry_allowed_fn_none():
|
||||
BuyIntent(intent_id="b2", portfolio_id="PF-2", qty=5, entry_price=500)
|
||||
)
|
||||
assert decision.admitted
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BAT-054: REQ-113 AC-2 / REQ-115 AC-1 — KISBroker reinit callback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_kis_reinit_callback_called_on_success():
|
||||
"""REQ-113 AC-2 / REQ-115 AC-1: fetch 성공 시 kis_reinit_callback 호출됨."""
|
||||
from trex_core.application.portfolio import BrokerCredentialPayload
|
||||
|
||||
reinit_calls = []
|
||||
|
||||
def fake_reinit(credential):
|
||||
reinit_calls.append(credential)
|
||||
|
||||
read = _make_stub() # valid broker + trading_enabled=True
|
||||
store = SnapshotStore()
|
||||
sched = SnapshotScheduler(
|
||||
read_port=read, store=store, broker=PaperBroker(),
|
||||
credential_fetch_enabled=True,
|
||||
kis_reinit_callback=fake_reinit,
|
||||
)
|
||||
sched.run_once()
|
||||
assert sched.get_entry_allowed() is True
|
||||
# callback should have been called exactly once with the fetched credential
|
||||
assert len(reinit_calls) == 1
|
||||
assert isinstance(reinit_calls[0], BrokerCredentialPayload)
|
||||
assert reinit_calls[0].account_ref == "stub-account"
|
||||
assert reinit_calls[0].app_key == "stub-app-key"
|
||||
# app_secret should be passed (for KISBroker reinit) but NOT logged
|
||||
assert reinit_calls[0].app_secret == "stub-app-secret"
|
||||
|
||||
|
||||
def test_kis_reinit_callback_not_called_when_fail_closed():
|
||||
"""REQ-113 AC-2: fetch 실패(fail-closed) 시 kis_reinit_callback 호출 안 됨."""
|
||||
reinit_calls = []
|
||||
|
||||
def fake_reinit(credential):
|
||||
reinit_calls.append(credential)
|
||||
|
||||
read = _make_stub(fetch_raises=True)
|
||||
store = SnapshotStore()
|
||||
sched = SnapshotScheduler(
|
||||
read_port=read, store=store, broker=PaperBroker(),
|
||||
credential_fetch_enabled=True,
|
||||
kis_reinit_callback=fake_reinit,
|
||||
)
|
||||
sched.run_once()
|
||||
assert sched.get_entry_allowed() is False
|
||||
# No reinit on failure
|
||||
assert len(reinit_calls) == 0
|
||||
|
||||
|
||||
def test_kis_reinit_callback_not_called_when_disabled():
|
||||
"""REQ-113 AC-1 / REQ-115 회귀: credential_fetch_enabled=False → callback 호출 없음."""
|
||||
reinit_calls = []
|
||||
|
||||
def fake_reinit(credential):
|
||||
reinit_calls.append(credential)
|
||||
|
||||
read = _make_stub()
|
||||
store = SnapshotStore()
|
||||
sched = SnapshotScheduler(
|
||||
read_port=read, store=store, broker=PaperBroker(),
|
||||
credential_fetch_enabled=False, # opt-in off (default)
|
||||
kis_reinit_callback=fake_reinit, # callback present but should not fire
|
||||
)
|
||||
sched.run_once()
|
||||
# no fetch, no reinit
|
||||
assert len(reinit_calls) == 0
|
||||
|
||||
|
||||
def test_kis_reinit_callback_failure_does_not_flip_entry_allowed():
|
||||
"""REQ-113 AC-2 설계 D2: reinit callback 실패 시 entry_allowed를 False로 전환 안 함."""
|
||||
|
||||
def failing_reinit(credential):
|
||||
raise RuntimeError("simulated KISBroker reinit failure")
|
||||
|
||||
read = _make_stub() # valid broker + trading_enabled=True
|
||||
store = SnapshotStore()
|
||||
sched = SnapshotScheduler(
|
||||
read_port=read, store=store, broker=PaperBroker(),
|
||||
credential_fetch_enabled=True,
|
||||
kis_reinit_callback=failing_reinit,
|
||||
)
|
||||
sched.run_once()
|
||||
# reinit failure must NOT cause fail-closed
|
||||
assert sched.get_entry_allowed() is True
|
||||
|
||||
|
||||
def test_kis_reinit_callback_cached_for_day_not_called_twice():
|
||||
"""REQ-115 AC-3 + REQ-113 AC-2: 그날 두 번째 run_once에서 callback 재호출 안 됨."""
|
||||
reinit_calls = []
|
||||
|
||||
def fake_reinit(credential):
|
||||
reinit_calls.append(credential)
|
||||
|
||||
read = _make_stub()
|
||||
store = SnapshotStore()
|
||||
sched = SnapshotScheduler(
|
||||
read_port=read, store=store, broker=PaperBroker(),
|
||||
credential_fetch_enabled=True,
|
||||
kis_reinit_callback=fake_reinit,
|
||||
)
|
||||
sched.run_once()
|
||||
assert len(reinit_calls) == 1
|
||||
# Second run_once same day: cache hit, no re-fetch, no re-reinit
|
||||
sched.run_once()
|
||||
assert len(reinit_calls) == 1 # still 1
|
||||
|
||||
|
||||
def test_kis_reinit_callback_none_when_break_glass(monkeypatch):
|
||||
"""REQ-113 AC-3 + AC-2: break-glass 시 callback 호출 없음 (env 경로)."""
|
||||
monkeypatch.setenv("TREX_KIS_BROKER_BREAK_GLASS", "true")
|
||||
reinit_calls = []
|
||||
|
||||
def fake_reinit(credential):
|
||||
reinit_calls.append(credential)
|
||||
|
||||
read = _make_stub()
|
||||
store = SnapshotStore()
|
||||
sched = SnapshotScheduler(
|
||||
read_port=read, store=store, broker=PaperBroker(),
|
||||
credential_fetch_enabled=True,
|
||||
kis_reinit_callback=fake_reinit,
|
||||
)
|
||||
sched.run_once()
|
||||
# break-glass: env-only path, no fetch, no reinit
|
||||
assert len(reinit_calls) == 0
|
||||
assert sched.get_entry_allowed() is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BAT-054: REQ-113 AC-4 보강 — break-glass 로그 content assert
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_break_glass_log_contains_break_glass_keyword(monkeypatch, caplog):
|
||||
"""REQ-113 AC-4 보강: break-glass 활성화 시 로그에 'break-glass' 키워드 포함."""
|
||||
import logging
|
||||
monkeypatch.setenv("TREX_KIS_BROKER_BREAK_GLASS", "true")
|
||||
read = _make_stub()
|
||||
store = SnapshotStore()
|
||||
sched = SnapshotScheduler(
|
||||
read_port=read, store=store, broker=PaperBroker(),
|
||||
credential_fetch_enabled=True,
|
||||
)
|
||||
with caplog.at_level(logging.INFO):
|
||||
sched.run_once()
|
||||
log_lower = caplog.text.lower()
|
||||
assert "break-glass" in log_lower or "break_glass" in log_lower
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BAT-054: REQ-115 AC-1 — KISBroker.reinitialize() 단위 테스트
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_kis_broker_reinitialize_updates_credentials():
|
||||
"""REQ-115 AC-1 / REQ-113 AC-2: KISBroker.reinitialize()가 인증정보를 교체함."""
|
||||
from trex_core.adapters.broker.kis import KISBroker
|
||||
from trex_core.application.portfolio import BrokerCredentialPayload
|
||||
|
||||
broker = KISBroker(
|
||||
"old-account",
|
||||
app_key="old-key",
|
||||
app_secret="old-secret",
|
||||
base_url="https://openapivts.koreainvestment.com:29443",
|
||||
allow_live=False,
|
||||
)
|
||||
# Simulate a cached token
|
||||
broker._token = "old-token"
|
||||
|
||||
new_cred = BrokerCredentialPayload(
|
||||
account_ref="new-account-01234567-01",
|
||||
app_key="new-app-key",
|
||||
app_secret="new-app-secret",
|
||||
credential_version=2,
|
||||
)
|
||||
broker.reinitialize(new_cred)
|
||||
|
||||
assert broker._account_ref == "new-account-01234567-01"
|
||||
assert broker._app_key == "new-app-key"
|
||||
assert broker._app_secret == "new-app-secret"
|
||||
# L1 token should be cleared
|
||||
assert broker._token is None
|
||||
|
||||
|
||||
def test_kis_broker_reinitialize_preserves_base_url_and_flags():
|
||||
"""REQ-115 AC-1: reinitialize()가 base_url/allow_live/_mock를 보존함."""
|
||||
from trex_core.adapters.broker.kis import KISBroker
|
||||
from trex_core.application.portfolio import BrokerCredentialPayload
|
||||
|
||||
broker = KISBroker(
|
||||
"old-account",
|
||||
app_key="old-key",
|
||||
app_secret="old-secret",
|
||||
base_url="https://openapivts.koreainvestment.com:29443",
|
||||
allow_live=False,
|
||||
)
|
||||
original_base_url = broker._base_url
|
||||
original_mock = broker._mock
|
||||
original_timeout = broker._timeout
|
||||
|
||||
new_cred = BrokerCredentialPayload(
|
||||
account_ref="new-account",
|
||||
app_key="new-key",
|
||||
app_secret="new-secret",
|
||||
)
|
||||
broker.reinitialize(new_cred)
|
||||
|
||||
assert broker._base_url == original_base_url
|
||||
assert broker._mock == original_mock
|
||||
assert broker._timeout == original_timeout
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BAT-054: daemon.py _build_kis_reinit_callback 단위 테스트
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
|
||||
# minimal config: paper broker, credential_fetch_enabled=True
|
||||
cfg = DaemonConfig(dsn="x", broker="paper", execution_interval_sec=1.0,
|
||||
credential_fetch_enabled=True)
|
||||
result = _build_kis_reinit_callback(cfg, PB())
|
||||
assert result is None
|
||||
|
||||
|
||||
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
|
||||
|
||||
cfg = DaemonConfig(dsn="x", broker="kis", execution_interval_sec=1.0,
|
||||
credential_fetch_enabled=False)
|
||||
result = _build_kis_reinit_callback(cfg, PB())
|
||||
assert result is None
|
||||
|
||||
|
||||
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
|
||||
|
||||
cfg = DaemonConfig(dsn="x", broker="kis", execution_interval_sec=1.0,
|
||||
credential_fetch_enabled=True)
|
||||
broker = KISBroker(
|
||||
"acct",
|
||||
app_key="k",
|
||||
app_secret="s",
|
||||
base_url="https://openapivts.koreainvestment.com:29443",
|
||||
allow_live=False,
|
||||
)
|
||||
result = _build_kis_reinit_callback(cfg, broker)
|
||||
assert callable(result)
|
||||
# verify calling the callback actually updates the broker
|
||||
new_cred = BrokerCredentialPayload(
|
||||
account_ref="new-acct",
|
||||
app_key="new-k",
|
||||
app_secret="new-s",
|
||||
)
|
||||
result(new_cred)
|
||||
assert broker._account_ref == "new-acct"
|
||||
assert broker._app_key == "new-k"
|
||||
|
||||
Reference in New Issue
Block a user