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:
2026-04-16 17:22:29 +08:00
parent 3819e35a7b
commit 52cd76a750
78 changed files with 2023 additions and 5407 deletions

View File

@@ -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())