"""Ledger-gap operational check tests (REQ-055 / dcy-011 FR-5, issue #11). PostgreSQL-gated (TREX_POSTGRESQL_TEST_DSN). dcy-020 (BAT-030): the detector keys off slot_transition_log sell fills (the `closed` status no longer exists). Inserts sold slots + outbox rows directly and asserts find_ledger_gap_slots detects only the gaps. """ from __future__ import annotations import uuid from datetime import date, datetime, timezone from trex_core.adapters.persistence.ops_checks import find_ledger_gap_slots _DAY = date(2026, 6, 10) _TS = datetime(2026, 6, 10, 6, 0, tzinfo=timezone.utc) def _insert_slot(conn, *, status, portfolio_id, symbol="005930"): sid = str(uuid.uuid4()) with conn.cursor() as cur: cur.execute( "INSERT INTO daily_slot (slot_id, status, symbol, portfolio_id) " "VALUES (%s, %s, %s, %s)", (sid, status, symbol, portfolio_id), ) return sid def _insert_sell_fill(conn, slot_id, *, reason="sell_filled", ts=_TS): with conn.cursor() as cur: cur.execute( "INSERT INTO slot_transition_log (slot_id, from_state, to_state, ts, reason, actor) " "VALUES (%s, 'pending_sell', 'inactive', %s, %s, 'execution_loop')", (slot_id, ts, reason), ) def _insert_outbox(conn, *, asset_id, order_id="O1", execution_id="E1"): with conn.cursor() as cur: cur.execute( "INSERT INTO ledger_outbox (correlation_id, asset_id, order_id, execution_id, " "side, qty, price) VALUES (%s, %s, %s, %s, 'sell', 1, 1000)", (f"{asset_id}:{order_id}:{execution_id}", asset_id, order_id, execution_id), ) def test_detects_only_gap_slots(pg_clean): import psycopg dsn = pg_clean with psycopg.connect(dsn, autocommit=True) as conn: gap_null = _insert_slot(conn, status="inactive", portfolio_id=None) # gap (no pf) _insert_sell_fill(conn, gap_null) gap_no_outbox = _insert_slot(conn, status="inactive", portfolio_id="PF-GAP") _insert_sell_fill(conn, gap_no_outbox) # gap (no outbox row) ok = _insert_slot(conn, status="inactive", portfolio_id="PF-OK") # not a gap _insert_sell_fill(conn, ok) _insert_outbox(conn, asset_id="PF-OK") never_sold = _insert_slot(conn, status="holding", portfolio_id=None) # no sell fill → ignored gaps = find_ledger_gap_slots(dsn, trading_date=_DAY) gap_ids = {str(g["slot_id"]) for g in gaps} assert gap_null in gap_ids assert gap_no_outbox in gap_ids assert ok not in gap_ids assert never_sold not in gap_ids def test_no_gaps_when_all_recorded(pg_clean): import psycopg dsn = pg_clean with psycopg.connect(dsn, autocommit=True) as conn: sid = _insert_slot(conn, status="inactive", portfolio_id="PF-1") _insert_sell_fill(conn, sid) _insert_outbox(conn, asset_id="PF-1") assert find_ledger_gap_slots(dsn, trading_date=_DAY) == [] def test_date_scope_filters_by_sell_fill_day(pg_clean): import psycopg dsn = pg_clean other_day_ts = datetime(2026, 6, 9, 6, 0, tzinfo=timezone.utc) with psycopg.connect(dsn, autocommit=True) as conn: sid = _insert_slot(conn, status="inactive", portfolio_id="PF-OLD") _insert_sell_fill(conn, sid, ts=other_day_ts) assert find_ledger_gap_slots(dsn, trading_date=_DAY) == [] gaps = find_ledger_gap_slots(dsn, trading_date=date(2026, 6, 9)) assert {str(g["slot_id"]) for g in gaps} == {sid}