diff --git a/tests/runtime/test_cli_ledger_skips.py b/tests/runtime/test_cli_ledger_skips.py index b152735..523cbf6 100644 --- a/tests/runtime/test_cli_ledger_skips.py +++ b/tests/runtime/test_cli_ledger_skips.py @@ -91,7 +91,7 @@ def test_ledger_skips_list_outputs_records(monkeypatch, capsys): assert out["records"][0]["skip_reason"] == "missing_execution_id" -def test_ledger_skips_resolve_marks_record_and_emits_alert(monkeypatch, capsys): +def test_ledger_skips_resolve_marks_record_and_emits_alert(monkeypatch, tmp_path, capsys): class _Store: def __init__(self, *_args, **_kwargs): pass @@ -125,7 +125,7 @@ def test_ledger_skips_resolve_marks_record_and_emits_alert(monkeypatch, capsys): instance="inst-1", yes=True, live=False, - audit_dir="audit-dir", + audit_dir=str(tmp_path), config=None, config_path=None, env_file=None, @@ -172,3 +172,74 @@ def test_ledger_skips_resolve_live_requires_live_flag(monkeypatch, tmp_path, cap audit = json.loads(next(tmp_path.glob("*.json")).read_text(encoding="utf-8")) assert audit["request"]["instance"] == "inst-1" assert audit["result"]["status"] == "rejected" + assert audit["audit_path"].endswith(".json") + + +def test_ledger_skips_resolve_live_requires_yes_and_live(monkeypatch, tmp_path, capsys): + monkeypatch.setattr( + "trex_core.adapters.persistence.ledger_skip_store.PostgreSQLLedgerSkipStore", + lambda *_args, **_kwargs: object(), + ) + args = SimpleNamespace( + cmd="ledger", + ledger_cmd="skips", + ledger_skips_cmd="resolve", + skip_key="abc", + reason="operator_reconciled", + instance="inst-1", + yes=False, + live=True, + audit_dir=str(tmp_path), + config=None, + config_path=None, + env_file=None, + json=True, + timeout=10.0, + ) + + rc = cli._cmd_ledger_skips_resolve(args, _cfg(live=True), lambda _: True) + + assert rc == 2 + err = capsys.readouterr().err + assert "--yes --live" in err + audit = json.loads(next(tmp_path.glob("*.json")).read_text(encoding="utf-8")) + assert audit["result"]["reason"] == "live account requires --yes --live" + + +def test_ledger_skips_resolve_not_found_emits_audit_report(monkeypatch, tmp_path, capsys): + class _Store: + def __init__(self, *_args, **_kwargs): + pass + + def resolve_by_key(self, *_args, **_kwargs): + return None + + monkeypatch.setattr( + "trex_core.adapters.persistence.ledger_skip_store.PostgreSQLLedgerSkipStore", + _Store, + ) + args = SimpleNamespace( + cmd="ledger", + ledger_cmd="skips", + ledger_skips_cmd="resolve", + skip_key="missing", + reason="operator_reconciled", + instance="inst-1", + yes=True, + live=False, + audit_dir=str(tmp_path), + config=None, + config_path=None, + env_file=None, + json=False, + timeout=10.0, + ) + + rc = cli._cmd_ledger_skips_resolve(args, _cfg(), lambda _: True) + + assert rc == 1 + out = json.loads(capsys.readouterr().out) + assert out["result"]["status"] == "not_found" + assert out["audit_path"].endswith(".json") + audit = json.loads(next(tmp_path.glob("*.json")).read_text(encoding="utf-8")) + assert audit["audit_path"].endswith(".json") diff --git a/trex_core/adapters/persistence/ledger_skip_store.py b/trex_core/adapters/persistence/ledger_skip_store.py index 14fa399..4849293 100644 --- a/trex_core/adapters/persistence/ledger_skip_store.py +++ b/trex_core/adapters/persistence/ledger_skip_store.py @@ -41,8 +41,8 @@ class PostgreSQLLedgerSkipStore: now = datetime.now(tz=UTC) notify = False for _ in range(2): - with self._psycopg.connect(self._dsn, autocommit=False) as conn, conn.cursor() as cur: - try: + try: + with self._psycopg.connect(self._dsn, autocommit=False) as conn, conn.cursor() as cur: cur.execute( "INSERT INTO ledger_skip " "(skip_key, instance_key, slot_id, portfolio_id, symbol, order_id, " @@ -72,8 +72,8 @@ class PostgreSQLLedgerSkipStore: conn.commit() notify = True break - except self._psycopg.errors.UniqueViolation: - conn.rollback() + except self._psycopg.errors.UniqueViolation: + with self._psycopg.connect(self._dsn, autocommit=False) as conn, conn.cursor() as cur: cur.execute( "SELECT status FROM ledger_skip WHERE skip_key = %s FOR UPDATE", (skip_key,), diff --git a/trex_core/runtime/cli.py b/trex_core/runtime/cli.py index 7b276cc..90bc283 100644 --- a/trex_core/runtime/cli.py +++ b/trex_core/runtime/cli.py @@ -401,6 +401,7 @@ def _ledger_backfill_audit_path(args, execution_id: str) -> Path: def _write_ledger_backfill_audit(args, report: dict) -> str: path = _ledger_backfill_audit_path(args, report["request"]["execution_id"]) + report["audit_path"] = str(path) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(_json.dumps(report, ensure_ascii=False, indent=2, default=str) + "\n", encoding="utf-8") @@ -419,6 +420,7 @@ def _ledger_skip_resolve_audit_path(args, skip_key: str) -> Path: def _write_ledger_skip_resolve_audit(args, report: dict) -> str: path = _ledger_skip_resolve_audit_path(args, report["request"]["skip_key"]) + report["audit_path"] = str(path) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(_json.dumps(report, ensure_ascii=False, indent=2, default=str) + "\n", encoding="utf-8") @@ -1101,10 +1103,10 @@ def _cmd_ledger_skips_resolve(args, cfg, confirm) -> int: "record": None, "audit_path": None, } - if is_live and not getattr(args, "live", False): - report["result"] = {"status": "rejected", "reason": "live account requires --live (with --yes)"} + if is_live and (not getattr(args, "yes", False) or not getattr(args, "live", False)): + report["result"] = {"status": "rejected", "reason": "live account requires --yes --live"} report["audit_path"] = _write_ledger_skip_resolve_audit(args, report) - _fail("live account requires --live (with --yes)", args.json) + _fail("live account requires --yes --live", args.json) if args.json: _emit(report, as_json=True) else: @@ -1139,6 +1141,8 @@ def _cmd_ledger_skips_resolve(args, cfg, confirm) -> int: _fail(f"unresolved ledger skip not found: {args.skip_key}", args.json) if args.json: _emit(report, as_json=True) + else: + _emit_pretty_json(report) return 1 emitter, transport = _default_alarm_emitter(cfg) try: