#!/usr/bin/env python3 """test_p2_cascade_design.py — 항목3: 디자인 파이프라인의 조건부 cascade 통합 + preview_ui 게이팅. 리뷰 항목3: `/design`은 설계 *문서*만 만들고, 실제 렌더된 화면(preview_ui: 상태·반응형·대비·포커스)을 BUILD 선행조건으로 게이팅하지 않았다. 이 스위트는 다음을 강제로 검증한다: 1. state_engine._has_preview_receipt — evidence-ledger 의 실제 preview_ui 렌더 receipt(exit 0)만 인정. --contrast-only 단독/미실행은 렌더 게이트로 안 침. 2. _must_read_unmet — trusted workload-profile이 UI-bearing인 워크플로에서 ui-design이 Accepted여도 preview_ui receipt가 없으면 spec→build를 **충족하지 않는다**. 3. non-UI workload-profile은 ui-design을 요구하지 않는다(과설계 금지). 4. 배선: design.md UI-bearing 분기 + preview 게이트 명시, collaboration-map design-system-gate. 브라우저·claude CLI 를 요구하지 않는다 — 원장/원장 조회 로직과 커맨드 텍스트만 친다. 실행: CLAUDE_PROJECT_DIR="$PWD" python3 .claude/tests/test_p2_cascade_design.py """ import json import hashlib import os import sys import tempfile import yaml ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) HOOKS = os.path.join(ROOT, ".claude", "hooks") sys.path.insert(0, HOOKS) # 격리 워크스페이스(절대경로) — run_all 의 _sandbox 를 오염시키지 않는다. _WS = tempfile.mkdtemp(prefix="p2cd_") os.environ["CLAUDE_PROJECT_DIR"] = ROOT os.environ["ORGOS_WORKSPACE"] = _WS import state_engine as SE # noqa: E402 import _workspace as W # noqa: E402 passed = failed = 0 def check(name, ok): global passed, failed if ok: passed += 1 print(f" PASS {name}") else: failed += 1 print(f" FAIL {name}") def _write_artifact(wf, artifact_id, kind, stage, producer, payload): directory = os.path.join(_WS, "completion-records", wf) os.makedirs(directory, exist_ok=True) path = os.path.join(directory, f"{artifact_id}.report.yaml") report = { "report-type": "workflow-artifact", "artifact-kind": kind, "artifact-version": 1, "tier": "light", "identity": {"artifact-id": artifact_id, "workflow-id": wf, "stage": stage, "producer-role-id": producer}, "payload": payload, "report-header": { "bottom-line": f"{kind} test fixture", "decision-needed": {"needed": False}, "confidence": {"value": "Med", "derived-from": "evidence"}, "risks": [], "evidence": [{"source-uri": "README.md", "grade": "E3"}], }, } with open(path, "w", encoding="utf-8") as fh: yaml.safe_dump(report, fh, allow_unicode=True, sort_keys=False) return path def _submit(wf, artifact_id, kind, stage, producer, payload, reviewer=None): led = SE.read_ledger(wf) or SE._default_ledger(wf, tier="light") led["stage"] = stage led["stage-status"] = "running" assert SE._write_ledger(wf, led) path = _write_artifact(wf, artifact_id, kind, stage, producer, payload) ok, event = SE.submit_artifact(wf, path, "OPS-ORCH") assert ok, event if reviewer: ok, error = SE.review_artifact(wf, path, "accepted", reviewer) assert ok, error return event def _profile(ui): return { "surfaces": {"ui": ui, "public-api": False, "persistence": False, "infrastructure": False}, "risk": {"security-bearing": False, "data-migration": False, "external-side-effect": False, "risk-level": "Low", "reversibility": "two-way-door", "blast-radius": "single-role", "privacy": False, "regulatory": False, "slo-impact": False}, "required-capabilities": ["product-delivery"], "product-feature": True, } def _seed_required_bundle(wf, ui): SE._write_ledger(wf, {**SE._default_ledger(wf, tier="light"), "stage": "spec", "plan": "cascade"}) _submit(wf, f"{wf}-profile", "workload-profile", "intake", "EXEC-CEO", _profile(ui)) packet = _submit(wf, f"{wf}-packet", "executive-decision-packet", "decide", "EXEC-CEO", {"recommendation": "ship"}, "HUMAN-001") basis = {"basis-artifact-id": packet["artifact-id"], "basis-artifact-sha256": packet["artifact-sha256"]} overall = _submit(wf, f"{wf}-overall", "overall-design", "design", "ARCH-SOLUTION", basis, "ARCH-EA") if ui: direction = _submit(wf, f"{wf}-direction", "approved-design-direction", "design", "DES-DIRECTOR", basis, "EXEC-CPO") ui_artifact = _submit(wf, f"{wf}-ui", "ui-design", "design", "DES-PROD", basis, "EXEC-CPO") check("cross-artifact bundle remains unaccepted before compatibility review", not SE._bundle_accepted(wf, SE._trusted_artifacts(wf), "design-bundle")) _submit(wf, f"{wf}-compat", "compatibility-review", "design", "QA", { "left": {"artifact-kind": "approved-design-direction", "artifact-id": direction["artifact-id"], "artifact-sha256": direction["artifact-sha256"]}, "right": {"artifact-kind": "ui-design", "artifact-id": ui_artifact["artifact-id"], "artifact-sha256": ui_artifact["artifact-sha256"]}, "dimensions": ["interaction", "tokens", "accessibility"], "findings": [], "verdict": "Passed", "reviewer-role-id": "QA", }, "EXEC-VPENG") spec_basis = {"basis-artifact-id": overall["artifact-id"], "basis-artifact-sha256": overall["artifact-sha256"]} _submit(wf, f"{wf}-prd", "prd", "spec", "PROD-PM", spec_basis, "PROD-PO") _submit(wf, f"{wf}-ac", "acceptance-criteria", "spec", "PROD-PM", {**spec_basis, "criteria": [{"criterion-id": "AC-1", "preconditions": [], "input": {}, "expected-result": "works", "risk-level": "Low", "verification-method": "automated-test"}]}, "PROD-PO") return SE._load_ledger_safe(wf) def _seed_receipt(wf, command, exit_code=0): ed = W.evidence_dir() os.makedirs(ed, exist_ok=True) with open(os.path.join(ed, "ledger.jsonl"), "a", encoding="utf-8") as fh: fh.write(json.dumps({"tool_name": "Bash", "command": command, "exit_code": exit_code, "workflow_id": wf, "session_id": "fixture-session", "agent_id": "fixture-agent"}) + "\n") def _seed_typed_receipt(wf, argv, exit_code=0, assertion_status=None, command=None): ed = W.evidence_dir() os.makedirs(ed, exist_ok=True) row = { "receipt_id": f"typed-{wf}", "tool_use_id": f"typed-{wf}", "receipt_type": "verification-run", "tool_name": "VerifyRun", "command_argv": argv, "exit_code": exit_code, "workflow_id": wf, "session_id": "fixture-session", "agent_id": "fixture-agent", "assertion_status": assertion_status or ("passed" if exit_code == 0 else "failed"), } if command is not None: row["command"] = command with open(os.path.join(ed, "ledger.jsonl"), "a", encoding="utf-8") as fh: fh.write(json.dumps(row) + "\n") # ── 1) _has_preview_receipt ──────────────────────────────────────────── print("== _has_preview_receipt: 실제 렌더 receipt 만 인정 ==") check("no receipt -> False", SE._has_preview_receipt("wf-none") is False) _seed_receipt("wf-contrast", "python3 .claude/hooks/preview_ui.py --contrast-only x.css", 0) check("contrast-only receipt -> NOT a render gate (False)", SE._has_preview_receipt("wf-contrast") is False) _seed_receipt("wf-fail", "python3 .claude/hooks/preview_ui.py design-system --check-css", 1) check("preview_ui exit!=0 -> False", SE._has_preview_receipt("wf-fail") is False) _render_dir = os.path.join(_WS, "rendered-ui") os.makedirs(_render_dir, exist_ok=True) with open(os.path.join(_render_dir, "fixture.png"), "wb") as fh: fh.write(SE._PNG_SIG + b"x" * 1100) _seed_receipt("wf-ok", f"python3 .claude/hooks/preview_ui.py {_render_dir} --viewports 360,768 --check-css", 0) check("preview_ui exit 0 render receipt -> True", SE._has_preview_receipt("wf-ok") is True) # verify_run emits command_argv rather than a shell command string. That typed # representation must be recognized, while the same typed run must remain # bound to an actual successful exit and render artifact. _typed_dir = os.path.join(_WS, "typed rendered ui") os.makedirs(_typed_dir, exist_ok=True) with open(os.path.join(_typed_dir, "typed preview.w1280.png"), "wb") as fh: fh.write(SE._PNG_SIG + b"x" * 1100) _seed_typed_receipt("wf-typed-ok", [ sys.executable, os.path.join(HOOKS, "preview_ui.py"), _typed_dir, "--out", os.path.join(_typed_dir, "typed preview.png"), "--viewports", "1280", ]) check("typed command_argv preview receipt -> True", SE._has_preview_receipt("wf-typed-ok") is True) _seed_typed_receipt("wf-typed-fail", [ sys.executable, os.path.join(HOOKS, "preview_ui.py"), _typed_dir, "--out", os.path.join(_typed_dir, "typed preview.png"), "--viewports", "1280", ], exit_code=1) check("typed failed preview receipt is rejected even when PNG remains", SE._has_preview_receipt("wf-typed-fail") is False) # If both forms appear, canonical typed argv wins over a contradictory legacy # command string. This prevents ambiguity in the newer receipt contract. _seed_typed_receipt("wf-typed-canonical", [ sys.executable, os.path.join(HOOKS, "preview_ui.py"), _typed_dir, "--out", os.path.join(_typed_dir, "typed preview.png"), "--viewports", "1280", ], command="grep preview_ui.py README.md") check("typed command_argv is canonical when legacy command disagrees", SE._has_preview_receipt("wf-typed-canonical") is True) # workflow_id 결속: 다른 wf 의 receipt 는 인정 안 함(receipt 가 wf 를 명시할 때). check("receipt bound to other wf -> not counted for wf-x", SE._has_preview_receipt("wf-x-different") is False) # ── 2) _must_read_unmet: UI-bearing ui-design 게이팅 ─────────────────── print("== _must_read_unmet: trusted UI workload + preview gate ==") WF = "wf-ui" led = _seed_required_bundle(WF, ui=True) unmet0 = SE._must_read_unmet(WF, led, led["artifacts"]) check("ui-design accepted but NO preview receipt -> still unmet", any("ui-design" in u for u in unmet0)) check("all other conditional bundle components are met", unmet0 and all("ui-design" in u for u in unmet0)) # guard 도 실제로 막는지: can_transition(spec->build) False ok_before, reasons_before = SE.can_transition(WF, "build") check("spec->build BLOCKED while design-system not render-gated", ok_before is False and any("ui-design" in r for r in reasons_before)) # preview_ui 렌더 receipt 를 남기면 충족 _seed_receipt(WF, f"python3 .claude/hooks/preview_ui.py {_render_dir} --viewports 360,768,1280 --check-css", 0) unmet1 = SE._must_read_unmet(WF, led, led["artifacts"]) check("ui-design MET after real preview receipt", not any("ui-design" in u for u in unmet1)) check("must-read fully satisfied (empty unmet)", unmet1 == []) # ── 3) non-UI 워크플로는 design-system 불요(과설계 금지) ───────────────── print("== non-UI workload는 ui-design 불요 ==") WFB = "wf-backend" ledb = _seed_required_bundle(WFB, ui=False) unmetb = SE._must_read_unmet(WFB, ledb, ledb["artifacts"]) check("backend must-read has NO ui-design requirement", not any("ui-design" in u for u in unmetb)) check("backend must-read satisfied without any preview receipt", unmetb == []) # ── 4) 배선: 커맨드/맵 텍스트 ─────────────────────────────────────────── print("== 배선: design.md + collaboration-map ==") _design = open(os.path.join(ROOT, ".claude/commands/design.md"), encoding="utf-8").read() check("design.md has UI-bearing branch", "UI-bearing" in _design) check("design.md uses workload-profile UI predicate (not family fallback)", "workload-profile.payload.surfaces.ui" in _design and "family 선택 결과로 UI 여부를 다시 추론하지 않는다" in _design) check("design.md invokes design-system sub-pipeline", "design-system" in _design and "preview_ui" in _design) check("design.md states preview receipt gates must-read", "must-read-designs-accepted" in _design and "receipt" in _design) check("design.md guards over-design (non-UI skips)", "non-UI" in _design or "과설계" in _design) _cmap = open(os.path.join(ROOT, "org-os/06-agent-work/collaboration-map.yaml"), encoding="utf-8").read() check("collaboration-map has design-system-gate", "design-system-gate" in _cmap) check("design-system-gate names preview_ui render receipt", "preview_ui" in _cmap and "_has_preview_receipt" in _cmap) print(f"\n{passed} passed, {failed} failed") sys.exit(1 if failed else 0)