"""HTTP /v1 API tests via in-memory adapters (REQ-005/007/008). No PostgreSQL needed: the app is wired with the in-memory slot repo + intent store, exercising the full request → admission → transition → response path. """ from __future__ import annotations from datetime import date import pytest from fastapi.testclient import TestClient from trex_core.adapters.api.app import create_app from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore from trex_core.application.intents import IntentService from trex_core.domain.slot import SlotService, SlotState from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository DAY = date(2026, 6, 3) @pytest.fixture def ctx(): repo = InMemorySlotRepository() svc = SlotService(repo) intents = IntentService(svc, repo, InMemoryIntentStore()) svc.preallocate_daily_slots(DAY, 3) app = create_app( slot_service=svc, slot_repo=repo, intent_service=intents, default_total_slots=3 ) return repo, svc, intents, TestClient(app) def _buy(client, intent_id, symbol="005930"): return client.post( "/v1/intents/buy", json={"intent_id": intent_id, "trading_date": DAY.isoformat(), "symbol": symbol, "qty": 10}, ) def test_healthz(ctx): _, _, _, client = ctx r = client.get("/v1/healthz") assert r.status_code == 200 and r.json()["status"] == "ok" def test_buy_intent_admits_and_transitions(ctx): repo, _, _, client = ctx r = _buy(client, "buy-1") assert r.status_code == 200 body = r.json() assert body["status"] == "admitted" and body["slot_id"] slot = repo.fetch_slot(__import__("uuid").UUID(body["slot_id"])) assert slot.status is SlotState.PENDING_BUY and slot.symbol == "005930" def test_buy_intent_idempotent_resubmit(ctx): repo, _, _, client = ctx r1 = _buy(client, "buy-dup") r2 = _buy(client, "buy-dup") # same intent_id assert r1.json() == r2.json() # identical decision # only ONE slot moved out of idle pending = [s for s in repo.list_slots(DAY) if s.status is SlotState.PENDING_BUY] assert len(pending) == 1 def test_buy_intent_rejected_when_no_idle(ctx): _, _, _, client = ctx for i in range(3): assert _buy(client, f"buy-fill-{i}").json()["status"] == "admitted" r = _buy(client, "buy-overflow") assert r.json()["status"] == "rejected" and r.json()["reason"] == "no_idle_slot" def test_sell_intent_requires_holding(ctx): repo, _, _, client = ctx # a freshly bought slot is pending_buy, not holding -> sell rejected slot_id = _buy(client, "buy-for-sell").json()["slot_id"] r = client.post("/v1/intents/sell", json={"intent_id": "sell-1", "slot_id": slot_id}) assert r.json()["status"] == "rejected" and r.json()["reason"].startswith("slot_not_holding") def test_sell_intent_admits_from_holding(ctx): repo, svc, _, client = ctx import uuid slot_id = uuid.UUID(_buy(client, "buy-h").json()["slot_id"]) svc.transit_slot(slot_id, SlotState.PENDING_BUY, SlotState.HOLDING, reason="filled", actor="t") r = client.post("/v1/intents/sell", json={"intent_id": "sell-h", "slot_id": str(slot_id)}) assert r.json()["status"] == "admitted" assert repo.fetch_slot(slot_id).status is SlotState.PENDING_SELL def test_get_intent_roundtrip(ctx): _, _, _, client = ctx _buy(client, "buy-q") r = client.get("/v1/intents/buy-q") assert r.status_code == 200 and r.json()["status"] == "admitted" assert client.get("/v1/intents/does-not-exist").status_code == 404 def test_list_slots_and_capacity(ctx): _, _, _, client = ctx _buy(client, "buy-c") slots = client.get( "/v1/slots", params={"status": "pending_buy", "trading_date": DAY.isoformat()} ).json() assert len(slots) == 1 and slots[0]["status"] == "pending_buy" cap = client.get( "/v1/capacity", params={"total_slots": 3, "trading_date": DAY.isoformat()} ).json() assert cap["idle_count"] == 2 and cap["pending_count"] == 1 and cap["total_slots"] == 3