feat(reconcile): safely seed canonical cost basis

This commit is contained in:
Hermes Agent
2026-07-17 09:57:08 +09:00
parent 18bd8d9f0b
commit 81a853a314
25 changed files with 1689 additions and 66 deletions
+73 -1
View File
@@ -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",