#!/usr/bin/env python3 """run_all.py — 단일 테스트 러너 (finding #18). CI·로컬 공통 진입점. .claude/tests/test_*.py 를 모두 발견해 순차 실행하고, 스위트별 결과와 총계를 출력한다. 하나라도 실패하면 exit 1(CI가 빨간불). 실행 무결성(doctor·lint_refs)도 함께 게이트한다. Usage: CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py python3 .claude/tests/run_all.py --no-preflight # doctor/lint_refs 생략(테스트만) 각 test_*.py 는 exit 0=통과 / 비0=실패 규약을 따른다(기존 스위트와 동일). """ import glob import os import signal import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(os.path.dirname(HERE)) PY = sys.executable # CI/로컬에서 workspace 미지정 시 조용한 실패 대신 명확히 _sandbox 로 고정(운영 아님·테스트용). ENV = dict(os.environ) ENV.setdefault("CLAUDE_PROJECT_DIR", ROOT) ENV.setdefault("ORGOS_WORKSPACE", "_sandbox") # 스위트별 타임아웃(초). 한 테스트가 멈춰도 CI 전체가 정지하지 않게 한다(재리뷰 지적). SUITE_TIMEOUT = int(os.environ.get("ORGOS_TEST_TIMEOUT", "300")) def run(cmd, label): print(f"\n{'='*60}\n▶ {label}\n{'='*60}") process = None try: process = subprocess.Popen(cmd, env=ENV, start_new_session=True) return process.wait(timeout=SUITE_TIMEOUT) == 0 except subprocess.TimeoutExpired: print(f"⏱️ TIMEOUT after {SUITE_TIMEOUT}s — {label} (fail)", file=sys.stderr) if process is not None: try: os.killpg(process.pid, signal.SIGTERM) process.wait(timeout=3) except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGKILL) process.wait() except ProcessLookupError: pass return False def main(): no_preflight = "--no-preflight" in sys.argv[1:] results = [] if not no_preflight: results.append(("artifact-registry", run( [PY, os.path.join(ROOT, ".claude/hooks/compile_artifact_registry.py"), "--check"], "preflight: compiled artifact registry"))) results.append(("orgos-registry", run( [PY, os.path.join(ROOT, ".claude/hooks/compile_orgos_registry.py"), "--check"], "preflight: compiled Org OS registries + architecture views"))) results.append(("design-system", run( [PY, os.path.join(ROOT, ".claude/hooks/compile_design_system.py"), "--check"], "preflight: compiled organization design system"))) results.append(("doctor.py", run( [PY, os.path.join(ROOT, ".claude/hooks/doctor.py")], "preflight: doctor.py"))) results.append(("lint_refs.py", run( [PY, os.path.join(ROOT, ".claude/hooks/lint_refs.py")], "preflight: lint_refs.py"))) suites = sorted(glob.glob(os.path.join(HERE, "test_*.py"))) if not suites: print("no test_*.py found", file=sys.stderr) return 1 for s in suites: name = os.path.basename(s) results.append((name, run([PY, s], name))) print(f"\n{'='*60}\nSUMMARY\n{'='*60}") passed = [n for n, ok in results if ok] failed = [n for n, ok in results if not ok] for n, ok in results: print(f" {'✅' if ok else '❌'} {n}") print(f"\n{len(passed)}/{len(results)} green · failed: {failed or '없음'}") return 0 if not failed else 1 if __name__ == "__main__": sys.exit(main())