"""새 규칙이 **어긴 것을 잡는지** 본다. 헬퍼 함수가 맞는지는 `test_tree_anchors.py` 가 본다. 여기서는 프로젝트 사본을 만들어 한 군데씩 어기고 검사기가 그것을 세는지 본다 — 검사기의 값은 통과시키는 것이 아니라 안 지킨 것을 잡는 것이다. """ import importlib.util import json import os import shutil import sys import tempfile import unittest ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) SRC = os.path.join(ROOT, "docs", "keycloak") def _load(name, filename, root): """검사기를 그 저장소 루트를 보도록 다시 읽는다.""" spec = importlib.util.spec_from_file_location( name, os.path.join(ROOT, "scripts", filename)) mod = importlib.util.module_from_spec(spec) sys.path.insert(0, os.path.join(ROOT, "scripts")) spec.loader.exec_module(mod) mod.ROOT = root return mod @unittest.skipUnless(os.path.isdir(SRC), "keycloak 프로젝트가 없다") class Sandbox(unittest.TestCase): """keycloak 사본을 만들어 한 군데씩 어긴다. 원본은 건드리지 않는다.""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.root = self.tmp.name os.makedirs(os.path.join(self.root, "docs")) shutil.copytree(SRC, os.path.join(self.root, "docs", "keycloak"), symlinks=True) self.tree_path = os.path.join( self.root, "docs/keycloak/tech-log-studio/tech-log-tree.json") def tree(self): return json.load(open(self.tree_path, encoding="utf-8")) def save(self, data): json.dump(data, open(self.tree_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2) def run_tree(self): return _load("vt", "verify-tech-log-tree.py", self.root).verify("keycloak") def run_layout(self): return _load("vl", "verify-project-layout.py", self.root).verify("keycloak") # ── 사본이 깨끗한지 먼저 ──────────────────────────────────────── def test_사본은_깨끗하다(self): rep = self.run_tree() self.assertEqual(rep.error_count, 0, dict(rep.errors)) # ── 규칙 1 · 앵커가 실재하는 절을 가리키나 ───────────────────── def test_없는_절을_가리키는_앵커를_잡는다(self): t = self.tree() t["candidates"][0]["sourceRefs"] = ["final/document.md#이런-절은-없다-정말로"] self.save(t) rep = self.run_tree() self.assertIn("SSOT 에 없는 절을 가리키는 앵커", rep.errors) # ── 규칙 2 · 계약과 기록의 source 가 같은가 ──────────────────── def test_계약과_기록의_source_가_다르면_잡는다(self): t = self.tree() for kind, items in t["topics"]["oauth-oidc-auth-boundary"]["kinds"].items(): for node in items: if node.get("file") and node.get("source"): node["source"] = node["source"][:1] break else: continue break self.save(t) rep = self.run_tree() self.assertIn("계약과 기록의 source 가 다르다", rep.errors) # ── 규칙 3 · 범위 안인데 아무 후보도 안 짚은 절 ──────────────── def test_아무_후보도_안_짚은_절을_센다(self): # 전부 비우면 앵커 형식 판별이 꺼진다. 한 h2 를 가리키는 앵커만 걷어낸다 gone = "선택이-코드와-흐름에-반영되는-방식" t = self.tree() for c in t["candidates"]: c["sourceRefs"] = [r for r in (c.get("sourceRefs") or []) if gone not in r] for _, items in t["topics"]["oauth-oidc-auth-boundary"]["kinds"].items(): for node in items: node["source"] = [r for r in (node.get("source") or []) if gone not in r] self.save(t) rep = self.run_tree() self.assertIn("범위 안인데 아무 후보도 가리키지 않는 절", rep.warns) # ── 규칙 4 · assetLedger 가 실물과 맞나 ──────────────────────── def test_없는_그림을_배정했다고_적으면_잡는다(self): t = self.tree() t["assetLedger"]["assigned"] = list(t["assetLedger"]["assigned"]) + ["없는-그림"] self.save(t) rep = self.run_tree() self.assertIn("assetLedger 가 없는 그림을 배정했다고 적었다", rep.errors) def test_그림이_대장에_없으면_잡는다(self): t = self.tree() t["assetLedger"]["assigned"] = list(t["assetLedger"]["assigned"])[1:] self.save(t) rep = self.run_tree() self.assertIn("그림이 assetLedger 에 없다", rep.errors) # ── 규칙 5 · 그림의 근거가 SSOT 인가 ─────────────────────────── def test_그림의_근거가_SSOT_가_아니면_잡는다(self): spec_path = os.path.join( self.root, "docs/keycloak/final/.techviz/ap4-edge-trust-architecture/spec.json") spec = json.load(open(spec_path, encoding="utf-8")) spec["source_context"]["document"] = "case-ap4-identity-header-trust.md" json.dump(spec, open(spec_path, "w", encoding="utf-8"), ensure_ascii=False) rep = self.run_layout() self.assertIn("그림의 근거가 SSOT 가 아니다", rep.errors) # ── 규칙 6 · 그림 안에서 상자와 라벨이 겹치나 ────────────────── def test_겹친_그림을_배치_검사가_잡는다(self): """`check-figure-overlap.py` 가 `verify-project-layout.py` 의 관문인지.""" before = os.path.join( ROOT, "runs/keycloak/2026-09-08-0030/stage/S4/before/assets", "ap4-edge-trust-architecture/ap4-edge-trust-architecture.svg") if not os.path.exists(before): self.skipTest("고치기 전 그림 사본이 없다") target = os.path.join( self.root, "docs/keycloak/final/assets", "ap4-edge-trust-architecture/ap4-edge-trust-architecture.svg") shutil.copyfile(before, target) rep = self.run_layout() self.assertIn("그림 안에서 상자와 라벨이 겹친다", rep.errors) if __name__ == "__main__": unittest.main()