feat: add kill-switch control flow

This commit is contained in:
2026-06-07 14:00:20 +09:00
parent c912c08071
commit 7e23648489
30 changed files with 1424 additions and 51 deletions
@@ -0,0 +1,79 @@
"""Kill-switch day-scoped exclusion store tests (REQ-044 / BAT-012).
In-memory tests run always; PostgreSQL tests are gated on TREX_POSTGRESQL_TEST_DSN.
"""
from __future__ import annotations
from datetime import date
import pytest
from trex_core.application.portfolio import GLOBAL_KILLSWITCH, InMemoryKillSwitchStore
_D = date(2026, 6, 7)
_D_NEXT = date(2026, 6, 8)
class TestInMemoryKillSwitchStore:
def test_exclude_then_list(self):
s = InMemoryKillSwitchStore()
s.exclude("pf-A", _D)
assert "pf-A" in s.list_excluded(_D)
def test_day_scoped_auto_clear(self):
"""AC-4: next trading day does not include today's exclusion."""
s = InMemoryKillSwitchStore()
s.exclude("pf-A", _D)
assert "pf-A" not in s.list_excluded(_D_NEXT)
def test_idempotent_exclude(self):
s = InMemoryKillSwitchStore()
s.exclude("pf-A", _D)
s.exclude("pf-A", _D)
assert s.list_excluded(_D) == {"pf-A"}
def test_clear(self):
s = InMemoryKillSwitchStore()
s.exclude("pf-A", _D)
s.clear("pf-A", _D)
assert "pf-A" not in s.list_excluded(_D)
def test_global_sentinel(self):
"""AC-7: global exclusion stored under the '*' sentinel."""
s = InMemoryKillSwitchStore()
s.exclude(GLOBAL_KILLSWITCH, _D)
assert GLOBAL_KILLSWITCH in s.list_excluded(_D)
@pytest.fixture
def pg_killswitch_store(pg_clean):
from trex_core.adapters.portfolio.killswitch_store import PostgreSQLKillSwitchStore
return PostgreSQLKillSwitchStore(pg_clean)
class TestPostgreSQLKillSwitchStore:
def test_schema_and_round_trip(self, pg_killswitch_store):
"""AC-1/3: table exists; exclude→list_excluded round-trips."""
pg_killswitch_store.exclude("pf-A", _D, actor="op")
assert pg_killswitch_store.list_excluded(_D) == {"pf-A"}
def test_day_scoped(self, pg_killswitch_store):
"""AC-4: list_excluded(next day) excludes today's row."""
pg_killswitch_store.exclude("pf-A", _D)
assert pg_killswitch_store.list_excluded(_D_NEXT) == set()
def test_idempotent(self, pg_killswitch_store):
"""AC-5: ON CONFLICT DO NOTHING — duplicate exclude is a no-op."""
pg_killswitch_store.exclude("pf-A", _D)
pg_killswitch_store.exclude("pf-A", _D)
assert pg_killswitch_store.list_excluded(_D) == {"pf-A"}
def test_clear(self, pg_killswitch_store):
"""AC-6: clear removes the row."""
pg_killswitch_store.exclude("pf-A", _D)
pg_killswitch_store.clear("pf-A", _D)
assert pg_killswitch_store.list_excluded(_D) == set()
def test_global_sentinel(self, pg_killswitch_store):
pg_killswitch_store.exclude(GLOBAL_KILLSWITCH, _D)
assert GLOBAL_KILLSWITCH in pg_killswitch_store.list_excluded(_D)