#!/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)