from __future__ import annotations """프로젝트 R0 계약 RED. 이 파일은 `docs/design/07-tdd-red-system.md` 의 7.1 backlog 를 실행 가능한 테스트로 만든다. 목적은 기능 세부가 아니라 "프로젝트가 에이전트/사용자 관점에서 부팅 가능한가"를 보장하는 것이다. """ import importlib import json import os import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] PYTHON = ROOT / ".venv" / "Scripts" / "python.exe" def _python() -> str: return str(PYTHON if PYTHON.exists() else Path(sys.executable)) def _env() -> dict[str, str]: env = os.environ.copy() env.setdefault("PYTHONIOENCODING", "utf-8") env["DMF_CRAWLER_HOME"] = str(ROOT) return env def test_all_expected_runtime_entry_modules_import() -> None: """CLI/저장소/리포트/GUI/알림 진입 모듈은 모두 import 가능해야 한다.""" modules = [ "dmf_crawler.__main__", "dmf_crawler.cli", "dmf_crawler.storage.repo", "dmf_crawler.report", "dmf_crawler.gui.app", "dmf_crawler.notify.pump", ] failed: list[str] = [] for name in modules: try: importlib.import_module(name) except Exception as exc: # noqa: BLE001 - 모든 실패를 한 번에 보여준다 failed.append(f"{name}: {type(exc).__name__}: {exc}") assert not failed, "\n".join(failed) def test_cli_help_exits_zero() -> None: proc = subprocess.run( [_python(), "-m", "dmf_crawler", "--help"], cwd=ROOT, env=_env(), text=True, encoding="utf-8", errors="replace", capture_output=True, timeout=30, ) assert proc.returncode == 0, proc.stderr assert "doctor" in proc.stdout assert "run" in proc.stdout def test_doctor_json_never_crashes_without_key() -> None: """인증키가 없어도 doctor 는 JSON 진단 결과를 출력하고 종료 코드 2로 끝나야 한다.""" proc = subprocess.run( [_python(), "-m", "dmf_crawler", "doctor", "--json"], cwd=ROOT, env=_env(), text=True, encoding="utf-8", errors="replace", capture_output=True, timeout=60, ) assert proc.returncode in {0, 1, 2}, proc.stderr payload = json.loads(proc.stdout) assert isinstance(payload, list) keys = {row["key"] for row in payload} assert "api_key_present" in keys assert "database" in keys def test_powershell_scripts_have_utf8_bom() -> None: scripts = sorted((ROOT / "scripts").glob("*.ps1")) assert scripts, "scripts/*.ps1 이 없다" offenders = [p.name for p in scripts if not p.read_bytes().startswith(b"\xef\xbb\xbf")] assert not offenders, "UTF-8 BOM 누락: " + ", ".join(offenders) def test_cmd_files_are_ascii_only() -> None: scripts = [ROOT / "bootstrap.cmd"] offenders: list[str] = [] for path in scripts: data = path.read_bytes() if any(byte >= 128 for byte in data): offenders.append(path.name) assert not offenders, "CMD 파일은 ASCII 만 허용: " + ", ".join(offenders) def test_agent_docs_link_tdd_ssot() -> None: agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8") claude = (ROOT / "CLAUDE.md").read_text(encoding="utf-8") required = "docs/design/07-tdd-red-system.md" assert required in agents assert required in claude assert "No RED, No Code" in agents assert "테스트를 약하게" in agents