refactor: rewrite to CoinHunter V2 flat architecture
Replace the V1 commands/services split with a flat, direct architecture: - cli.py dispatches directly to service functions - New services: account, market, trade, opportunity - Thin Binance wrappers: spot_client, um_futures_client - Add audit logging, runtime paths, and TOML config - Remove legacy V1 code: commands/, precheck, review engine, smart executor - Add ruff + mypy toolchain and fix edge cases in trade params Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,58 +1,38 @@
|
||||
"""Tests for CLI routing and parser behavior."""
|
||||
"""CLI tests for CoinHunter V2."""
|
||||
|
||||
import pytest
|
||||
from __future__ import annotations
|
||||
|
||||
from coinhunter.cli import ALIASES, MODULE_MAP, build_parser, run_python_module
|
||||
import io
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from coinhunter import cli
|
||||
|
||||
|
||||
class TestAliases:
|
||||
def test_all_aliases_resolve_to_canonical(self):
|
||||
for alias, canonical in ALIASES.items():
|
||||
assert canonical in MODULE_MAP, f"alias {alias!r} points to missing canonical {canonical!r}"
|
||||
|
||||
def test_no_alias_is_itself_an_alias_loop(self):
|
||||
for alias in ALIASES:
|
||||
assert alias not in ALIASES.values() or alias in MODULE_MAP
|
||||
|
||||
|
||||
class TestModuleMap:
|
||||
def test_all_modules_exist(self):
|
||||
import importlib
|
||||
|
||||
for command, module_name in MODULE_MAP.items():
|
||||
full = f"coinhunter.{module_name}"
|
||||
mod = importlib.import_module(full)
|
||||
assert hasattr(mod, "main"), f"{full} missing main()"
|
||||
|
||||
|
||||
class TestBuildParser:
|
||||
def test_help_includes_commands(self):
|
||||
parser = build_parser()
|
||||
class CLITestCase(unittest.TestCase):
|
||||
def test_help_includes_v2_commands(self):
|
||||
parser = cli.build_parser()
|
||||
help_text = parser.format_help()
|
||||
assert "coinhunter diag" in help_text
|
||||
assert "coinhunter exec" in help_text
|
||||
self.assertIn("init", help_text)
|
||||
self.assertIn("account", help_text)
|
||||
self.assertIn("opportunity", help_text)
|
||||
|
||||
def test_parses_command_and_args(self):
|
||||
parser = build_parser()
|
||||
ns = parser.parse_args(["exec", "bal"])
|
||||
assert ns.command == "exec"
|
||||
assert "bal" in ns.args
|
||||
def test_init_dispatches(self):
|
||||
captured = {}
|
||||
with patch.object(cli, "ensure_init_files", return_value={"force": True, "root": "/tmp/ch"}), patch.object(
|
||||
cli, "print_json", side_effect=lambda payload: captured.setdefault("payload", payload)
|
||||
):
|
||||
result = cli.main(["init", "--force"])
|
||||
self.assertEqual(result, 0)
|
||||
self.assertTrue(captured["payload"]["force"])
|
||||
|
||||
def test_version_action_exits(self):
|
||||
parser = build_parser()
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
parser.parse_args(["--version"])
|
||||
assert exc.value.code == 0
|
||||
def test_old_command_is_rejected(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
cli.main(["exec", "bal"])
|
||||
|
||||
|
||||
class TestRunPythonModule:
|
||||
def test_runs_module_main_and_returns_int(self):
|
||||
result = run_python_module("commands.paths", [], "coinhunter paths")
|
||||
assert result == 0
|
||||
|
||||
def test_mutates_sys_argv(self):
|
||||
import sys
|
||||
|
||||
original = sys.argv[:]
|
||||
run_python_module("commands.paths", ["--help"], "coinhunter paths")
|
||||
assert sys.argv == original
|
||||
def test_runtime_error_is_rendered_cleanly(self):
|
||||
stderr = io.StringIO()
|
||||
with patch.object(cli, "load_config", side_effect=RuntimeError("boom")), patch("sys.stderr", stderr):
|
||||
result = cli.main(["market", "tickers", "BTCUSDT"])
|
||||
self.assertEqual(result, 1)
|
||||
self.assertIn("error: boom", stderr.getvalue())
|
||||
|
||||
Reference in New Issue
Block a user