"""KIS 스트림 무수신 경보: 등록 0건(대시보드 미접속)은 정상 — 2026-10-12 운영 오경보 3건.""" import app.main as m class _T: def __init__(self, done): self._d = done def done(self): return self._d class _App: class state: pass def _app(done=False): a = _App() a.state = type("S", (), {})() a.state.kis_stream_task = _T(done) return a def test_zero_registrations_is_not_silent(monkeypatch): monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) monkeypatch.setattr(m, "_kis_stream_regs_since", None) limit = m._HB_KIS_STREAM_SILENT_SECONDS r = m._kis_stream_health_v3(_app(), now=100.0 + limit + 1, market_open=True, registrations=0) assert r["silent"] is False and r["registrations"] == 0 def test_dead_task_is_silent_even_with_zero_registrations(monkeypatch): monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) monkeypatch.setattr(m, "_kis_stream_regs_since", None) r = m._kis_stream_health_v3(_app(done=True), now=101.0, market_open=True, registrations=0) assert r["silent"] is True def test_resubscribe_uses_registration_time_as_reference(monkeypatch): limit = m._HB_KIS_STREAM_SILENT_SECONDS monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) monkeypatch.setattr(m, "_kis_stream_regs_since", None) t0 = 100.0 + limit + 500 # 옛 메시지 이후 오래 지나 재구독 assert m._kis_stream_health_v3(_app(), now=t0, market_open=True, registrations=8)["silent"] is False assert m._kis_stream_health_v3(_app(), now=t0 + limit - 1, market_open=True, registrations=8)["silent"] is False assert m._kis_stream_health_v3(_app(), now=t0 + limit + 1, market_open=True, registrations=8)["silent"] is True def test_registered_and_receiving_is_not_silent(monkeypatch): limit = m._HB_KIS_STREAM_SILENT_SECONDS monkeypatch.setattr(m, "_kis_stream_regs_since", 0.0) monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 1000.0) assert m._kis_stream_health_v3(_app(), now=1000.0 + limit - 5, market_open=True, registrations=8)["silent"] is False def test_unknown_registrations_falls_back_to_v2(monkeypatch): limit = m._HB_KIS_STREAM_SILENT_SECONDS monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) monkeypatch.setattr(m, "_kis_stream_regs_since", None) assert m._kis_stream_health_v3(_app(), now=100.0 + limit + 1, market_open=True, registrations=None)["silent"] is True # ── R9 E-1: 희망 구독 수 기준 v4 ───────────────────────────────────────── def test_v4_desired_subscribers_zero_regs_silent(monkeypatch): """구독자는 있는데 등록 0(연결 단절) + 수신 이력 있고 임계 초과 → silent.""" limit = m._HB_KIS_STREAM_SILENT_SECONDS monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) monkeypatch.setattr(m, "_kis_stream_regs_since", 5.0) r = m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True, registrations=0, desired=3) assert r["silent"] is True and r["desired"] == 3 assert m._kis_stream_regs_since is None def test_v4_no_desired_subscribers_is_normal(monkeypatch): limit = m._HB_KIS_STREAM_SILENT_SECONDS monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) monkeypatch.setattr(m, "_kis_stream_regs_since", None) r = m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True, registrations=0, desired=0) assert r["silent"] is False def test_v4_regs_transition_resets_regs_since(monkeypatch): """regs>0 -> 0 -> >0: 0 구간에서 기준점이 리셋되고 재등록 시각이 새 기준점이 된다.""" limit = m._HB_KIS_STREAM_SILENT_SECONDS monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) monkeypatch.setattr(m, "_kis_stream_regs_since", None) t0 = 100.0 + limit + 10 m._kis_stream_health_v4(_app(), now=t0, market_open=True, registrations=5, desired=5) assert m._kis_stream_regs_since == t0 m._kis_stream_health_v4(_app(), now=t0 + 1, market_open=True, registrations=0, desired=0) assert m._kis_stream_regs_since is None t1 = t0 + 50 m._kis_stream_health_v4(_app(), now=t1, market_open=True, registrations=5, desired=5) assert m._kis_stream_regs_since == t1 def test_v4_sentinel_reads_live_sources(monkeypatch): """registrations/desired 기본 센티널(-1) 이면 실제 조회 함수를 쓴다.""" limit = m._HB_KIS_STREAM_SILENT_SECONDS monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) monkeypatch.setattr(m, "_kis_stream_regs_since", None) monkeypatch.setattr(m, "_kis_stream_registrations_v2", lambda: 0) monkeypatch.setattr(m, "_kis_stream_desired_subscriptions_v2", lambda: 2) assert m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True)["silent"] is True monkeypatch.setattr(m, "_kis_stream_desired_subscriptions_v2", lambda: 0) assert m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True)["silent"] is False def test_v4_unknown_desired_falls_back_to_v3(monkeypatch): limit = m._HB_KIS_STREAM_SILENT_SECONDS monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) monkeypatch.setattr(m, "_kis_stream_regs_since", None) r = m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True, registrations=0, desired=None) assert r["silent"] is False def test_v4_dead_task_none_stream_and_off_hours(monkeypatch): monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0) assert m._kis_stream_health_v4(_app(done=True), now=101.0, market_open=True, registrations=0, desired=0)["silent"] is True class _NoTask: state = type("S", (), {})() assert m._kis_stream_health_v4(_NoTask(), now=101.0, market_open=True, registrations=0, desired=3) is None limit = m._HB_KIS_STREAM_SILENT_SECONDS assert m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=False, registrations=0, desired=3)["silent"] is False