feat: add CLI entry point with argparse (#55)
Implement the main CLI entry point for the Polymarket Insider Tracker. Features: - argparse-based CLI with version, config-check, dry-run, log-level flags - Structured logging configuration with multiple formatters - Configuration validation with friendly error messages - Startup banner display - Config check mode for validation without running - Integration with Pipeline orchestrator Tests: 20 new tests covering parser, logging, banner, config validation, and integration scenarios. Closes #55 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
bf7b972edb
commit
5cf6382e42
@@ -0,0 +1,214 @@
|
||||
"""Tests for the CLI entry point."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from polymarket_insider_tracker.__main__ import (
|
||||
EXIT_CONFIG_ERROR,
|
||||
EXIT_SUCCESS,
|
||||
configure_logging,
|
||||
create_parser,
|
||||
main,
|
||||
print_banner,
|
||||
run_config_check,
|
||||
validate_config,
|
||||
)
|
||||
|
||||
|
||||
class TestCreateParser:
|
||||
"""Tests for argument parser creation."""
|
||||
|
||||
def test_parser_has_version(self):
|
||||
"""Parser should have version flag."""
|
||||
parser = create_parser()
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
parser.parse_args(["--version"])
|
||||
assert exc_info.value.code == 0
|
||||
|
||||
def test_parser_config_check(self):
|
||||
"""Parser should accept --config-check flag."""
|
||||
parser = create_parser()
|
||||
args = parser.parse_args(["--config-check"])
|
||||
assert args.config_check is True
|
||||
|
||||
def test_parser_log_level(self):
|
||||
"""Parser should accept --log-level option."""
|
||||
parser = create_parser()
|
||||
args = parser.parse_args(["--log-level", "DEBUG"])
|
||||
assert args.log_level == "DEBUG"
|
||||
|
||||
def test_parser_dry_run(self):
|
||||
"""Parser should accept --dry-run flag."""
|
||||
parser = create_parser()
|
||||
args = parser.parse_args(["--dry-run"])
|
||||
assert args.dry_run is True
|
||||
|
||||
def test_parser_health_port(self):
|
||||
"""Parser should accept --health-port option."""
|
||||
parser = create_parser()
|
||||
args = parser.parse_args(["--health-port", "9090"])
|
||||
assert args.health_port == 9090
|
||||
|
||||
def test_parser_default_values(self):
|
||||
"""Parser should have correct defaults."""
|
||||
parser = create_parser()
|
||||
args = parser.parse_args([])
|
||||
assert args.config_check is False
|
||||
assert args.log_level is None
|
||||
assert args.dry_run is False
|
||||
assert args.health_port is None
|
||||
|
||||
|
||||
class TestConfigureLogging:
|
||||
"""Tests for logging configuration."""
|
||||
|
||||
def test_configure_logging_info(self):
|
||||
"""Should configure logging at INFO level."""
|
||||
configure_logging("INFO")
|
||||
import logging
|
||||
|
||||
assert logging.getLogger().level == logging.INFO
|
||||
|
||||
def test_configure_logging_debug(self):
|
||||
"""Should configure logging at DEBUG level."""
|
||||
configure_logging("DEBUG")
|
||||
import logging
|
||||
|
||||
assert logging.getLogger().level == logging.DEBUG
|
||||
|
||||
|
||||
class TestPrintBanner:
|
||||
"""Tests for banner printing."""
|
||||
|
||||
def test_banner_contains_app_name(self, capsys):
|
||||
"""Banner should contain application name."""
|
||||
print_banner()
|
||||
captured = capsys.readouterr()
|
||||
assert "Polymarket Insider Tracker" in captured.out
|
||||
|
||||
def test_banner_contains_version(self, capsys):
|
||||
"""Banner should contain version."""
|
||||
print_banner()
|
||||
captured = capsys.readouterr()
|
||||
assert "v0.1.0" in captured.out
|
||||
|
||||
|
||||
class TestValidateConfig:
|
||||
"""Tests for configuration validation."""
|
||||
|
||||
def test_validate_config_success(self, monkeypatch):
|
||||
"""Should return settings on valid config."""
|
||||
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
|
||||
|
||||
settings = validate_config()
|
||||
assert settings is not None
|
||||
|
||||
def test_validate_config_failure(self, monkeypatch, capsys):
|
||||
"""Should return None on invalid config."""
|
||||
# Clear any existing DATABASE_URL
|
||||
monkeypatch.delenv("DATABASE_URL", raising=False)
|
||||
|
||||
settings = validate_config()
|
||||
assert settings is None
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Configuration validation failed" in captured.err
|
||||
|
||||
|
||||
class TestRunConfigCheck:
|
||||
"""Tests for config check mode."""
|
||||
|
||||
def test_config_check_prints_summary(self, monkeypatch, capsys):
|
||||
"""Config check should print configuration summary."""
|
||||
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
|
||||
|
||||
settings = validate_config()
|
||||
assert settings is not None
|
||||
|
||||
result = run_config_check(settings)
|
||||
assert result == EXIT_SUCCESS
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Configuration is valid!" in captured.out
|
||||
assert "Configuration:" in captured.out
|
||||
|
||||
|
||||
class TestMain:
|
||||
"""Tests for main entry point."""
|
||||
|
||||
def test_main_with_config_check(self, monkeypatch):
|
||||
"""Main should exit successfully with --config-check."""
|
||||
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main(["--config-check"])
|
||||
|
||||
assert exc_info.value.code == EXIT_SUCCESS
|
||||
|
||||
def test_main_with_invalid_config(self, monkeypatch):
|
||||
"""Main should exit with config error on invalid config."""
|
||||
monkeypatch.delenv("DATABASE_URL", raising=False)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main([])
|
||||
|
||||
assert exc_info.value.code == EXIT_CONFIG_ERROR
|
||||
|
||||
def test_main_with_dry_run_and_config_check(self, monkeypatch):
|
||||
"""Main should handle dry-run with config-check."""
|
||||
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main(["--config-check", "--dry-run"])
|
||||
|
||||
assert exc_info.value.code == EXIT_SUCCESS
|
||||
|
||||
@patch("polymarket_insider_tracker.__main__.run_pipeline")
|
||||
@patch("polymarket_insider_tracker.__main__.asyncio.run")
|
||||
def test_main_runs_pipeline(self, mock_asyncio_run, _mock_run_pipeline, monkeypatch):
|
||||
"""Main should run pipeline when not in config-check mode."""
|
||||
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
|
||||
mock_asyncio_run.return_value = EXIT_SUCCESS
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main([])
|
||||
|
||||
assert exc_info.value.code == EXIT_SUCCESS
|
||||
mock_asyncio_run.assert_called_once()
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests for CLI invocation."""
|
||||
|
||||
def test_cli_help_option(self, capsys):
|
||||
"""CLI should display help with -h option."""
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main(["-h"])
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "polymarket-insider-tracker" in captured.out
|
||||
assert "--config-check" in captured.out
|
||||
assert "--dry-run" in captured.out
|
||||
assert "--log-level" in captured.out
|
||||
|
||||
def test_cli_version_option(self, capsys):
|
||||
"""CLI should display version with --version option."""
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main(["--version"])
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "0.1.0" in captured.out
|
||||
|
||||
def test_cli_invalid_log_level(self, capsys):
|
||||
"""CLI should reject invalid log level."""
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main(["--log-level", "INVALID"])
|
||||
|
||||
assert exc_info.value.code != 0
|
||||
captured = capsys.readouterr()
|
||||
assert "invalid choice" in captured.err
|
||||
Reference in New Issue
Block a user