feat: complete Faza 1 implementation (105 tests green)
All 12 modules built per reviewed plan: - detector, state_machine (5-state phased FSM), canary, levels Phase B - notifier fanout (Discord + Telegram, bounded queue, retry, dead-letter) - audit (JSONL daily rotation), journal, report (weekly R-multiple PnL) - calibrate + labeler (Tk, lazy-imported), dryrun with acceptance gate - unified CLI: atm calibrate|label|dryrun|run|journal|report README + Phase 2 prop-firm TOS audit checklist included. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
137
tests/test_main.py
Normal file
137
tests/test_main.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Tests for atm.main unified CLI."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import types
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
SUBCOMMANDS = ["calibrate", "label", "dryrun", "run", "journal", "report"]
|
||||
|
||||
# Ensure subprocess invocations find the atm package even without pip install
|
||||
_SRC = str(Path(__file__).resolve().parent.parent / "src")
|
||||
_SUBPROCESS_ENV = {**os.environ, "PYTHONPATH": _SRC}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _mock_config_class(cfg=None):
|
||||
"""Return a Config-like class whose load_current() returns *cfg*."""
|
||||
if cfg is None:
|
||||
cfg = MagicMock()
|
||||
mock_cls = MagicMock()
|
||||
mock_cls.load_current.return_value = cfg
|
||||
return mock_cls
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# test_help_works
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_help_works():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "atm", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=_SUBPROCESS_ENV,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# test_subcommands_listed
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_subcommands_listed():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "atm", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=_SUBPROCESS_ENV,
|
||||
)
|
||||
output = result.stdout
|
||||
for cmd in SUBCOMMANDS:
|
||||
assert cmd in output, f"Expected subcommand '{cmd}' in --help output"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# test_dryrun_wiring
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class _DryrunResult:
|
||||
acceptance_pass: bool
|
||||
|
||||
|
||||
def _make_dryrun_module(acceptance_pass: bool):
|
||||
mod = types.ModuleType("atm.dryrun")
|
||||
mod.dryrun = lambda *a, **kw: _DryrunResult(acceptance_pass=acceptance_pass)
|
||||
mod.print_report = lambda r: None
|
||||
return mod
|
||||
|
||||
|
||||
def test_dryrun_wiring_pass(monkeypatch, tmp_path):
|
||||
import atm.main as _main
|
||||
|
||||
monkeypatch.setattr("atm.main.dryrun", _make_dryrun_module(acceptance_pass=True))
|
||||
monkeypatch.setattr("atm.main.Config", _mock_config_class())
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
_main.main(["dryrun", str(tmp_path)])
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
|
||||
|
||||
def test_dryrun_wiring_fail(monkeypatch, tmp_path):
|
||||
import atm.main as _main
|
||||
|
||||
monkeypatch.setattr("atm.main.dryrun", _make_dryrun_module(acceptance_pass=False))
|
||||
monkeypatch.setattr("atm.main.Config", _mock_config_class())
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
_main.main(["dryrun", str(tmp_path)])
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# test_report_current_week_default
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_report_current_week_default(monkeypatch, tmp_path):
|
||||
import atm.main as _main
|
||||
|
||||
# Journal.all returns no entries — report should print a zero-trade week
|
||||
monkeypatch.setattr("atm.journal.Journal.all", lambda self: [])
|
||||
|
||||
# Should not raise; no sys.exit expected
|
||||
_main.main(["report", "--file", str(tmp_path / "trades.jsonl")])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# test_run_live_dry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_run_live_dry(monkeypatch):
|
||||
import atm.main as _main
|
||||
|
||||
calls: list[dict] = []
|
||||
|
||||
def _mock_run_live(cfg, duration_s=None, capture_stub=False):
|
||||
calls.append({"cfg": cfg, "duration_s": duration_s, "capture_stub": capture_stub})
|
||||
|
||||
monkeypatch.setattr("atm.main.run_live", _mock_run_live)
|
||||
monkeypatch.setattr("atm.main.Config", _mock_config_class())
|
||||
|
||||
_main.main(["run", "--duration", "0"])
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0]["duration_s"] == pytest.approx(0.0)
|
||||
Reference in New Issue
Block a user