Files
llm-wiki/.claude/hooks/test_wiki_rules.py
T

151 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""wiki_rules.py 단위 테스트 (stdlib unittest)."""
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
_SPEC = importlib.util.spec_from_file_location(
"wiki_rules", str(Path(__file__).with_name("wiki_rules.py")))
wr = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(wr)
class TestProjectedContent(unittest.TestCase):
def test_write_full_content(self):
# Write 스타일: content 키가 있으면 그대로 반환
inp = {"content": "FULL BODY"}
self.assertEqual(wr.projected_content(None, inp), "FULL BODY")
def test_edit_applies_old_new(self):
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "f.md"
p.write_text("alpha BETA gamma")
inp = {"old_string": "BETA", "new_string": "DELTA"}
self.assertEqual(wr.projected_content(p, inp), "alpha DELTA gamma")
class TestSeverityData(unittest.TestCase):
def test_critical_codes_are_links(self):
self.assertIn("BROKEN_LINK", wr.CRITICAL_CODES)
self.assertIn("BROKEN_MD_LINK", wr.CRITICAL_CODES)
self.assertNotIn("MISSING_SECTION", wr.CRITICAL_CODES)
def test_fixup_codes_are_completeness(self):
for c in ("MISSING_SECTION", "MISSING_FRONTMATTER",
"EMPTY_SELECTION_CRITERION", "DANGLING_ANCHOR",
"PROJECT_NO_DIAGRAM", "PROJECT_NO_BRANCH_TABLE",
"UNMAPPED_SOURCE_TYPE"):
self.assertIn(c, wr.FIXUP_CODES)
self.assertNotIn("BROKEN_LINK", wr.FIXUP_CODES)
def test_claim_requirements_cover_five_prefixes(self):
prefixes = {p for req in wr.CLAIM_REQUIREMENTS for p in req["prefix"]}
for p in ("raw/official-docs/", "raw/company-tech-blogs/",
"raw/branch-notes/", "wiki/concepts/"):
self.assertIn(p, prefixes)
STD_OK = "리포트...\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: not-ready\nblocking: 2\nshould_fix: 1\nadvisory: 0\n```\n끝"
STD_CONTRADICT = "```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 2\nshould_fix: 0\nadvisory: 0\n```"
STD_BADVERDICT = "```wiki-verdict\nagent: x\nverdict: foo\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"
ADV_OK = "```wiki-verdict\nagent: wiki-adversarial-reviewer\nfinding: 4.1.1 action: KEEP\nfinding: 4.2.1 action: REJECT\n```"
ADV_BADACTION = "```wiki-verdict\nagent: wiki-adversarial-reviewer\nfinding: 4.1.1 action: NOPE\n```"
ADV_EMPTY = "```wiki-verdict\nagent: wiki-adversarial-reviewer\n```"
class TestVerdictBlock(unittest.TestCase):
def test_no_marker_returns_none(self):
parsed, errors = wr.validate_verdict_block("그냥 산문, 마커 없음")
self.assertIsNone(parsed)
self.assertEqual(errors, [])
def test_standard_valid(self):
parsed, errors = wr.validate_verdict_block(STD_OK)
self.assertEqual(errors, [])
self.assertEqual(parsed["agent"], "branch-depth-auditor")
self.assertEqual(parsed["kv"]["verdict"], "not-ready")
def test_standard_contradiction_flagged(self):
_, errors = wr.validate_verdict_block(STD_CONTRADICT)
self.assertTrue(any("blocking" in e for e in errors))
def test_standard_bad_verdict_flagged(self):
_, errors = wr.validate_verdict_block(STD_BADVERDICT)
self.assertTrue(any("verdict" in e for e in errors))
def test_adversarial_valid(self):
parsed, errors = wr.validate_verdict_block(ADV_OK)
self.assertEqual(errors, [])
self.assertEqual(len(parsed["findings"]), 2)
def test_adversarial_bad_action_flagged(self):
_, errors = wr.validate_verdict_block(ADV_BADACTION)
self.assertTrue(any("action" in e for e in errors))
def test_adversarial_empty_findings_flagged(self):
_, errors = wr.validate_verdict_block(ADV_EMPTY)
self.assertTrue(any("finding" in e for e in errors))
def _adv(*pairs):
lines = "\n".join(f"finding: {fid} action: {act}" for fid, act in pairs)
return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"
class TestTallyQuorum(unittest.TestCase):
def test_two_rejects_kill(self):
blocks = [_adv(("A", "REJECT")), _adv(("A", "REJECT")), _adv(("A", "KEEP"))]
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "KILL")
def test_unanimous_keep(self):
blocks = [_adv(("A", "KEEP")), _adv(("A", "KEEP")), _adv(("A", "KEEP"))]
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "KEEP")
def test_reject_plus_downgrade_is_downgrade(self):
blocks = [_adv(("A", "REJECT")), _adv(("A", "DOWNGRADE")), _adv(("A", "KEEP"))]
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "DOWNGRADE")
def test_abstain_not_pass(self):
# 한 블록만 A=KEEP, 나머지 둘은 A 누락(abstain) → 정족수 미달 → UNVERIFIED
blocks = [_adv(("A", "KEEP")), _adv(("B", "KEEP")), _adv(("C", "KEEP"))]
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "UNVERIFIED")
def _stats(found, processed, dropped, reason=None):
body = f"agent: coverage-auditor\nfound: {found}\nprocessed: {processed}\ndropped: {dropped}"
if reason is not None:
body += f"\ndropped_reason: {reason}"
return f"```wiki-stats\n{body}\n```"
class TestStatsBlock(unittest.TestCase):
def test_no_marker_returns_none(self):
parsed, errors = wr.validate_stats_block("산문, 마커 없음")
self.assertIsNone(parsed)
self.assertEqual(errors, [])
def test_balanced_ok(self):
parsed, errors = wr.validate_stats_block(_stats(12, 10, 2, "2 out-of-scope"))
self.assertEqual(errors, [])
self.assertEqual(parsed["agent"], "coverage-auditor")
def test_imbalance_flagged(self):
_, errors = wr.validate_stats_block(_stats(12, 10, 0))
self.assertTrue(any("불균형" in e for e in errors))
def test_dropped_without_reason_flagged(self):
_, errors = wr.validate_stats_block(_stats(12, 10, 2))
self.assertTrue(any("dropped_reason" in e for e in errors))
def test_non_integer_flagged(self):
block = "```wiki-stats\nagent: x\nfound: many\nprocessed: 1\ndropped: 0\n```"
_, errors = wr.validate_stats_block(block)
self.assertTrue(any("정수" in e for e in errors))
if __name__ == "__main__":
unittest.main(verbosity=2)