"""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 subprocess import uuid from datetime import UTC, date, datetime from pathlib import Path from trex_core.adapters.persistence.ops_checks import ( collect_release_provenance, find_ledger_gap_slots, isolate_dirty_checkout_files, ) _DAY = date(2026, 6, 10) _TS = datetime(2026, 6, 10, 6, 0, tzinfo=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=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} def _init_git_repo(tmp_path: Path) -> tuple[Path, str]: repo = tmp_path / "repo" repo.mkdir() subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True) subprocess.run(["git", "config", "user.name", "test"], cwd=repo, check=True, capture_output=True) subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repo, check=True, capture_output=True) (repo / "tracked.txt").write_text("v1\n", encoding="utf-8") subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True) subprocess.run(["git", "commit", "-m", "init"], cwd=repo, check=True, capture_output=True) head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip() return repo, head def test_collect_release_provenance_passes_on_clean_checkout(tmp_path): repo, head = _init_git_repo(tmp_path) report = collect_release_provenance( actor="operator-a", reason="deploy", instance="inst-a", command_class="manual-start", checkout_dir=repo, expected_oci_revision=head, image_digest="sha256:" + ("a" * 64), runtime_files=["tracked.txt"], capture_commands={}, broker_log_path=None, ) assert report["checkout"]["git_clean"] is True assert report["artifact_identity"]["oci_revision_matches_head"] is True assert report["runtime_file_verification"][0]["matches_expected_revision"] is True assert report["gate"]["passed"] is True def test_collect_release_provenance_blocks_on_revision_or_runtime_mismatch(tmp_path): repo, old_head = _init_git_repo(tmp_path) (repo / "tracked.txt").write_text("v2\n", encoding="utf-8") subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True) subprocess.run(["git", "commit", "-m", "v2"], cwd=repo, check=True, capture_output=True) report = collect_release_provenance( actor="operator-a", reason="deploy", instance="inst-a", command_class="manual-start", checkout_dir=repo, expected_oci_revision=old_head, runtime_files=["tracked.txt"], capture_commands={}, broker_log_path=None, ) assert report["checkout"]["git_clean"] is True assert report["artifact_identity"]["oci_revision_matches_head"] is False assert report["runtime_file_verification"][0]["matches_expected_revision"] is False assert report["gate"]["passed"] is False def test_isolate_dirty_checkout_files_copies_dirty_and_untracked(tmp_path): repo, _ = _init_git_repo(tmp_path) (repo / "tracked.txt").write_text("dirty\n", encoding="utf-8") (repo / "new.txt").write_text("new\n", encoding="utf-8") isolated = tmp_path / "isolated" manifest = isolate_dirty_checkout_files(checkout_dir=repo, isolate_dir=isolated) assert set(manifest["dirty_paths"]) == {"tracked.txt", "new.txt"} assert set(manifest["copied_paths"]) == {"tracked.txt", "new.txt"} assert (isolated / "tracked.txt").read_text(encoding="utf-8") == "dirty\n" assert (isolated / "new.txt").read_text(encoding="utf-8") == "new\n" assert (isolated / "manifest.json").is_file() def test_collect_release_provenance_rejects_runtime_path_escape(tmp_path): repo, head = _init_git_repo(tmp_path) report = collect_release_provenance( actor="operator-a", reason="deploy", instance="inst-a", command_class="manual-start", checkout_dir=repo, expected_oci_revision=head, runtime_files=["../outside.txt"], capture_commands={}, broker_log_path=None, ) runtime = report["runtime_file_verification"][0] assert runtime["path_within_checkout"] is False assert runtime["matches_expected_revision"] is False assert report["gate"]["passed"] is False