"""Smoke-level orchestrator tests for the Kevin signal bridge.""" from decimal import Decimal from unittest.mock import AsyncMock, MagicMock from services.kevin_signal_bridge.main import KevinBridge async def test_bridge_dry_run_writes_audit_does_not_publish(): """When TRADING_KEVIN_ENABLE_TRADING=false the bridge writes an audit row and never calls publisher.publish.""" config = MagicMock(kevin_enable_trading=False, kevin_stop_loss_pct=0.08, kevin_take_profit_pct=0.20, kevin_avoid_blocks_days=7) cursor = AsyncMock() cursor.last_seen_id.return_value = 0 publisher = AsyncMock() aggregator = AsyncMock() aggregator.fetch_pending.return_value = [ MagicMock( id=1, symbol="NVDA", action=MagicMock(value="buy"), conviction=Decimal("0.8"), time_horizon=MagicMock(value="weeks"), ), ] strategy = AsyncMock() strategy.evaluate_mention.return_value = MagicMock( decision=MagicMock(value="open_long", name="OPEN_LONG"), symbol="NVDA", target_dollars=Decimal("3000"), holding_days=10, effective_conviction=Decimal("0.8"), rationale="ok", ) # Make the .decision attribute compare against KevinDecisionType.OPEN_LONG from shared.schemas.kevin import KevinDecisionType strategy.evaluate_mention.return_value.decision = KevinDecisionType.OPEN_LONG audit_writer = AsyncMock() broker = AsyncMock() broker.is_asset_tradable.return_value = True broker.get_latest_price.return_value = Decimal("100") broker.get_account.return_value = MagicMock( equity=Decimal("100000"), cash=Decimal("100000") ) broker.get_positions.return_value = [] bridge = KevinBridge( config=config, cursor=cursor, publisher=publisher, aggregator=aggregator, strategy=strategy, audit_writer=audit_writer, broker=broker, ) await bridge.process_one_pass() publisher.publish.assert_not_called() audit_writer.write.assert_awaited() args = audit_writer.write.call_args assert "dry_run" in str(args).lower() async def test_bridge_kill_switch_on_publishes_to_stream(): config = MagicMock( kevin_enable_trading=True, kevin_stop_loss_pct=0.08, kevin_take_profit_pct=0.20, kevin_avoid_blocks_days=7, ) cursor = AsyncMock() cursor.last_seen_id.return_value = 0 publisher = AsyncMock() publisher.publish.return_value = "1234-0" aggregator = AsyncMock() aggregator.fetch_pending.return_value = [ MagicMock( id=1, symbol="NVDA", action=MagicMock(value="buy"), conviction=Decimal("0.8"), effective_conviction=Decimal("0.8"), time_horizon=MagicMock(value="weeks"), ), ] strategy = AsyncMock() from shared.schemas.kevin import KevinDecisionType strategy.evaluate_mention.return_value = MagicMock( decision=KevinDecisionType.OPEN_LONG, symbol="NVDA", target_dollars=Decimal("3000"), holding_days=10, effective_conviction=Decimal("0.8"), rationale="ok", ) audit_writer = AsyncMock() broker = AsyncMock() broker.is_asset_tradable.return_value = True broker.get_latest_price.return_value = Decimal("100") broker.get_account.return_value = MagicMock( equity=Decimal("100000"), cash=Decimal("100000") ) broker.get_positions.return_value = [] bridge = KevinBridge( config=config, cursor=cursor, publisher=publisher, aggregator=aggregator, strategy=strategy, audit_writer=audit_writer, broker=broker, ) await bridge.process_one_pass() publisher.publish.assert_awaited_once() cursor.advance.assert_awaited_with(1) async def test_bridge_advances_cursor_only_after_publish(): """Race-condition guard: cursor must NOT advance if publish raises.""" config = MagicMock( kevin_enable_trading=True, kevin_stop_loss_pct=0.08, kevin_take_profit_pct=0.20, kevin_avoid_blocks_days=7, ) cursor = AsyncMock() cursor.last_seen_id.return_value = 0 publisher = AsyncMock() publisher.publish.side_effect = RuntimeError("XADD failed") aggregator = AsyncMock() aggregator.fetch_pending.return_value = [ MagicMock( id=1, symbol="NVDA", action=MagicMock(value="buy"), conviction=Decimal("0.8"), effective_conviction=Decimal("0.8"), time_horizon=MagicMock(value="weeks"), ), ] strategy = AsyncMock() from shared.schemas.kevin import KevinDecisionType strategy.evaluate_mention.return_value = MagicMock( decision=KevinDecisionType.OPEN_LONG, symbol="NVDA", target_dollars=Decimal("3000"), holding_days=10, effective_conviction=Decimal("0.8"), rationale="ok", ) audit_writer = AsyncMock() broker = AsyncMock() broker.is_asset_tradable.return_value = True broker.get_latest_price.return_value = Decimal("100") broker.get_account.return_value = MagicMock( equity=Decimal("100000"), cash=Decimal("100000") ) broker.get_positions.return_value = [] bridge = KevinBridge( config=config, cursor=cursor, publisher=publisher, aggregator=aggregator, strategy=strategy, audit_writer=audit_writer, broker=broker, ) await bridge.process_one_pass() # cursor.advance must NOT have been called for mention 1 cursor.advance.assert_not_called()