"""Tests for smart executor deduplication and routing.""" from coinhunter.services import exchange_service from coinhunter.services import smart_executor_service as ses class FakeArgs: def __init__(self, command="buy", symbol="BTCUSDT", amount_usdt=50, dry_run=False, decision_id=None, analysis=None, reasoning=None, order_id=None, from_symbol=None, to_symbol=None): self.command = command self.symbol = symbol self.amount_usdt = amount_usdt self.dry_run = dry_run self.decision_id = decision_id self.analysis = analysis self.reasoning = reasoning self.order_id = order_id self.from_symbol = from_symbol self.to_symbol = to_symbol class TestDeduplication: def test_skips_duplicate_mutating_action(self, monkeypatch, capsys): monkeypatch.setattr(ses, "parse_cli_args", lambda argv: (FakeArgs(command="buy"), ["BTCUSDT", "50"])) monkeypatch.setattr(ses, "get_execution_state", lambda did: {"status": "success"}) get_exchange_calls = [] monkeypatch.setattr(exchange_service, "get_exchange", lambda: get_exchange_calls.append(1) or "ex") result = ses.run(["buy", "BTCUSDT", "50"]) assert result == 0 assert get_exchange_calls == [] captured = capsys.readouterr() assert "already executed successfully" in captured.err def test_allows_duplicate_read_only_action(self, monkeypatch): monkeypatch.setattr(ses, "parse_cli_args", lambda argv: (FakeArgs(command="balances"), ["balances"])) monkeypatch.setattr(ses, "get_execution_state", lambda did: {"status": "success"}) monkeypatch.setattr(ses, "command_balances", lambda ex: None) get_exchange_calls = [] monkeypatch.setattr(exchange_service, "get_exchange", lambda: get_exchange_calls.append(1) or "ex") result = ses.run(["bal"]) assert result == 0 assert len(get_exchange_calls) == 1 def test_allows_retry_after_failure(self, monkeypatch): monkeypatch.setattr(ses, "parse_cli_args", lambda argv: (FakeArgs(command="buy"), ["BTCUSDT", "50"])) monkeypatch.setattr(ses, "get_execution_state", lambda did: {"status": "failed"}) monkeypatch.setattr(ses, "record_execution_state", lambda did, state: None) monkeypatch.setattr(ses, "build_decision_context", lambda ex, action, tail, did: {}) monkeypatch.setattr(ses, "action_buy", lambda *a, **k: {"id": "123"}) monkeypatch.setattr(ses, "print_json", lambda d: None) get_exchange_calls = [] monkeypatch.setattr(exchange_service, "get_exchange", lambda: get_exchange_calls.append(1) or "ex") result = ses.run(["buy", "BTCUSDT", "50"]) assert result == 0 assert len(get_exchange_calls) == 1 class TestReadOnlyRouting: def test_routes_balances(self, monkeypatch): monkeypatch.setattr(ses, "parse_cli_args", lambda argv: (FakeArgs(command="balances"), ["balances"])) monkeypatch.setattr(ses, "get_execution_state", lambda did: None) routed = [] monkeypatch.setattr(ses, "command_balances", lambda ex: routed.append("balances")) monkeypatch.setattr(exchange_service, "get_exchange", lambda: "ex") result = ses.run(["balances"]) assert result == 0 assert routed == ["balances"] def test_routes_orders(self, monkeypatch): monkeypatch.setattr(ses, "parse_cli_args", lambda argv: (FakeArgs(command="orders"), ["orders"])) monkeypatch.setattr(ses, "get_execution_state", lambda did: None) routed = [] monkeypatch.setattr(ses, "command_orders", lambda ex: routed.append("orders")) monkeypatch.setattr(exchange_service, "get_exchange", lambda: "ex") result = ses.run(["orders"]) assert result == 0 assert routed == ["orders"] def test_routes_order_status(self, monkeypatch): monkeypatch.setattr(ses, "parse_cli_args", lambda argv: ( FakeArgs(command="order-status", symbol="BTCUSDT", order_id="123"), ["order-status", "BTCUSDT", "123"] )) monkeypatch.setattr(ses, "get_execution_state", lambda did: None) routed = [] monkeypatch.setattr(ses, "command_order_status", lambda ex, sym, oid: routed.append((sym, oid))) monkeypatch.setattr(exchange_service, "get_exchange", lambda: "ex") result = ses.run(["order-status", "BTCUSDT", "123"]) assert result == 0 assert routed == [("BTCUSDT", "123")]