"""검사할 것이 없을 때 관문이 무엇을 내는가 (CLAUDE.md 「검사」 절의 표). 봤고 괜찮다 exit 0 문제 없음 · error 0 대상이 성립하지 않는다 exit 2 대상이 성립하지 않는다 — <이유> 볼 것이 아직 없다 exit 0 기록 0건 — … 가운데 줄이 없으면 오타 한 번으로 검사를 다 돈 것처럼 보인다. 실제로 그랬다 — 여섯 중 넷이 없는 프로젝트에 초록을 냈고 `verify-tech-log-tree.py` 는 그것을 「프로젝트 1 · error 0 · PASS」 로 셌다. """ from __future__ import annotations import os import subprocess import sys import unittest ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) PY_CHECKERS = ( "check-figure-text.py", "check-figure-overlap.py", "audit-records.py", "verify-tech-log-tree.py", "verify-project-layout.py", ) MJS_CHECKER = os.path.join(".agents", "skills", "writing-tech-log-records", "scripts", "check_evidence.mjs") ABSENT = "no-such-project-r4-regression" def _run(cmd: list[str]) -> subprocess.CompletedProcess: return subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True, timeout=300) class NoTargetExitCode(unittest.TestCase): """대상이 성립하지 않으면 exit 2 와 그 문구를 낸다.""" def test_python_checkers_reject_absent_project(self) -> None: for name in PY_CHECKERS: with self.subTest(checker=name): run = _run([sys.executable, os.path.join("scripts", name), ABSENT]) self.assertEqual(run.returncode, 2, run.stdout + run.stderr) self.assertIn("대상이 성립하지 않는다", run.stdout + run.stderr) def test_check_evidence_rejects_absent_project(self) -> None: if not os.path.exists(os.path.join(ROOT, MJS_CHECKER)): self.skipTest("check_evidence.mjs 가 없다") run = _run(["node", MJS_CHECKER, ABSENT]) self.assertEqual(run.returncode, 2, run.stdout + run.stderr) self.assertIn("대상이 성립하지 않는다", run.stdout + run.stderr) def test_project_without_contract_is_not_a_target(self) -> None: """계약이 없는 실재 프로젝트도 「대상이 성립하지 않는다」다.""" base = os.path.join(ROOT, "docs", "ca-tmpl") if not os.path.isdir(base) or os.path.exists(os.path.join(base, "tech-log-studio")): self.skipTest("계약 없는 프로젝트가 이 저장소에 없다") for name in ("audit-records.py", "verify-tech-log-tree.py"): with self.subTest(checker=name): run = _run([sys.executable, os.path.join("scripts", name), "ca-tmpl"]) self.assertEqual(run.returncode, 2, run.stdout + run.stderr) class EmptyTargetIsNotGreen(unittest.TestCase): """계약은 있고 기록이 0건인 것은 결함이 아니다. 다만 「문제 없음」이라고 쓰지 않는다.""" def test_zero_records_says_so(self) -> None: base = os.path.join(ROOT, "docs", "keycloak-session-store") if not os.path.isdir(base): self.skipTest("기록 0건 프로젝트가 이 저장소에 없다") run = _run([sys.executable, os.path.join("scripts", "audit-records.py"), "keycloak-session-store"]) self.assertEqual(run.returncode, 0, run.stdout + run.stderr) self.assertIn("기록 0건", run.stdout) self.assertNotIn("문제 없음", run.stdout) class FileTargets(unittest.TestCase): """`--file` 로 준 경로도 같은 규칙을 지킨다 (R13). 프로젝트 이름만 고치면 `--file` 로 오타를 내는 순간 다시 조용히 0건이 된다. """ FILE_CHECKERS = ("check-figure-text.py", "check-figure-overlap.py", "preview-figure.py") ABSENT_FILE = os.path.join("docs", "no-such-project", "no-such-figure.svg") def test_absent_file_is_not_a_target(self) -> None: for name in self.FILE_CHECKERS: with self.subTest(checker=name): run = _run([sys.executable, os.path.join("scripts", name), "--file", self.ABSENT_FILE]) self.assertEqual(run.returncode, 2, run.stdout + run.stderr) self.assertIn("대상이 성립하지 않는다", run.stdout + run.stderr) def test_present_file_still_passes(self) -> None: """정상 경로를 막으면 안 된다 — 무조건 거절은 결함이다.""" svg = os.path.join("docs", "n+1liner", "final", "assets", "diagrams", "eager-lazy-query-sequence", "eager-lazy-query-sequence.svg") if not os.path.exists(os.path.join(ROOT, svg)): self.skipTest("대조에 쓸 그림이 이 저장소에 없다") for name in ("check-figure-text.py", "check-figure-overlap.py"): with self.subTest(checker=name): run = _run([sys.executable, os.path.join("scripts", name), "--file", svg]) self.assertEqual(run.returncode, 0, run.stdout + run.stderr) class ContractlessProjectIsCounted(unittest.TestCase): """계약이 없는 프로젝트가 전체 훑기의 목록에서 사라지면 안 된다 (R6). `verify_projects()` 가 `docs/*/tech-log-studio` 만 훑던 동안 `ca-tmpl` 은 `TECH LOG TREES` 블록에 아예 안 나왔다. CLAUDE.md 는 「계약 미채택도 error」라고 적었는데 그 error 를 셀 자리가 없었다. """ def test_missing_contract_is_an_error(self) -> None: base = os.path.join(ROOT, "docs", "ca-tmpl") if not os.path.isdir(base) or os.path.exists(os.path.join(base, "tech-log-studio")): self.skipTest("계약 없는 프로젝트가 이 저장소에 없다") run = _run([sys.executable, os.path.join("scripts", "verify-pipeline.py")]) self.assertIn("분해 계약 없음", run.stdout) block = run.stdout.split("TECH LOG TREES:", 1) self.assertEqual(len(block), 2, "TECH LOG TREES 블록이 없다") head = block[1].splitlines()[0] self.assertNotIn("error 0", head, f"계약 없는 프로젝트가 안 세졌다 — {head}") if __name__ == "__main__": unittest.main()