Files
document-haness/tests/live_runner.py
T

90 lines
3.4 KiB
Python

"""Optional live runner for the installed technical-doc-flow Claude plugin."""
from __future__ import annotations
import json
import os
import shutil
import subprocess
from pathlib import Path
from typing import Any
from helpers import ROOT
CLAUDE_BIN = shutil.which("claude")
class LiveSkillError(RuntimeError):
"""The external skill run could not produce one verified run."""
def run_live_case(case_dir: Path, *, timeout: int = 300) -> tuple[Path, dict[str, Any]]:
if CLAUDE_BIN is None:
raise LiveSkillError("claude CLI를 찾을 수 없습니다.")
case_dir.mkdir(parents=True, exist_ok=True)
brief = case_dir / "brief.md"
draft = case_dir / "draft.md"
workspace = case_dir / "_workspace"
brief.write_text(
"이 초안을 HTTP 요청과 응답만 아는 주니어 백엔드 개발자가 따라갈 수 있게 수정하세요. "
"논리의 원인→요구→선택→검증→한계 흐름을 만들고, 전문용어는 쉬운 설명 뒤에 소개하세요. "
"light revise 경로를 사용하세요.\n",
encoding="utf-8",
)
draft.write_text(
"# 캐시 복구\n\n"
"42ms TTL 만료 뒤 cache stampede가 생기므로 `cacheKey`에 distributed mutex, "
"request coalescing, jitter를 적용한다. 자세한 배경은 https://example.test/cache 이다.\n\n"
"## 검증\n\nload test가 correctness와 resilience를 증명한다.\n",
encoding="utf-8",
)
prompt = (
"이 세션에 로드된 technical-doc-flow 스킬을 반드시 활성화해 전체 절차와 결정적 스크립트를 실행하세요. "
f"brief={brief}, draft={draft}, workspace={workspace}. "
"문서 종류는 explanation이고, 사용자가 light revise를 명시했습니다. "
"09_final_report.json verdict가 pass가 아니면 성공이라고 말하지 마세요."
)
command = [
CLAUDE_BIN,
"--print",
"--no-session-persistence",
"--plugin-dir",
str(ROOT),
"--permission-mode",
"acceptEdits",
"--allowedTools",
"Read,Write,Edit,Bash,Task",
prompt,
]
environment = os.environ.copy()
try:
completed = subprocess.run(
command,
cwd=case_dir,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
timeout=timeout,
env=environment,
check=False,
)
except subprocess.TimeoutExpired as exc:
raise LiveSkillError(f"live 스킬 실행이 {timeout}초를 넘었습니다.") from exc
if completed.returncode != 0:
diagnostic = (completed.stderr or completed.stdout)[-1000:]
raise LiveSkillError(f"claude CLI exit={completed.returncode}: {diagnostic}")
reports = sorted(workspace.glob("*/09_final_report.json"))
if len(reports) != 1:
raise LiveSkillError(
f"09_final_report.json이 정확히 하나여야 합니다: {[str(path) for path in reports]}"
)
try:
report = json.loads(reports[0].read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise LiveSkillError(f"final report를 읽을 수 없습니다: {exc}") from exc
if not isinstance(report, dict):
raise LiveSkillError("final report 최상위 값이 객체가 아닙니다.")
return reports[0].parent, report