261 lines
13 KiB
Python
261 lines
13 KiB
Python
#!/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)
|