import json from datetime import date from decimal import Decimal import pytest from zhixing_server.modules.sector_radar.application.build import ( BuildDateOutcome, BuildOutcomeStatus, BuildSummary, ) from zhixing_server.modules.sector_radar.presentation import cli from zhixing_server.modules.sector_radar.presentation.cli import build_parser def test_sector_radar_cli_parses_single_range_and_retry_modes() -> None: parser = build_parser() single = parser.parse_args(["--trade-date", "2026-08-28"]) date_range = parser.parse_args(["--start-date", "2026-08-18", "--end-date", "2026-08-28"]) retry = parser.parse_args(["--retry-publication-id", "publication-a"]) assert single.trade_date == date(2026, 8, 28) assert date_range.start_date == date(2026, 8, 18) assert date_range.end_date == date(2026, 8, 28) assert retry.retry_publication_id == "publication-a" class FakeSettings: log_level = "INFO" tushare_token = "secret-token" database_url = "postgresql://unused" sector_radar_max_retries = 3 sector_radar_retry_backoff_seconds = 1.0 sector_radar_request_interval_seconds = 0.2 sector_radar_advisory_lock_key = 7_380_522 sector_radar_coverage_threshold = Decimal("0.99") class FakeRepository: closed = False def __init__(self, database_url: str, *, advisory_lock_key: int) -> None: assert database_url == "postgresql://unused" assert advisory_lock_key == 7_380_522 def close(self) -> None: self.closed = True @pytest.mark.parametrize( ("outcome_status", "coverage", "expected_code"), (("success", Decimal(1), 0), ("partial", Decimal("0.8"), 2), ("failed", Decimal(0), 1)), ) def test_cli_main_returns_summary_exit_code_and_json( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], outcome_status: BuildOutcomeStatus, coverage: Decimal, expected_code: int, ) -> None: summary = BuildSummary( ( BuildDateOutcome( date(2026, 8, 28), outcome_status, "publication-a", coverage, 2, 6, ), ) ) class FakeSourceFactory: @staticmethod def from_token(token: str, **kwargs: object) -> object: assert token == "secret-token" assert kwargs["max_retries"] == 3 assert kwargs["backoff_seconds"] == 1.0 assert kwargs["request_interval_seconds"] == 0.2 return object() class FakeBuild: def __init__(self, source: object, repository: object, **kwargs: object) -> None: assert source is not None assert repository is not None assert kwargs def execute(self, command: object) -> BuildSummary: assert command is not None return summary monkeypatch.setattr(cli, "get_settings", FakeSettings) monkeypatch.setattr(cli, "TushareSectorRadarAdapter", FakeSourceFactory) monkeypatch.setattr(cli, "PostgresSectorRadarRepository", FakeRepository) monkeypatch.setattr(cli, "BuildSectorRadar", FakeBuild) exit_code = cli.main(["--trade-date", "2026-08-28"]) output = json.loads(capsys.readouterr().out) assert exit_code == expected_code assert output["status"] == summary.status assert output["exit_code"] == expected_code assert "secret-token" not in str(output) def test_cli_initialization_failure_is_redacted( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: class FailingSourceFactory: @staticmethod def from_token(token: str, **kwargs: object) -> object: del token, kwargs raise RuntimeError("private provider detail secret-token") monkeypatch.setattr(cli, "get_settings", FakeSettings) monkeypatch.setattr(cli, "TushareSectorRadarAdapter", FailingSourceFactory) exit_code = cli.main(["--trade-date", "2026-08-28"]) captured = capsys.readouterr() output = json.loads(captured.out) assert exit_code == 1 assert output["status"] == "failed" assert output["error_type"] == "RuntimeError" assert "private provider detail" not in captured.out assert "secret-token" not in captured.out