DMF_Crawler/tests/test_project_contracts.py
Yun Chan 56a6e2da93 chore: 저장소 구조 정리 및 문서화, 첫 커밋
- src/dist 산출물 분리 원칙 정리(.gitignore, .gitattributes)
- 루트 및 주요 폴더(config/scripts/prompts/tests/src, 런타임 폴더 5종)에
  안내용 README.md 추가
- CHANGELOG.md, LICENSE, docs/ops/05-release-and-versioning.md 추가
- docs/README.md 문서 지도 갱신
2026-09-04 09:25:44 +09:00

111 lines
3.5 KiB
Python

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