- Fix TOCTOU race conditions by wrapping read-modify-write cycles under single-file locks in execution_state, portfolio_service, precheck_state, state_manager, and precheck_service. - Add missing test coverage (96 tests total): - test_review_service.py (15 tests) - test_check_api.py (6 tests) - test_external_gate.py main branches (+10 tests) - test_trade_execution.py new commands (+8 tests) - Unify all agent-consumed JSON messages to English. - Config-ize hardcoded values (volume filter, schema_version) via get_user_config with sensible defaults. - Add 1-hour TTL to exchange cache with force_new override. - Add ruff and mypy to dev dependencies; fix all type errors. - Add __all__ declarations to 11 service modules. - Sync README with new commands, config tuning docs, and PyPI badge. - Publish package as coinhunter==1.0.0 on PyPI with MIT license. - Deprecate coinhunter-cli==1.0.1 with runtime warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
99 lines
4.5 KiB
Python
99 lines
4.5 KiB
Python
"""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")]
|