"""런 원장 검사기. 절차를 안 지킨 원장을 실제로 잡는지 본다. 이 검사기의 값은 「통과시키는 것」이 아니라 「안 지킨 것을 잡는 것」이라, 시험도 전부 위반을 넣어 걸리는지 보는 모양이다. """ import importlib.util import json import os import subprocess import sys import tempfile import unittest ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) SCRIPT = os.path.join(ROOT, "scripts", "verify-pipeline-run.py") LEDGER = os.path.join(ROOT, "runs", "keycloak", "2026-09-07-2215", "run.json") _spec = importlib.util.spec_from_file_location("verify_pipeline_run", SCRIPT) vpr = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(vpr) def run(path, *args): p = subprocess.run([sys.executable, SCRIPT, path, *args], capture_output=True, text=True, cwd=ROOT) return p.returncode, p.stdout + p.stderr @unittest.skipUnless(os.path.exists(LEDGER), "실증 런 원장이 없다") class LedgerRules(unittest.TestCase): """실제로 돈 원장을 밑감으로 삼아 한 군데씩 어긴다.""" def setUp(self): self.base = json.load(open(LEDGER, encoding="utf-8")) self.dir = tempfile.TemporaryDirectory() self.addCleanup(self.dir.cleanup) def write(self, mutate): run_ = json.loads(json.dumps(self.base)) mutate({s["id"]: s for s in run_["stages"]}, run_) path = os.path.join(self.dir.name, "run.json") json.dump(run_, open(path, "w", encoding="utf-8"), ensure_ascii=False) return path def test_실증한_원장은_통과한다(self): code, out = run(LEDGER) self.assertEqual(code, 0, out) self.assertIn("PASS", out) def test_영수증이_그_스킬의_문장이_아니면_잡는다(self): path = self.write(lambda st, _: st["S5"].update( skillEcho="이 문장은 그 스킬 어디에도 없다 정말로 없다 한 글자도")) code, out = run(path) self.assertEqual(code, 1) self.assertIn("스킬 영수증이 그 스킬의 문장이 아니다", out) def test_영수증이_비면_잡는다(self): path = self.write(lambda st, _: st["S6"].update(skillEcho="")) code, out = run(path) self.assertEqual(code, 1) self.assertIn("스킬 영수증이 없다", out) def test_관문이_실패하면_잡는다(self): def m(st, _): st["S3"]["gates"][0]["exit"] = 1 path = self.write(m) code, out = run(path) self.assertEqual(code, 1) self.assertIn("관문이 통과하지 못했다", out) def test_관문이_빠지면_잡는다(self): path = self.write(lambda st, _: st["S6"].update(gates=[])) code, out = run(path) self.assertEqual(code, 1) self.assertIn("관문이 빠졌다", out) def test_건너뛸_수_없는_단계를_건너뛰면_잡는다(self): path = self.write(lambda st, _: st["S3"].update( status="SKIPPED", skipReason="그냥")) code, out = run(path) self.assertEqual(code, 1) self.assertIn("건너뛸 수 없는 단계를 건너뛰었다", out) def test_건너뛴_사유가_없으면_잡는다(self): path = self.write(lambda st, _: st["S4"].update(skipReason="")) code, out = run(path) self.assertEqual(code, 1) self.assertIn("건너뛴 사유가 없다", out) def test_없는_산출물을_적으면_잡는다(self): path = self.write(lambda st, _: st["S1"].update( outputs=["docs/keycloak/없는파일.md"])) code, out = run(path) self.assertEqual(code, 1) self.assertIn("적어 낸 산출물이 디스크에 없다", out) def test_다른_스킬을_쓰면_잡는다(self): path = self.write(lambda st, _: st["S7"].update(skill="technical-visualizer")) code, out = run(path) self.assertEqual(code, 1) self.assertIn("단계가 다른 스킬을 썼다", out) def test_끝나지_않은_단계를_잡는다(self): path = self.write(lambda st, _: st["S5"].update(status="RUNNING")) code, out = run(path) self.assertEqual(code, 1) self.assertIn("끝나지 않은 단계가 있다", out) def test_단계가_통째로_빠지면_잡는다(self): def m(_, run_): run_["stages"] = [s for s in run_["stages"] if s["id"] != "S6"] path = self.write(m) code, out = run(path) self.assertEqual(code, 1) self.assertIn("단계가 원장에 없다", out) def test_곁증명의_영수증도_대조한다(self): """건너뛴 단계라도 곁증명을 냈으면 같은 잣대로 본다.""" proof = os.path.join(ROOT, "runs/keycloak/2026-09-07-2215/stage/S4/stage-report.json") if not os.path.exists(proof): self.skipTest("곁증명이 없다") data = json.load(open(proof, encoding="utf-8")) data["skillEcho"] = "그 스킬에 없는 문장을 곁증명에 적었다 정말로 없다" bad = os.path.join(self.dir.name, "stage-report.json") json.dump(data, open(bad, "w", encoding="utf-8"), ensure_ascii=False) rel = os.path.relpath(bad, ROOT) path = self.write(lambda st, _: st["S4"].update(outputs=[rel])) code, out = run(path) self.assertEqual(code, 1) self.assertIn("곁증명의 스킬 영수증이 그 스킬의 문장이 아니다", out) class EvidenceGateV5(unittest.TestCase): def check(self, gate): rep = vpr.Report("fixture") run_ = {"schemaVersion": vpr.EVIDENCE_RECONCILIATION_SCHEMA, "project": "demo"} found = vpr._evidence_gate_v5(rep, run_, "S3", [gate], "S3 fixture") return rep, found def test_repo_flag_is_required(self): rep, _ = self.check({ "semanticId": "evidence-repo", "cmd": "node check_evidence.mjs demo", "exit": 0, "status": "PASS", }) self.assertTrue(rep.error_count) self.assertIn("live source evidence gate에서 --repo가 빠졌다", rep.errors) def test_unverifiable_preserves_exit_three_and_reason(self): rep, gate = self.check({ "semanticId": "evidence-repo", "cmd": "node check_evidence.mjs demo --repo", "exit": 3, "status": "UNVERIFIABLE", "reason": "source repository unavailable on current machine", "acceptedByProjectReview": True, }) self.assertEqual(0, rep.error_count, rep.errors) self.assertIsNotNone(gate) self.assertEqual(["S3"], rep.facts["live source UNVERIFIABLE"]) def test_unverifiable_cannot_hide_an_arbitrary_exit(self): rep, _ = self.check({ "semanticId": "evidence-repo", "cmd": "node check_evidence.mjs demo --repo", "exit": 1, "status": "UNVERIFIABLE", "reason": "something failed", "acceptedByProjectReview": True, }) self.assertIn("UNVERIFIABLE evidence gate는 실제 대조 불가 exit 3이어야 한다", rep.errors) def test_unverifiable_needs_project_review_acceptance(self): rep, _ = self.check({ "semanticId": "evidence-repo", "cmd": "node check_evidence.mjs demo --repo", "exit": 3, "status": "UNVERIFIABLE", "reason": "source repository unavailable on current machine", }) self.assertIn("UNVERIFIABLE evidence gate가 프로젝트 리뷰에서 수용되지 않았다", rep.errors) def test_pass_requires_real_repo_gate_and_zero_exit(self): rep, _ = self.check({ "semanticId": "evidence-repo", "cmd": "node check_evidence.mjs demo --repo", "exit": 0, "status": "PASS", }) self.assertEqual(0, rep.error_count, rep.errors) def test_semantic_gate_id_is_required(self): rep = vpr.Report("fixture") run_ = {"schemaVersion": vpr.EVIDENCE_RECONCILIATION_SCHEMA, "project": "demo"} vpr._evidence_gate_v5(rep, run_, "S3", [{ "cmd": "node check_evidence.mjs demo --repo", "exit": 0, "status": "PASS" }], "S3 fixture") self.assertIn("필수 evidence semantic gate가 정확히 하나가 아니다", rep.errors) class Init(unittest.TestCase): def test_틀에서_런을_연다(self): with tempfile.TemporaryDirectory() as d: path = os.path.join(d, "run.json") p = subprocess.run( [sys.executable, SCRIPT, "--init", path, "--project", "keycloak", "--record", "docs/keycloak/final/document.md"], capture_output=True, text=True, cwd=ROOT) self.assertEqual(p.returncode, 0, p.stdout + p.stderr) run_ = json.load(open(path, encoding="utf-8")) self.assertEqual([s["id"] for s in run_["stages"]], ["S1", "S2", "S3", "S4", "S5", "S6", "S7"]) self.assertEqual(run_["project"], "keycloak") # 갓 연 원장은 아직 아무 단계도 안 끝났으므로 통과하면 안 된다 code, out = run(path) self.assertEqual(code, 1) self.assertIn("끝나지 않은 단계가 있다", out) if __name__ == "__main__": unittest.main()