"""윤문 전후에 보호 구간이 그대로인지 보는 검사기. 정상 윤문은 통과하고, 수치·코드·인용·URL 을 건드린 편집은 걸린다. 유보 표현이 줄어든 것은 걸러 내되 판정하지 않는다 — 판단은 근거를 읽는 검토가 한다. """ import importlib.util import os import unittest ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) F = os.path.join(ROOT, "scripts", "tests", "fixtures", "preservation") _spec = importlib.util.spec_from_file_location( "check_preservation", os.path.join(ROOT, "scripts", "check-preservation.py")) cp = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(cp) def _read(name): return open(os.path.join(F, name), encoding="utf-8").read() class PreservationTest(unittest.TestCase): def setUp(self): self.before = _read("before.md") def test_an_ordinary_rewrite_passes(self): res = cp.compare(self.before, _read("after-ok.md")) self.assertEqual([], res["findings"]) self.assertEqual([], res["hedgesDropped"]) def test_a_number_changed_in_prose_is_caught(self): res = cp.compare(self.before, _read("after-tampered.md")) values = {f["value"] for f in res["findings"] if f["kind"] == "수치"} self.assertIn("14ms", values) self.assertIn("4ms", values) def test_a_number_changed_inside_a_code_block_is_caught(self): res = cp.compare(self.before, _read("after-tampered.md")) blocks = {f["value"] for f in res["findings"] if f["kind"] == "코드블록"} self.assertTrue(any("exit=1" in b for b in blocks)) self.assertTrue(any("exit=0" in b for b in blocks)) def test_a_changed_direct_quotation_is_caught(self): res = cp.compare(self.before, _read("after-tampered.md")) kinds = {f["kind"] for f in res["findings"]} self.assertIn("직접인용", kinds) def test_a_changed_url_is_caught(self): res = cp.compare(self.before, _read("after-tampered.md")) kinds = {f["kind"] for f in res["findings"]} self.assertIn("URL", kinds) def test_dropped_hedges_are_surfaced_without_a_verdict(self): """유보가 줄면 낸다. 옳은지 그른지는 말하지 않는다.""" res = cp.compare(self.before, _read("after-tampered.md")) dropped = {h["word"] for h in res["hedgesDropped"]} self.assertIn("확인하지 못했다", dropped) self.assertIn("로컬", dropped) self.assertEqual(0, res["hedgeTotalAfter"]) def test_adding_a_hedge_is_not_reported(self): """유보를 더하는 것은 이 규범에서 안전한 쪽이다.""" after = self.before.replace("14ms 였다", "14ms 였다. 다만 한 번만 쟀다") res = cp.compare(self.before, after) self.assertEqual([], res["hedgesDropped"]) def test_a_whole_section_removed_is_surfaced(self): """절을 통째로 지우면 안에 보호 구간이 없어도 보여야 한다. 「확인하지 못한 것」 절을 지우는 편집이 그 모양이다. 낱말 계수만 보면 아무것도 안 움직여서 경고가 비고, 「경고가 비면 자동 통과」로 그대로 지나간다. """ before = ("# 제목\n\n요약이다.\n\n## 결론\n\n값은 14ms 였다.\n\n" "## 확인하지 못한 것\n\n다른 환경에서는 세 보지 않았다.\n") after = "# 제목\n\n요약이다.\n\n## 결론\n\n값은 14ms 였다.\n" res = cp.compare(before, after) self.assertEqual([], res["errors"], "절 삭제는 error 가 아니다") gone = [w["value"] for w in res["warnings"] if w["kind"] == "절"] self.assertIn("## 확인하지 못한 것", gone) def test_a_section_that_stays_is_not_reported(self): """대조군. 절이 그대로면 아무것도 안 나온다.""" text = "# 제목\n\n요약이다.\n\n## 결론\n\n값은 14ms 였다.\n" self.assertEqual([], cp.compare(text, text)["warnings"]) def test_limitation_hedges_are_counted(self): """「안 했다」로 한계를 밝히는 말도 유보다. 목록이 추정·가능성 쪽에만 몰려 있으면 한계를 적은 문장을 지워도 계수가 안 움직인다. """ for word in ("세 보지 않았다", "돌려 보지 않았다", "재 보지 않았다", "확인 안 했다"): with self.subTest(word=word): before = f"값은 14ms 였다. 다른 환경에서는 {word}." after = "값은 14ms 였다." dropped = {h["word"] for h in cp.compare(before, after)["hedgesDropped"]} self.assertIn(word, dropped) def test_a_missing_file_is_not_reported_as_clean(self): import subprocess p = subprocess.run( ["python3", os.path.join(ROOT, "scripts", "check-preservation.py"), os.path.join(F, "before.md"), os.path.join(F, "nope.md")], cwd=ROOT, capture_output=True, text=True) self.assertEqual(2, p.returncode) if __name__ == "__main__": unittest.main()