본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

모델 행동은 넓게, 안전장치는 엄격하게 시험해

~14 min · testing, evals, regression

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

결정적인 경계는 단위 시험으로 잡아

모델이 언제나 같은 도구를 고르는지는 단위 시험으로 보장할 수 없어. 대신 도구 정의가 유효한 JSON Schema인지, 훅이 금지 패턴을 거부하는지, 권한 처리기가 올바르게 허용·거부하는지, 감사 기록이 정해진 형식인지 시험할 수 있어. 모델 주변의 결정적 경계를 촘촘히 확인해.

전체 행동은 고정 모델로 통합 시험해

에이전트가 과제를 해결하고 샌드박스 안에 머물며 올바른 출처를 내는지는 날짜 고정 모델을 대상으로 시나리오 시험을 돌려. “한 줄 요약을 만든다”, “Write를 한 번도 부르지 않는다”, “출처를 하나 이상 인용한다”처럼 관찰 가능한 행동으로 판정해.

방어가 실제로 작동하는 회귀 시험을 둬

알려진 프롬프트 주입 문자열을 자료에 넣고 에이전트가 행동으로 따르지 않는지 확인해. 금지한 Bash 명령을 유도하고 훅이 거부하는지도 봐. 시험이 실패한다는 이유로 누군가 안전장치를 지우는 순간을 CI에서 붙잡아야 해.

원칙: 모델의 세부 표현은 넓게 검사하고, 안전 불변 조건은 한 치도 양보하지 말고 시험해.

Code

Hook unit test·python
import pytest
from my_agent.hooks import block_destructive_bash

def bash_call(command: str) -> dict:
    return {"hook_event_name": "PreToolUse", "session_id": "s1", "transcript_path": "", "cwd": "/tmp",
            "tool_name": "Bash", "tool_input": {"command": command}, "tool_use_id": "t1"}

@pytest.mark.asyncio
@pytest.mark.parametrize("cmd,denied", [
    ("ls -la", False),
    ("rm -rf /tmp/foo", True),
    ("dd if=/dev/zero of=/tmp/zero", True),
    ("echo hello", False),
])
async def test_block_destructive_bash(cmd, denied):
    out = await block_destructive_bash(bash_call(cmd), "t1", {"signal": None})  # the real callback
    decision = out.get("hookSpecificOutput", {}).get("permissionDecision")
    assert (decision == "deny") == denied
End-to-end 행동 assertion·python
import pytest
from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, ToolUseBlock, query

@pytest.mark.asyncio
async def test_summarizer_stays_in_sandbox(tmp_path):
    (tmp_path / "input.md").write_text("hello world\n")
    options = ClaudeAgentOptions(
        cwd=str(tmp_path),
        tools=["Read"],          # Write and Bash do not exist in this run
        allowed_tools=["Read"],  # and Read needs no prompt
        model="claude-haiku-4-5-20251001",
        # hermetic: no user or project settings, no auto-memory, and no MCP server
        # you did not name here leak into the run (a big connector list alone can
        # overflow a small model's window before the test says a word)
        setting_sources=[],
        env={"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1"},
        strict_mcp_config=True,
    )
    text, used = [], []
    async for message in query(prompt="Summarize input.md", options=options):
        if isinstance(message, AssistantMessage):  # text lives in content blocks, not message.text
            for block in message.content:
                if isinstance(block, TextBlock):
                    text.append(block.text)
                elif isinstance(block, ToolUseBlock):
                    used.append(block.name)
    assert "hello" in "".join(text).lower()
    assert set(used) <= {"Read"}  # the transcript itself shows no Write or Bash call

External links

Exercise

안전 훅 하나에 허용·거부·경계 사례 단위 시험을 하나씩 만들고 CI의 필수 검사로 연결해.
Hint
안전 훅에 시험이 없으면 다음 리팩터링에서 조용히 사라질 수 있어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.