"""rejected SellIntent `retryable` flag (issue #27 / REQ-007 CHG-20260615-01). Additive: only on intent_type="sell" + status="rejected". Maps reason→retryable via the core-owned table (unrecognized → True, safety default; bad_request → False). admitted / processing / buy responses must NOT carry the field. """ from __future__ import annotations from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore from trex_core.application.intents import IntentService from trex_core.application.messaging import RpcRequest from trex_core.application.messaging_handlers import ( _sell_reject_retryable, make_intent_rpc_handler, ) from trex_core.domain.slot import SlotService, SlotState from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository INST = "default" def _wire(): repo = InMemorySlotRepository() svc = SlotService(repo, instance_key=INST) intents = IntentService(svc, repo, InMemoryIntentStore(), resolve_symbol=lambda pid: "005930", instance_key=INST, max_active_slots=5) repo.reconcile_universe(INST, {"PF-1": "SYM"}) handler = make_intent_rpc_handler(intents) slot = repo.list_slots(INST)[0] return repo, svc, intents, handler, slot def _sell(handler, slot_id, intent_id="s1"): return handler(RpcRequest(payload={ "intent_type": "sell", "intent_id": intent_id, "slot_id": str(slot_id), }, correlation_id=intent_id)) class TestRetryableMapping: def test_each_reason_maps(self): assert _sell_reject_retryable("slot_state_changed") is True assert _sell_reject_retryable("slot_not_holding:pending_buy") is True assert _sell_reject_retryable("duplicate_in_flight") is True assert _sell_reject_retryable("slot_not_found") is False assert _sell_reject_retryable("slot_not_holding:inactive") is False assert _sell_reject_retryable("slot_not_holding:pending_sell") is False assert _sell_reject_retryable("slot_not_holding:error") is False def test_unrecognized_defaults_true(self): assert _sell_reject_retryable("some_future_reason") is True assert _sell_reject_retryable(None) is True def test_bad_request_is_false(self): assert _sell_reject_retryable("bad_request:KeyError('slot_id')") is False class TestResponseField: def test_rejected_sell_inactive_slot_has_retryable_false(self): # slot is inactive (never bought) → slot_not_holding:inactive → terminal repo, svc, intents, handler, slot = _wire() resp = _sell(handler, slot.slot_id) assert resp.payload["status"] == "rejected" assert resp.payload["reason"] == "slot_not_holding:inactive" assert resp.payload["retryable"] is False def test_rejected_sell_unknown_slot_false(self): from uuid import uuid4 _, _, _, handler, _ = _wire() resp = _sell(handler, uuid4()) assert resp.payload["status"] == "rejected" assert resp.payload["reason"] == "slot_not_found" assert resp.payload["retryable"] is False def test_bad_request_sell_carries_retryable_false(self): _, _, _, handler, _ = _wire() resp = handler(RpcRequest(payload={ "intent_type": "sell", "intent_id": "x", # missing slot_id }, correlation_id="x")) assert resp.payload["status"] == "rejected" assert resp.payload["reason"].startswith("bad_request") assert resp.payload["retryable"] is False def test_admitted_sell_has_no_retryable(self): repo, svc, intents, handler, slot = _wire() # bring slot to holding so the sell is admitted svc.transit_slot(slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY, reason="buy", actor="t") svc.transit_slot(slot.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING, reason="filled", actor="t", qty=10, avg_price=100) resp = _sell(handler, slot.slot_id) assert resp.payload["status"] == "admitted" assert "retryable" not in resp.payload def test_buy_rejection_has_no_retryable(self): # buy on a non-existent slot → rejected, but buy responses omit retryable from uuid import uuid4 _, _, _, handler, _ = _wire() resp = handler(RpcRequest(payload={ "intent_type": "buy", "intent_id": "b1", "slot_id": str(uuid4()), }, correlation_id="b1")) assert resp.payload["status"] == "rejected" assert "retryable" not in resp.payload