부채 ④. TechLog 의 tech-log-design-package 가 ca1bbfe·1aae8dc·ffa088b 를 적고 있었는데
매니페스트는 그 뒤 다시 만들어져 세 파일 모두를 tech-log-frontend @ 0d4d1e5 로 적는다.
옛 셋은 tech-log-frontend 에 없다 — 옛 매니페스트가 안 남아 그 사이 계약 내용이 바뀌었는지는
대조할 수 없고, 그 사실을 verified 에 적었다. 기록의 「검증 환경」이 적은 리비전은 그때 잰
조건이라 고치지 않는다. 파일 sha256 셋을 함께 적어 리비전 문자열이 아니라 내용에 못박는다.
check_evidence --repo TechLog exit 0 「문제 없음」 (3 → 0)
review-package.py TechLog exit 0 · 관문 10 전부 exit 0
R14. 이 묶음의 막는 경고는 전부 편집 전후 보존 비교에서 나오고(review-package.py 의
_warn 자리 둘이 모두 if preservation.get("available") 안이다) 그 비교는 --before 를 줘야
돈다. 빼면 검토 관문이 아무 줄도 안 남기고 통과한다.
- --before 를 필수로 만들지 않는다. 새로 쓴 기록엔 윤문 전 사본이 없어 첫 기록이 막힌다.
없다는 것을 적고 출력한다
- preservation.reason 으로 두 원인을 가른다 — NO_BEFORE · CHECKER_UNREADABLE
- 묶음이 자기 입력을 적는다. 줬으면 before·beforeSha256, 안 줬으면 명시적 null.
빠진 칸과 null 은 다르다
- 종료 코드는 양쪽 다 0 이다. 새 관문이 아니라 읽는 계약이라 갈리는 것은 문구다.
_review_gate 의 stderr 와 계획 한 줄 요약 둘 다 가른다
- B-B004c-plans/verdict.json 을 지웠다. 옛 묶음 53ce6a5b… 에 묶여 있는데 지금 계획은
0beb5420… 이라 읽히지 않는다 — 검토를 받은 것처럼 보이는 파일이 남는다
python3 -m unittest discover -s scripts/tests — Ran 266 · OK (skipped=13)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
152 lines
6.5 KiB
Python
152 lines
6.5 KiB
Python
"""경고 0 건에는 두 가지가 있다 — 「봤는데 없었다」와 「볼 것이 아직 없었다」.
|
|
|
|
이 묶음의 **막는** 경고는 전부 편집 전후 보존 비교에서 나온다. `review-package.py` 에서
|
|
`_warn` 을 부르는 자리가 둘뿐이고 둘 다 `if preservation.get("available"):` 안이다.
|
|
그 비교는 `--before` 를 줘야 돈다. 빼면 `studio-save.py` 의 검토 관문이 **아무 줄도 안
|
|
남기고 통과한다.**
|
|
|
|
종료 코드는 양쪽 다 0 이다. 새 관문이 아니라 **읽는 계약**이라 갈리는 것은 찍는 문구다.
|
|
**같은 문구가 나오면 고친 것이 아니다.**
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
RECORD = os.path.join(
|
|
"docs", "document-haness", "tech-log-studio", "pipeline-gate-exit-codes",
|
|
"case", "case-exit-code-read-behind-a-pipe.md")
|
|
|
|
|
|
def _package(out: str, before: str | None) -> subprocess.CompletedProcess:
|
|
cmd = [sys.executable, "scripts/review-package.py", "document-haness",
|
|
"--record", RECORD, "-o", out]
|
|
if before:
|
|
cmd += ["--before", before]
|
|
return subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
|
|
|
|
|
|
class PreservationScopeTest(unittest.TestCase):
|
|
"""묶음이 자기 입력을 적는가. 빠진 칸과 null 은 다르다."""
|
|
|
|
def setUp(self):
|
|
self.tmp = tempfile.mkdtemp()
|
|
self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
|
|
# 윤문 전 사본. 한 문장만 다르게 둔다 — 비교가 실제로 돌아야 한다
|
|
self.before = os.path.join(self.tmp, "before.md")
|
|
src = open(os.path.join(ROOT, RECORD), encoding="utf-8").read()
|
|
open(self.before, "w", encoding="utf-8").write(
|
|
src.replace("열넷이 전부 `exit=0` 이었다.",
|
|
"열넷이 전부 `exit=0` 이었을 수도 있다."))
|
|
|
|
def test_without_before_the_package_says_it_did_not_look(self):
|
|
out = os.path.join(self.tmp, "nb.json")
|
|
r = _package(out, None)
|
|
self.assertEqual(0, r.returncode, r.stderr)
|
|
pkg = json.load(open(out, encoding="utf-8"))
|
|
pres = pkg["preservation"]
|
|
self.assertFalse(pres["available"])
|
|
self.assertEqual("NO_BEFORE", pres["reason"])
|
|
# 빠진 칸과 null 은 다르다 — 안 줬다는 것을 **명시적으로** 적는다
|
|
self.assertIn("before", pres)
|
|
self.assertIsNone(pres["before"])
|
|
self.assertIsNone(pres["beforeSha256"])
|
|
|
|
def test_with_before_the_package_records_its_input(self):
|
|
out = os.path.join(self.tmp, "wb.json")
|
|
r = _package(out, self.before)
|
|
self.assertEqual(0, r.returncode, r.stderr)
|
|
pres = json.load(open(out, encoding="utf-8"))["preservation"]
|
|
self.assertTrue(pres["available"])
|
|
self.assertNotIn("reason", pres) # 안 돈 이유가 있을 리 없다
|
|
self.assertTrue(pres["before"])
|
|
self.assertEqual(64, len(pres["beforeSha256"]))
|
|
|
|
def test_the_two_packages_print_different_lines(self):
|
|
"""**같은 문구가 나오면 고친 것이 아니다.**"""
|
|
a = _package(os.path.join(self.tmp, "a.json"), None)
|
|
b = _package(os.path.join(self.tmp, "b.json"), self.before)
|
|
self.assertEqual(0, a.returncode)
|
|
self.assertEqual(0, b.returncode)
|
|
self.assertIn("편집 전후 비교를 안 돌렸다", a.stdout)
|
|
self.assertIn("사정권 밖", a.stdout)
|
|
self.assertNotIn("사정권 밖", b.stdout)
|
|
|
|
|
|
class ReviewGateWordingTest(unittest.TestCase):
|
|
"""저장 게이트가 「0 건」을 두 가지로 찍는가. 종료 코드는 양쪽 다 0 이다."""
|
|
|
|
def setUp(self):
|
|
path = os.path.join(ROOT, "scripts", "studio-save.py")
|
|
import importlib.util as u
|
|
spec = u.spec_from_file_location("studio_save", path)
|
|
self.ss = u.module_from_spec(spec)
|
|
spec.loader.exec_module(self.ss)
|
|
|
|
def _gate(self, preservation):
|
|
import io
|
|
import contextlib
|
|
buf = io.StringIO()
|
|
with contextlib.redirect_stderr(buf):
|
|
out = self.ss._review_gate([], None, __file__, preservation)
|
|
return out, buf.getvalue()
|
|
|
|
def test_it_says_it_looked_when_the_comparison_ran(self):
|
|
out, err = self._gate({"available": True})
|
|
self.assertEqual([], out)
|
|
self.assertIn("편집 전후 비교를 돌렸고", err)
|
|
self.assertNotIn("사정권 밖", err)
|
|
|
|
def test_it_says_it_did_not_look_when_there_was_no_before(self):
|
|
out, err = self._gate({"available": False, "reason": "NO_BEFORE"})
|
|
self.assertEqual([], out)
|
|
self.assertIn("본 것이 없다", err)
|
|
self.assertIn("--before", err)
|
|
self.assertIn("사정권 밖", err)
|
|
|
|
def test_it_names_the_other_cause_too(self):
|
|
"""`available: false` 의 두 원인을 같은 문구로 찍지 않는다."""
|
|
_, a = self._gate({"available": False, "reason": "NO_BEFORE"})
|
|
_, b = self._gate({"available": False, "reason": "CHECKER_UNREADABLE"})
|
|
self.assertIn("check-preservation.py 를 못 읽었다", b)
|
|
self.assertNotEqual(a, b)
|
|
|
|
def test_a_package_that_says_nothing_is_not_read_as_green(self):
|
|
"""옛 묶음에는 `reason` 이 없다. 그래도 초록으로 읽지 않는다."""
|
|
_, err = self._gate({"available": False})
|
|
self.assertIn("본 것이 없다", err)
|
|
self.assertIn("사정권 밖", err)
|
|
|
|
|
|
class PlanSummaryLineTest(unittest.TestCase):
|
|
"""사람이 보는 마지막 줄에서도 갈려야 한다. 여기만 그냥 0 이면
|
|
사정권 밖이 초록으로 돌아온다."""
|
|
|
|
def setUp(self):
|
|
path = os.path.join(ROOT, "scripts", "studio-save.py")
|
|
import importlib.util as u
|
|
spec = u.spec_from_file_location("studio_save", path)
|
|
self.ss = u.module_from_spec(spec)
|
|
spec.loader.exec_module(self.ss)
|
|
|
|
def test_the_summary_splits_zero_two_ways(self):
|
|
looked = self.ss._zero_note({"preservation": {"available": True}})
|
|
blind = self.ss._zero_note({"preservation": {"available": False,
|
|
"reason": "NO_BEFORE"}})
|
|
self.assertNotEqual(looked, blind)
|
|
self.assertIn("사정권 밖", blind)
|
|
self.assertNotIn("사정권 밖", looked)
|
|
|
|
def test_an_old_package_without_preservation_is_not_green(self):
|
|
self.assertIn("사정권 밖", self.ss._zero_note({}))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|