feat(slot): BAT-027 M2/M3 — 상태머신 inactive 재정의 + 인스턴스 스코프 advisory lock (dcy-020/REQ-057)

안 B 점진의 2/3단계. runtime 무변경(idle/closed 별칭 흡수)으로 cascade 통제.

- state.py: SlotState.INACTIVE 추가 + idle/closed 별칭 normalize_state(); 전이 집합
  inactive 기준 재정의(inactive↔pending_buy↔holding↔pending_sell), closed→idle 제거;
  is_allowed_transition 양쪽 정규화; slot_advisory_lock_id(instance_key) 헬퍼
- service.py: SlotService(instance_key) + advisory lock 인스턴스 스코프(self._lock_id)
- __init__: normalize_state/slot_advisory_lock_id export; daemon: instance_key 전달
- 테스트 갱신: test_state(전이/멤버7/락/size12), test_service(closed→idle 제거→reallocation),
  test_slot_entry_count, test_postgresql_repository(closed→idle 거부+reallocation)

검증: 단위 375 passed / 35 skipped, PG-gated(trex_core_test) 8 passed
(마이그레이션 009 드리프트 + lifecycle/reallocation), ruff clean.

REQ-056/057 은 토대만 충족 → Approved 유지(entry_count/trading_date drop·literal
inactive 전면 전환은 BAT-δ; confirm-req-implemented 는 dcy-020 후속 배치 완료 후).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 10:53:39 +09:00
co-authored by Claude Opus 4.8
parent 86c892aa0c
commit 017b8185e0
10 changed files with 232 additions and 151 deletions
@@ -3,6 +3,8 @@ from __future__ import annotations
from datetime import date
import pytest
from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore # noqa: F401
from trex_core.adapters.persistence.postgresql import (
PostgreSQLIntentStore,
@@ -10,6 +12,7 @@ from trex_core.adapters.persistence.postgresql import (
)
from trex_core.application.intents import BuyIntent, IntentService, SellIntent
from trex_core.domain.slot import SlotService, SlotState
from trex_core.domain.slot.errors import SlotTransitionError
DAY = date(2026, 6, 3)
@@ -48,7 +51,9 @@ def test_buy_intent_idempotent_on_pg(pg_clean):
assert len(pending) == 1 # no double transition
def test_full_lifecycle_recovery_bumps_entry_count(pg_clean):
def test_full_lifecycle_then_closed_reallocation_bumps_entry_count(pg_clean):
# dcy-020: closed→idle recovery removed. Reuse a closed slot via
# closed→pending_buy reallocation (dcy-019); entry_count is cumulative.
repo, svc, intents = _wire(pg_clean)
svc.preallocate_daily_slots(DAY, 1)
d = intents.submit_buy(BuyIntent(intent_id="pg-life", trading_date=DAY, symbol="005930", portfolio_id="PF-1"))
@@ -56,6 +61,11 @@ def test_full_lifecycle_recovery_bumps_entry_count(pg_clean):
svc.transit_slot(sid, SlotState.PENDING_BUY, SlotState.HOLDING, reason="filled", actor="t")
intents.submit_sell(SellIntent(intent_id="pg-sell", slot_id=sid))
svc.transit_slot(sid, SlotState.PENDING_SELL, SlotState.CLOSED, reason="filled", actor="t")
assert repo.get_symbol_entry_count(DAY, "005930") == 0
svc.transit_slot(sid, SlotState.CLOSED, SlotState.IDLE, reason="recovery", actor="t")
assert repo.get_symbol_entry_count(DAY, "005930") == 1
assert repo.fetch_slot(sid).entry_count == 1
# closed→idle is now rejected
with pytest.raises(SlotTransitionError):
svc.transit_slot(sid, SlotState.CLOSED, SlotState.IDLE, reason="recovery", actor="t")
# reuse via reallocation
realloc = svc.transit_slot(sid, SlotState.CLOSED, SlotState.PENDING_BUY,
reason="reallocate", actor="t", symbol="000660")
assert realloc.entry_count == 2 and realloc.symbol == "000660"