Files
coinhunter-cli/tests/test_smart_executor_service.py
Tacit Lab 62c40a9776 refactor: address high-priority debt and publish to PyPI
- 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>
2026-04-16 01:21:27 +08:00

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")]