test: cover unmanaged position lifecycle flow

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-12 13:27:12 +00:00
committed by GitHub
co-authored by genichin
parent fe51d211d9
commit f2b825e720
6 changed files with 45 additions and 42 deletions
@@ -32,7 +32,7 @@ class FakeBroker:
class RaisingBroker(FakeBroker):
def get_balance(self):
raise BrokerBalanceError("broker_unavailable")
raise BrokerBalanceError("transport_failed")
def _wire():
+1 -2
View File
@@ -96,7 +96,6 @@ def pg_clean(pg_dsn: str):
from trex_core.adapters.persistence.migrate import apply_migrations
apply_migrations(pg_dsn)
with psycopg.connect(pg_dsn, autocommit=True) as conn:
with conn.cursor() as cur:
with psycopg.connect(pg_dsn, autocommit=True) as conn, conn.cursor() as cur:
cur.execute(f"TRUNCATE {', '.join(_PG_TABLES)}")
return pg_dsn
+10 -7
View File
@@ -383,8 +383,11 @@ def test_sync_requires_yes_when_not_dry_run(capsys, monkeypatch):
"""sync without --yes (and not dry-run) should abort."""
def _fake_sync(args, cfg, confirm):
# Simulate the confirmation prompt returning False (no --yes, interactive aborts)
if not getattr(args, "dry_run", False) and not getattr(args, "yes", False):
if not confirm("Sync?"):
if (
not getattr(args, "dry_run", False)
and not getattr(args, "yes", False)
and not confirm("Sync?")
):
print("aborted")
return 1
return 0
@@ -398,7 +401,7 @@ def test_sync_live_requires_live_flag(capsys, monkeypatch):
monkeypatch.setattr(cli, "_broker_mode", lambda cfg: ("kis", True))
def _fake_sync(args, cfg, confirm):
broker, is_live = cli._broker_mode(cfg)
_broker, is_live = cli._broker_mode(cfg)
if is_live and not getattr(args, "live", False):
cli._fail("live account requires --live (with --yes) to confirm a real sync", args.json)
return 2
@@ -415,7 +418,7 @@ def test_sync_fail_closed_skipped_error_exits_nonzero_without_writes(
"""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
return SimpleNamespace(get_balance=list), None
class _WarningStore:
def clear_warned(self, *_args):
@@ -452,7 +455,7 @@ def test_sync_fail_closed_skipped_error_exits_nonzero_without_writes(
}
psycopg = types.ModuleType("psycopg")
setattr(psycopg, "connect", lambda *_args, **_kwargs: _Connection())
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())
@@ -707,10 +710,10 @@ def test_verify_cmd_broker_import_does_not_raise(monkeypatch):
# Patch at module level — _cmd_verify uses local `from ... import` so we
# must patch the source module, not the cli namespace.
import trex_core.adapters.persistence.postgresql as pg_mod
import trex_core.adapters.portfolio.reconcile_warning_store as rws_mod
from trex_core.application.portfolio import InMemoryReconcileWarningStore
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
import trex_core.adapters.portfolio.reconcile_warning_store as rws_mod
import trex_core.adapters.persistence.postgresql as pg_mod
monkeypatch.setattr(rws_mod, "PostgreSQLReconcileWarningStore",
lambda dsn: InMemoryReconcileWarningStore())
+20 -18
View File
@@ -12,8 +12,8 @@ from __future__ import annotations
import logging
import os
import threading
from datetime import date, datetime, time, timedelta, timezone
from typing import Callable, Optional
from collections.abc import Callable
from datetime import UTC, date, datetime, time, timedelta, timezone
from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.broker_port import BrokerPort
@@ -23,10 +23,10 @@ from trex_core.application.portfolio import (
PortfolioHubReadPort,
PortfolioHubUnavailableError,
ReconcileNotificationStore,
UnmanagedPositionStore,
ReconcileWarningStore,
SnapshotStore,
TradingContext,
UnmanagedPositionStore,
)
from trex_core.application.reconciliation import reconcile_positions
@@ -45,32 +45,32 @@ class SnapshotScheduler:
store: SnapshotStore,
broker: BrokerPort,
schedule_kst: time = time(hour=7, minute=30),
clock: Optional[Callable[[], datetime]] = None,
clock: Callable[[], datetime] | None = None,
slot_repo=None,
slot_service=None,
instance_key: str = "default",
alarm_emitter: Optional[AlarmEmitter] = None,
warn_store: Optional[ReconcileWarningStore] = None,
notification_store: Optional[ReconcileNotificationStore] = None,
unmanaged_position_store: Optional[UnmanagedPositionStore] = None,
alarm_emitter: AlarmEmitter | None = None,
warn_store: ReconcileWarningStore | None = None,
notification_store: ReconcileNotificationStore | None = None,
unmanaged_position_store: UnmanagedPositionStore | None = None,
credential_fetch_enabled: bool = False,
kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = None,
credential_refresh: Optional[Callable[[], object]] = None,
kis_reinit_callback: Callable[[BrokerCredentialPayload], None] | None = None,
credential_refresh: Callable[[], object] | None = None,
) -> None:
self._read = read_port
self._store = store
self._broker = broker
self._schedule_kst = schedule_kst
self._clock = clock or (lambda: datetime.now(tz=timezone.utc))
self._daemon_id: Optional[str] = None
self._last_fired_date: Optional[datetime] = None
self._clock = clock or (lambda: datetime.now(tz=UTC))
self._daemon_id: str | None = None
self._last_fired_date: datetime | None = None
# dcy-020 / REQ-058 (BAT-030): asset-centric slot reconcile after each
# universe fetch — always on when slot_repo is wired (the reconcile is
# what creates/retires the permanent asset slots).
self._slot_repo = slot_repo
self._slot_service = slot_service
self._instance_key = instance_key
self._last_universe_size: Optional[int] = None
self._last_universe_size: int | None = None
# dcy-033 / REQ-100: best-effort alarm emitter.
self._alarm_emitter = alarm_emitter
# BAT-050 / REQ-103: durable reconcile-warning store (Optional).
@@ -84,7 +84,9 @@ class SnapshotScheduler:
# dcy-036 / REQ-113 AC-2 / REQ-115 AC-1: optional callback to reinitialize
# KISBroker with the fetched day-start credential. None = no reinit (paper
# broker or credential_fetch_enabled=False). Set by daemon.py assembly.
self._kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = kis_reinit_callback
self._kis_reinit_callback: Callable[[BrokerCredentialPayload], None] | None = (
kis_reinit_callback
)
# REQ-119: provider-owned refresh supersedes the legacy raw fetch hook.
self._credential_refresh = credential_refresh
# REQ-113: detect break-glass at init time (env var set by operator).
@@ -93,8 +95,8 @@ class SnapshotScheduler:
in ("1", "true", "yes")
)
# REQ-115: day-scoped credential cache. None until first successful fetch.
self._cached_credential: Optional[BrokerCredentialPayload] = None
self._credential_date: Optional[date] = None
self._cached_credential: BrokerCredentialPayload | None = None
self._credential_date: date | None = None
# REQ-114: entry gate. True = new entries allowed (default).
# credential_fetch_enabled=False keeps this always True (no gate).
self._entry_allowed: bool = True
@@ -266,7 +268,7 @@ class SnapshotScheduler:
try:
self._credential_refresh()
self._entry_allowed = True
except Exception as exc: # provider exposes only sanitized failure
except Exception as exc: # noqa: BLE001 - provider exposes only sanitized failure
_log.error("DAY_START_LOAD_FAILED: broker credential refresh failed: %s (instance=%s)",
exc.__class__.__name__, self._instance_key)
self._entry_allowed = False
+6 -6
View File
@@ -737,12 +737,12 @@ def _build_parser() -> argparse.ArgumentParser:
def _cmd_sync(args, cfg, confirm) -> int:
"""REQ-105 (dcy-034, BAT-051): sync slot qty to broker ground truth."""
from trex_core.adapters.portfolio.reconcile_warning_store import (
PostgreSQLReconcileWarningStore,
)
from trex_core.adapters.portfolio.reconcile_notification_store import (
PostgreSQLReconcileNotificationStore,
)
from trex_core.adapters.portfolio.reconcile_warning_store import (
PostgreSQLReconcileWarningStore,
)
from trex_core.application.reconciliation import sync_slot_to_broker
if not args.portfolio_id and not args.all:
@@ -891,12 +891,12 @@ def _cmd_sync(args, cfg, confirm) -> int:
def _cmd_verify(args, cfg) -> int:
"""REQ-106 (dcy-034, BAT-051): re-check C-case reconcile-warning (read-centric)."""
from trex_core.adapters.persistence.postgresql import PostgreSQLSlotRepository
from trex_core.adapters.portfolio.reconcile_warning_store import (
PostgreSQLReconcileWarningStore,
)
from trex_core.adapters.portfolio.reconcile_notification_store import (
PostgreSQLReconcileNotificationStore,
)
from trex_core.adapters.portfolio.reconcile_warning_store import (
PostgreSQLReconcileWarningStore,
)
from trex_core.application.portfolio import SnapshotStore
from trex_core.application.reconciliation import verify_slot_reconcile
+4 -5
View File
@@ -31,11 +31,11 @@ from trex_core.adapters.messaging import (
RabbitMQTransport,
topology,
)
from trex_core.adapters.persistence.migrate import apply_migrations
from trex_core.adapters.persistence.account_snapshot import (
PostgreSQLAccountSnapshotStore,
)
from trex_core.adapters.persistence.ledger_skip_store import PostgreSQLLedgerSkipStore
from trex_core.adapters.persistence.migrate import apply_migrations
from trex_core.adapters.persistence.postgresql import (
PgStrategyStateStore,
PostgreSQLIntentStore,
@@ -55,16 +55,15 @@ from trex_core.adapters.portfolio.reconcile_notification_store import (
from trex_core.adapters.portfolio.unmanaged_position_store import (
PostgreSQLUnmanagedPositionStore,
)
from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.account_snapshot import AccountSnapshotService
from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.broker_port import BrokerPort
from trex_core.application.execution import ExecutionService
from trex_core.application.intents import IntentService
from trex_core.application.intents import BuyIntent
from trex_core.application.intents import BuyIntent, IntentService
from trex_core.application.killswitch import KillSwitchService
from trex_core.application.ledger_outbox import LedgerWriter
from trex_core.application.ledger_port import LedgerPort
from trex_core.application.messaging import MessagingTransport
from trex_core.application.killswitch import KillSwitchService
from trex_core.application.messaging_handlers import (
make_control_rpc_handler,
make_intent_rpc_handler,