feat(reconcile): safely seed canonical cost basis
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -405,6 +408,76 @@ def test_sync_live_requires_live_flag(capsys, monkeypatch):
|
||||
assert rc == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("as_json", [False, True])
|
||||
def test_sync_fail_closed_skipped_error_exits_nonzero_without_writes(
|
||||
monkeypatch, capsys, as_json,
|
||||
):
|
||||
"""REQ-123: a fail-closed skipped live sync is a CLI failure."""
|
||||
class _Provider:
|
||||
def get_broker_for_read(self):
|
||||
return SimpleNamespace(get_balance=lambda: []), None
|
||||
|
||||
class _WarningStore:
|
||||
def clear_warned(self, *_args):
|
||||
raise AssertionError("warning clear must not be called")
|
||||
|
||||
class _Connection:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
def cursor(self):
|
||||
return self
|
||||
|
||||
def execute(self, *_args):
|
||||
return None
|
||||
|
||||
def fetchone(self):
|
||||
return ("slot-1",)
|
||||
|
||||
sync_calls = []
|
||||
|
||||
def _fail_closed_sync(**kwargs):
|
||||
sync_calls.append(kwargs)
|
||||
return {
|
||||
"slot_id": "slot-1", "symbol": "005930", "portfolio_id": "PF-1",
|
||||
"before": {"status": "inactive", "qty": None, "avg_price": None},
|
||||
"after": {"status": "inactive", "qty": None, "avg_price": None},
|
||||
"warning_cleared": False, "dry_run": False, "skipped": True,
|
||||
"skip_reason": "cost_basis_unavailable", "seeded_avg_price": False,
|
||||
"cost_basis_source": "none", "cost_basis_reason": "transport",
|
||||
"error": "cost_basis_unavailable",
|
||||
}
|
||||
|
||||
psycopg = types.ModuleType("psycopg")
|
||||
setattr(psycopg, "connect", lambda *_args, **_kwargs: _Connection())
|
||||
monkeypatch.setitem(sys.modules, "psycopg", psycopg)
|
||||
monkeypatch.setattr("trex_core.runtime.broker_provider.build_broker_provider", lambda _cfg: _Provider())
|
||||
monkeypatch.setattr("trex_core.adapters.portfolio.reconcile_warning_store.PostgreSQLReconcileWarningStore", lambda *_args: _WarningStore())
|
||||
monkeypatch.setattr("trex_core.adapters.persistence.postgresql.PostgreSQLSlotRepository", lambda *_args: object())
|
||||
monkeypatch.setattr("trex_core.domain.slot.service.SlotService", lambda *_args: object())
|
||||
monkeypatch.setattr("trex_core.application.reconciliation.sync_slot_to_broker", _fail_closed_sync)
|
||||
|
||||
args = SimpleNamespace(portfolio_id="PF-1", all=False, json=as_json, dry_run=False,
|
||||
yes=True, live=False, instance=None, config=None, config_path=None)
|
||||
rc = cli._cmd_sync(args, SimpleNamespace(dsn="dsn", broker="paper", kis=None, portfolio_hub=None), lambda _: True)
|
||||
|
||||
assert rc == 1
|
||||
assert len(sync_calls) == 1
|
||||
assert sync_calls[0]["dry_run"] is False
|
||||
captured = capsys.readouterr()
|
||||
if as_json:
|
||||
result = json.loads(captured.out)["results"][0]
|
||||
assert result["error"] == "cost_basis_unavailable"
|
||||
assert result["skipped"] is True
|
||||
else:
|
||||
assert "SKIP" in captured.out
|
||||
assert "error=cost_basis_unavailable" in captured.out
|
||||
assert "CLEARED" not in captured.out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# verify parser tests (REQ-106 / BAT-051) — parser-level only
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -455,7 +528,6 @@ def test_verify_cmd_broker_import_does_not_raise(monkeypatch):
|
||||
Paper broker config is used (no real network needed).
|
||||
"""
|
||||
from trex_core.runtime.config import DaemonConfig
|
||||
from trex_core.adapters.broker.paper import PaperBroker
|
||||
|
||||
cfg = DaemonConfig(
|
||||
dsn="postgresql://x/y",
|
||||
|
||||
Reference in New Issue
Block a user