Files
trex-core/tests/application/test_alarm_emitter.py
T
genichinandClaude Opus 4.8 d4c5fcbba1 feat(alarm): trex-core 알람 emitter + trex.alerts exchange (dcy-033 BAT-049, Implemented)
AlarmEmitter(application, best-effort 비차단)+trex.alerts topic exchange 가산·격리+8 CRITICAL code 배선. 608 passed, opus QC PASS, 팀장 e2e(텔레그램 HTTP200) 통과. REQ-098~101 Implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
2026-06-21 19:25:55 +09:00

157 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""AlarmEmitter unit tests (dcy-033 / REQ-100).
Coverage:
- 8 CRITICAL code × (a) normal publish + envelope validation
× (b) TransportUnavailableError → no re-raise + ERROR log
- arbitrary exception swallowing
- transport=None skip (reset.py / CLI path)
- routing key format: alarm.critical.trex-core
"""
from __future__ import annotations
import logging
import pytest
from trex_core.adapters.messaging.in_memory import InMemoryTransport
from trex_core.application.alarm_emitter import AlarmEmitter
# ------------------------------------------------------------------ helpers
CRITICAL_CODES = [
"fill_price_unresolvable",
"sell_skipped_no_balance",
"core_holding_broker_flat",
"ledger_delivery_failed",
"kill_switch_fired",
"publish_unavailable_after_reconnect",
"dlq_publish_failed",
"portfolio_hub_unavailable",
]
_ROUTING_KEY = "alarm.critical.trex-core"
def _make_emitter() -> tuple[AlarmEmitter, InMemoryTransport]:
transport = InMemoryTransport()
return AlarmEmitter(transport), transport
# ------------------------------------------------------------------ envelope validation
@pytest.mark.parametrize("code", CRITICAL_CODES)
def test_publish_alarm_envelope_fields(code: str):
"""(a) Normal publish: envelope contains all 7 required fields with correct values."""
emitter, transport = _make_emitter()
fields_in = {"slot_id": "abc", "symbol": "005930"}
emitter.publish_alarm(code, f"Test alarm for {code}", fields=fields_in)
alarms = transport.alarm_events()
assert len(alarms) == 1, f"Expected 1 alarm event for code={code}"
rk, payload = alarms[0]
assert rk == _ROUTING_KEY, f"routing key mismatch: {rk}"
# REQ-098: all 7 envelope fields
assert payload["source"] == "trex-core"
assert payload["severity"] == "critical"
assert payload["code"] == code
assert payload["title"] == f"Test alarm for {code}"
assert payload["fields"] == fields_in
assert "occurred_at" in payload
assert payload["schema_version"] == "1"
@pytest.mark.parametrize("code", CRITICAL_CODES)
def test_publish_alarm_transport_unavailable_no_reraise(
code: str, caplog: pytest.LogCaptureFixture
):
"""(b) TransportUnavailableError must not propagate; ERROR log must appear."""
emitter, transport = _make_emitter()
transport.set_available(False)
with caplog.at_level(logging.ERROR, logger="trex_core.application.alarm_emitter"):
# Must NOT raise
emitter.publish_alarm(code, f"Test alarm for {code}")
# No alarms recorded (transport was down)
assert transport.alarm_events() == []
# ERROR log must have been emitted
assert any("alarm publish" in r.message for r in caplog.records), (
f"Expected ERROR log for code={code}"
)
# ------------------------------------------------------------------ additional edge cases
@pytest.mark.parametrize("code", CRITICAL_CODES)
def test_publish_alarm_arbitrary_exception_swallowed(code: str, caplog: pytest.LogCaptureFixture):
"""Any exception (not just TransportUnavailableError) is swallowed + ERROR logged."""
class BrokenTransport:
def publish_alarm_event(self, routing_key, payload):
raise RuntimeError("boom")
def health(self):
from trex_core.application.messaging import TransportHealth
return TransportHealth.CONNECTED
emitter = AlarmEmitter(BrokenTransport()) # type: ignore[arg-type]
with caplog.at_level(logging.ERROR, logger="trex_core.application.alarm_emitter"):
emitter.publish_alarm(code, "boom test") # Must NOT raise
assert any("alarm publish" in r.message for r in caplog.records)
def test_publish_alarm_none_transport_skip(caplog: pytest.LogCaptureFixture):
"""transport=None: publish is skipped with INFO log (reset.py / CLI path, REQ-100 KD-2)."""
emitter = AlarmEmitter(None)
with caplog.at_level(logging.INFO, logger="trex_core.application.alarm_emitter"):
emitter.publish_alarm("kill_switch_fired", "no transport test")
assert any("alarm skipped" in r.message for r in caplog.records)
def test_publish_alarm_empty_fields_default():
"""fields defaults to {} when not provided — not None in payload."""
emitter, transport = _make_emitter()
emitter.publish_alarm("kill_switch_fired", "no fields")
_, payload = transport.alarm_events()[0]
assert payload["fields"] == {}
def test_publish_alarm_routing_key_format():
"""routing key must be alarm.critical.trex-core for default severity."""
emitter, transport = _make_emitter()
emitter.publish_alarm("kill_switch_fired", "routing key test")
rk, _ = transport.alarm_events()[0]
assert rk == "alarm.critical.trex-core"
# ------------------------------------------------------------------ domain purity sweep
def test_alarm_emitter_no_pika_import():
"""AlarmEmitter module must not import pika (domain/application purity)."""
import importlib
import sys
# Ensure pika is not in alarm_emitter's import chain at module level.
mod = sys.modules.get("trex_core.application.alarm_emitter")
if mod is None:
mod = importlib.import_module("trex_core.application.alarm_emitter")
# pika should NOT be in the module's global namespace
assert "pika" not in dir(mod), "alarm_emitter must not import pika directly"
def test_alarm_emitter_no_telegram_reference():
"""Verify no TELEGRAM_* references in alarm_emitter source (REQ-100 AC-5)."""
import inspect
import trex_core.application.alarm_emitter as ae_mod
source = inspect.getsource(ae_mod)
assert "TELEGRAM" not in source, "alarm_emitter must not reference TELEGRAM_*"
assert "telegram" not in source.lower(), "alarm_emitter must not reference telegram"