Files

124 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""P3-B T7 cutover 준비 검증 — standalone check(pytest 아님). exit 0=통과.
두 축을 증명한다:
T7.1 fixture 격리 활성화-방탄: 전 v2 profile 을 active 로 가정(full-activation)해도 디스크 fixture
report 가 method-execution 위반을 내지 않는다 — TST-* 는 v2 계약이 아니라 강제 대상 아님(격리).
T7.2 실제 active 역할 통합(조건#3): DES-PROD/post-direction(실제 active)에 대해 method-execution
negative(누락→Hard Fail)·positive(실계약 파생→통과)·drift(hash 불일치→Hard Fail)를 검증.
이 테스트가 green 이면 "전 참조 profile 일괄 활성화 시에도 테스트 스위트가 견딘다"는 readiness 증명이다.
활성화 자체는 HUMAN signoff 게이트(guard 차단) — 이 테스트는 활성화 전 안전성 사전검증.
"""
import glob
import importlib.util
import os
import sys
import yaml
ROOT = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
HOOKS = os.path.join(ROOT, ".claude", "hooks")
FIX = os.path.join(ROOT, ".claude", "tests", "fixtures")
passed = failed = 0
def check(name, ok, detail=""):
global passed, failed
if ok:
passed += 1
print(f" ✅ {name}")
else:
failed += 1
print(f" ❌ {name} {detail}")
def _load(mod, path):
spec = importlib.util.spec_from_file_location(mod, os.path.join(HOOKS, path))
m = importlib.util.module_from_spec(spec)
if HOOKS not in sys.path:
sys.path.insert(0, HOOKS)
spec.loader.exec_module(m)
return m
mc = _load("method_contracts", "method_contracts.py")
rm = mc.load_role_methods()
# 전 v2 profile 을 active 로 가정한 full-activation activations(일괄 활성화 시뮬레이션).
full_acts = {}
for r, e in rm.items():
if (e.get("method-contract") or {}).get("version") == 2:
full_acts[r] = {"methods": {
m["method-id"]: {"status": "active",
"contract-sha256": mc.canonical_contract_hash(mc.resolve_method_profile(r, m["method-id"]))}
for m in e.get("methods", [])}}
print("== T7.1: fixture 격리 활성화-방탄(full-activation 시뮬레이션) ==")
_tst = {"role-id": "TST-GENERIC-FIXTURE", "tier": "standard"}
check("TST-GENERIC-FIXTURE 는 full-activation 에도 method-execution 무강제(계약 아님)",
mc.validate_method_execution(_tst, activations=full_acts, methods=rm) == [])
# fixture 는 test_enforcement 가 런타임 재생성한다(run_all 알파벳순: enforcement < cutover 라 먼저).
# 스탠드얼론에서 디스크가 스테일(구 role-id)이면 스캔이 오탐하므로 canary 로 freshness 확인 후에만 스캔.
_canary = os.path.join(FIX, "agentrep.report.yaml")
_fresh = os.path.exists(_canary) and "TST-GENERIC-FIXTURE" in open(_canary, encoding="utf-8").read()
if _fresh:
_residual = []
for f in sorted(glob.glob(os.path.join(FIX, "*.report.yaml"))):
try:
d = yaml.safe_load(open(f, encoding="utf-8"))
except Exception: # noqa: BLE001
continue
if not isinstance(d, dict):
continue
if mc.validate_method_execution(d, activations=full_acts, methods=rm):
_residual.append(os.path.basename(f))
check("생성된 fixture는 full activation에서도 계약 위반 잔여가 없음",
_residual == [], detail=str(_residual))
_lowercase_missing_trace = {"role-id": "arch-solution", "tier": "standard"}
check("등록 역할의 소문자 표기는 active method 계약을 우회하지 못함",
bool(mc.validate_method_execution(
_lowercase_missing_trace, activations=full_acts, methods=rm)))
else:
check("디스크 fixture 스캔 — 재생성 대기(run_all 순서상 test_enforcement 후 유효, 스탠드얼론 생략)", True)
print("== T7.2: 실제 active 역할(DES-PROD/post-direction) 통합 — 조건#3 ==")
DESPROD, MID = "DES-PROD", "post-direction"
real_acts = mc.load_activations()
_active = mc._active_methods(DESPROD, real_acts)
check(f"{DESPROD}/{MID} 실제 active(활성화 계약 존재)", MID in _active)
prof = mc.resolve_method_profile(DESPROD, MID)
_hash = mc.canonical_contract_hash(prof)
# negative: active 인데 method-execution 없음 → 자기신고 차단
_neg = {"role-id": DESPROD, "tier": "standard"}
check("negative: active + method-execution 없음 → Hard Fail",
any("method-execution.method-id 없음" in e for e in mc.validate_method_execution(_neg, activations=real_acts)))
# positive: 실계약에서 파생한 유효 method-execution → 통과(hash·step-results·artifact-ref 정합)
_steps = []
for s in prof.get("workflow", []):
r = {"step-id": s["step-id"], "status": "completed"}
if s.get("required-output"):
r["artifact-refs"] = [{"report-id": f"artifact-{s['step-id']}", "sha256": "a" * 64}]
_steps.append(r)
_me = {"role-id": DESPROD, "method-id": MID, "contract-sha256": _hash, "step-results": _steps}
_ap = prof.get("alternatives-policy") or {}
_min = _ap.get("min-alternatives") or _ap.get("min") or 0
if _min:
_me["decisions"] = [{"decision-id": "d1", "alternatives": [f"alt{i}" for i in range(_min)]}]
_pos = {"role-id": DESPROD, "tier": "standard", "method-execution": _me}
check("positive: 실계약 파생 유효 method-execution → 통과",
mc.validate_method_execution(_pos, activations=real_acts) == [], detail=str(mc.validate_method_execution(_pos, activations=real_acts)))
# drift: 구버전 hash → 차단(재활성 강제)
_stale = {**_me, "contract-sha256": "0" * 64}
_pos_stale = {"role-id": DESPROD, "tier": "standard", "method-execution": _stale}
check("drift: 구버전 contract-sha256 → Hard Fail(재활성 전 실행 차단)",
any("contract-sha256" in e for e in mc.validate_method_execution(_pos_stale, activations=real_acts)))
print(f"\n{passed} passed · {failed} failed")
sys.exit(1 if failed else 0)