형식 관문 여덟이 확신 승격·수치 조작·화살표 뒤집기·필수 칸 삭제를 하나도 못 막는 것이 재현됐다. 그 가운데 결정적으로 판정 가능한 것을 코드로 옮긴다. check-required-content.py — 종류가 요구하는 칸이 없거나 비었는지 본다. audit-records.py 는 평문 칸 안에 마크업이 있는지만 보고 칸이 있는지는 안 센다. 고정 목차·자료 개수·답은 강제하지 않는다. 답이 없는 QUESTION 은 정상이고 「다음 검증」이 빈 것만 결함이다. 대상이 성립하지 않으면(프로젝트 없음·계약 없음) exit 2 로 막고, 계약은 있고 기록이 0건이면 통과시키되 초록으로 두지 않는다. 「봤고 괜찮다」와 「볼 것이 없어서 통과」는 다르다. check-preservation.py — 윤문 전후를 견준다. 지금 관문 가운데 편집 전후를 보는 것이 하나도 없어 수치를 바꾸거나 유보를 지운 편집이 그대로 통과했다. 사라진 것과 새로 생긴 것을 따로 센다. 새로 생긴 수치는 지어낸 값일 수 있다. 유보 표현이 줄면 내되 옳은지는 판정하지 않는다. 늘어난 것은 세지 않는다. review-package.py — 아무것도 판정하지 않는다. 판정할 사람이 받을 것을 모은다. 해시·검사기 버전·여기서 실제로 돌린 관문·주장 후보·판정 기준. 종료 코드로 안 걸리는 것은 warnings 로 따로 올린다 — 확신 승격이 딱 그 모양이라 안 실으면 아무도 못 본다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
107 lines
4.5 KiB
Python
107 lines
4.5 KiB
Python
"""종류가 요구하는 내용이 채워졌는지 보는 검사기.
|
|
|
|
고정 사례는 다섯 종류마다 둘이다 — 채운 것과 하나를 뺀 것.
|
|
「무조건 통과」도 「무조건 거절」도 아닌 것을 이 짝이 확인한다.
|
|
"""
|
|
import importlib.util
|
|
import os
|
|
import unittest
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
FIXTURES = os.path.join(ROOT, "scripts", "tests", "fixtures", "required-content")
|
|
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"check_required_content", os.path.join(ROOT, "scripts", "check-required-content.py"))
|
|
crc = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(crc)
|
|
|
|
KINDS = ("case", "concept", "reference", "question", "decision")
|
|
|
|
|
|
def _report(sub, name):
|
|
rep = crc.techlog.Report("fixture")
|
|
crc.check_record(os.path.join(FIXTURES, sub, f"{name}.md"), rep)
|
|
return rep
|
|
|
|
|
|
class RequiredContentTest(unittest.TestCase):
|
|
def test_every_kind_passes_when_filled(self):
|
|
for kind in KINDS:
|
|
with self.subTest(kind=kind):
|
|
rep = _report("ok", kind)
|
|
self.assertEqual(0, rep.error_count,
|
|
f"{kind}: {dict(rep.errors)}")
|
|
|
|
def test_every_kind_fails_when_a_required_part_is_missing(self):
|
|
for kind in KINDS:
|
|
with self.subTest(kind=kind):
|
|
rep = _report("missing", kind)
|
|
self.assertGreater(rep.error_count, 0,
|
|
f"{kind} 의 누락 사례가 통과했다")
|
|
|
|
def test_the_missing_part_is_named(self):
|
|
"""무엇이 빠졌는지 말한다. 「어딘가 잘못됐다」로 끝나지 않는다."""
|
|
expected = {
|
|
"case": "결론",
|
|
"concept": "basisVersion",
|
|
"reference": "적용 조건",
|
|
"question": "다음 검증",
|
|
"decision": "판단 이유",
|
|
}
|
|
for kind, part in expected.items():
|
|
with self.subTest(kind=kind):
|
|
rules = " / ".join(_report("missing", kind).errors)
|
|
self.assertIn(part, rules)
|
|
|
|
def test_decision_kind_is_project_decision_in_frontmatter(self):
|
|
"""decision/ 폴더의 기록은 kind: PROJECT_DECISION 이다 (templates/decision.md)."""
|
|
self.assertEqual("decision", crc.KIND_ALIASES["PROJECT_DECISION"])
|
|
rep = _report("ok", "decision")
|
|
self.assertNotIn("kind 를 모르겠다", rep.errors)
|
|
|
|
def test_an_unanswered_question_is_not_an_error(self):
|
|
"""답이 없는 QUESTION 자체는 결함이 아니다. 답을 구할 방법이 없는 것이 결함이다."""
|
|
rep = _report("ok", "question")
|
|
self.assertEqual(0, rep.error_count)
|
|
|
|
def test_body_markers_belong_only_to_case_and_concept(self):
|
|
self.assertEqual({"case", "concept"}, crc.BODY_KINDS)
|
|
|
|
def _cli(self, *args):
|
|
import subprocess
|
|
return subprocess.run(
|
|
["python3", os.path.join(ROOT, "scripts", "check-required-content.py"), *args],
|
|
cwd=ROOT, capture_output=True, text=True)
|
|
|
|
def test_a_missing_project_is_not_reported_as_clean(self):
|
|
"""대상이 없으면 통과가 아니다. 오타 하나로 관문이 무효가 되면 안 된다."""
|
|
p = self._cli("nonexistent-project")
|
|
self.assertEqual(2, p.returncode)
|
|
self.assertIn("대상이 성립하지 않는다", p.stderr)
|
|
|
|
def test_a_project_without_a_contract_does_not_come_back_green(self):
|
|
"""계약이 없으면 「볼 것이 없어서 통과」다. 그것을 초록으로 내지 않는다."""
|
|
p = self._cli("ca-tmpl")
|
|
self.assertEqual(2, p.returncode)
|
|
self.assertIn("tech-log-tree.json 이 없다", p.stderr)
|
|
|
|
def test_a_contract_with_no_records_yet_is_not_an_error(self):
|
|
"""아직 안 쓴 것은 결함이 아니다. 다만 초록으로 보이면 안 된다."""
|
|
p = self._cli("keycloak-session-store")
|
|
self.assertEqual(0, p.returncode)
|
|
self.assertIn("아직 안 쓰였다", p.stdout)
|
|
|
|
def test_one_ungrounded_target_stops_the_whole_run(self):
|
|
"""성립하는 것과 안 하는 것을 함께 주면 통과로 뭉개지 않는다.
|
|
|
|
성립하는 쪽으로 `keycloak` 을 쓴다. `verify-pipeline.py` 의 계약이 scripts/ 안에
|
|
저장소 체크아웃 이름을 적는 것을 금지해서(`FORBIDDEN_LITERAL`), 그 이름과 같은
|
|
프로젝트를 테스트에 적으면 계약 검사가 깨진다.
|
|
"""
|
|
p = self._cli("keycloak", "ca-tmpl")
|
|
self.assertEqual(2, p.returncode)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|