# Testing Guide for Claude Agent SDK

This guide provides comprehensive documentation for testing applications built with the Claude Agent SDK for Elixir. It covers the package-local mock fixture system, test fixtures, hooks testing, permission callbacks, integration testing patterns, and best practices.

The mock system is quarantined to this package's `:test` environment. It is not
a service-mode simulation engine and must not be used as a StackLab,
orchestration, ASM, or production runtime selector. Cross-stack simulation goes
through `agent_session_manager` and `cli_subprocess_core`.

Runnable files under `examples/` are live-only. They must use public SDK APIs
and may not invoke the Claude CLI directly or enable package test fixtures.
Keep deterministic provider doubles in `test/` or test-support modules.

## Table of Contents

1. [Mock System Overview](#mock-system-overview)
2. [Enabling Mock Mode](#enabling-mock-mode)
3. [Setting Mock Responses](#setting-mock-responses)
4. [Mock Response Format](#mock-response-format)
5. [Testing Hooks](#testing-hooks)
6. [Testing Permission Callbacks](#testing-permission-callbacks)
7. [Testing with SupertesterCase](#testing-with-supertestercase)
8. [Integration Testing Patterns](#integration-testing-patterns)
9. [Environment Configuration for Tests](#environment-configuration-for-tests)
10. [Best Practices](#best-practices)

---

## Mock System Overview

The Claude Agent SDK includes a package-local mocking system that enables SDK tests without making actual API calls to the Claude service. The mock system consists of three main components:

### Components

1. **Mock Server (`ClaudeAgentSDK.Mock`)**: A GenServer that stores and retrieves mock responses based on prompt patterns.

2. **Mock Process (`ClaudeAgentSDK.Mock.Process`)**: Intercepts CLI calls only in the package-local test fixture scope and returns stored responses.

3. **Mock Transport (`ClaudeAgentSDK.TestSupport.MockTransport`)**: A test transport that records outbound messages, allows pushing inbound frames, and reports subscription metadata as `{pid, :legacy | reference()}` so tests can exercise both legacy and tagged subscriber paths.

### Benefits

- **Fast Tests**: No network calls, instant responses
- **Predictable**: Deterministic responses for consistent test results
- **No API Costs**: Zero usage charges during testing
- **CI/CD Friendly**: No authentication required
- **Fixture Scope**: Parser and wrapper tests can run without provider access

---

## Enabling Mock Mode

Mock mode is only honored in this package's `:test` environment.

### Configuration-Based (Recommended for Tests)

Configure mock mode in your `config/test.exs`:

```elixir
# config/test.exs
config :claude_agent_sdk,
  use_mock: true
```

### Runtime Toggle For Tests

Enable or disable mocking at runtime inside package tests:

```elixir
# Enable mocking
Application.put_env(:claude_agent_sdk, :use_mock, true)

# Start the mock server (required)
{:ok, _pid} = ClaudeAgentSDK.Mock.start_link()

# Disable mocking
Application.put_env(:claude_agent_sdk, :use_mock, false)
```

### Test Helper Setup

The SDK's `test_helper.exs` automatically configures mock mode:

```elixir
# test/test_helper.exs
live_tests? = System.get_env("LIVE_TESTS") == "true"

# Enable mocking for tests unless running in live mode
Application.put_env(:claude_agent_sdk, :use_mock, !live_tests?)

# Start the mock server only when mock mode is enabled
unless live_tests? do
  {:ok, _} = ClaudeAgentSDK.Mock.start_link()
end

ExUnit.start(exclude: [:integration, :live], capture_log: true)
```

### Verifying Mock Mode

```elixir
# Check if mock mode is enabled
Application.get_env(:claude_agent_sdk, :use_mock)
# => true or false

# Verify mock server is running
Process.whereis(ClaudeAgentSDK.Mock)
# => PID or nil
```

---

## Setting Mock Responses

### Basic Pattern Matching

Set responses that match prompts containing a specific pattern:

```elixir
alias ClaudeAgentSDK.Mock

# Set a response for prompts containing "hello"
Mock.set_response("hello", [
  %{
    "type" => "assistant",
    "message" => %{"content" => "Hello from mock!"},
    "session_id" => "mock-123"
  }
])

# Any query containing "hello" returns this response
ClaudeAgentSDK.query("say hello") |> Enum.to_list()
```

### Setting Default Responses

Configure a fallback response for unmatched prompts:

```elixir
Mock.set_default_response([
  %{
    "type" => "assistant",
    "message" => %{"content" => "Custom default response"}
  }
])
```

### Clearing Responses

Reset all mock responses between tests:

```elixir
Mock.clear_responses()
```

### Getting Responses Programmatically

Retrieve the response that would be returned for a prompt:

```elixir
response = Mock.get_response("test prompt")
```

---

## Mock Response Format

Mock responses should match the Claude CLI JSON format. A minimum successful response typically includes three core message types, but live CLI runs may also include `user`, `stream_event`, or `rate_limit_event` frames.

### Complete Response Structure

```elixir
[
  # 1. System initialization message
  %{
    "type" => "system",
    "subtype" => "init",
    "session_id" => "mock-session-123",
    "model" => "opus",
    "tools" => ["bash", "editor"],
    "cwd" => "/current/dir",
    "permissionMode" => "default",
    "apiKeySource" => "mock"
  },

  # 2. Assistant response
  %{
    "type" => "assistant",
    "message" => %{
      "role" => "assistant",
      "content" => "Your response here"
    },
    "session_id" => "mock-session-123"
  },

  # 3. Result message
  %{
    "type" => "result",
    "subtype" => "success",
    "session_id" => "mock-session-123",
    "total_cost_usd" => 0.001,
    "duration_ms" => 100,
    "duration_api_ms" => 50,
    "num_turns" => 1,
    "is_error" => false
  }
]
```

### Message Types Reference

| Type | Description | Key Fields |
|------|-------------|------------|
| `system` | Session initialization | `session_id`, `model`, `cwd`, `tools` |
| `user` | User input | `message`, `session_id` |
| `assistant` | Claude response | `message`, `session_id` |
| `stream_event` | Raw streaming event | `event`, `uuid`, `session_id` |
| `rate_limit_event` | Rate limit state change | `rate_limit_info`, `uuid`, `session_id` |
| `result` | Final result | `total_cost_usd`, `duration_ms`, `num_turns` |

Note: `rate_limit_event` is included here because this SDK tracks the CLI wire format directly. The current Python SDK filters unknown message types instead of surfacing them.

### Result Subtypes

- `success` - Successful completion
- `error_max_turns` - Max turns limit reached
- `error_during_execution` - Error during execution

### Tool Use in Responses

Mock responses can include tool use:

```elixir
%{
  "type" => "assistant",
  "message" => %{
    "role" => "assistant",
    "content" => [
      %{
        "type" => "tool_use",
        "id" => "tool_123",
        "name" => "Bash",
        "input" => %{"command" => "ls -la"}
      }
    ]
  }
}
```

---

## Testing Hooks

The hooks system provides lifecycle event interception for testing security policies, logging, and custom behavior.

### Hook Events

All 12 hook events from the Python SDK are supported:

| Event | When Triggered | Use Case |
|-------|----------------|----------|
| `pre_tool_use` | Before tool executes | Security validation, logging |
| `post_tool_use` | After tool executes | Audit trails, monitoring |
| `post_tool_use_failure` | After tool execution fails | Error tracking |
| `user_prompt_submit` | When user submits prompt | Context injection |
| `stop` | When agent finishes | Final logging |
| `subagent_start` | When subagent is spawned | Subagent tracking |
| `subagent_stop` | When subagent finishes | Subagent result handling |
| `pre_compact` | Before context compaction | Preserve context |
| `notification` | CLI notification received | Alert routing |
| `permission_request` | Permission dialog triggered | Programmatic responses |
| `session_start` | Session begins | Initialization |
| `session_end` | Session ends | Cleanup |

### Creating Test Hooks

```elixir
alias ClaudeAgentSDK.Hooks.{Matcher, Output}

# Hook that allows all tools
allow_all_hook = fn _input, _tool_use_id, _context ->
  Output.allow()
end

# Hook that denies specific tools
deny_dangerous = fn input, _tool_use_id, _context ->
  case input do
    %{"tool_name" => "Bash", "tool_input" => %{"command" => cmd}} ->
      if String.contains?(cmd, "rm -rf") do
        Output.deny("Dangerous command blocked")
      else
        Output.allow()
      end
    _ -> Output.allow()
  end
end

# Hook that records invocations for testing
test_pid = self()
recording_hook = fn input, tool_use_id, context ->
  send(test_pid, {:hook_called, input["tool_name"], input, tool_use_id})
  Output.allow()
end
```

### Configuring Hooks in Options

```elixir
alias ClaudeAgentSDK.{Options, Hooks.Matcher}

options = %Options{
  hooks: %{
    pre_tool_use: [
      Matcher.new("*", [&audit_log/3]),           # Match all tools
      Matcher.new("Bash", [&security_check/3])    # Match specific tool
    ],
    post_tool_use: [
      Matcher.new("*", [&record_result/3])
    ]
  }
}
```

### Testing Hook Behavior

```elixir
defmodule MyApp.HooksTest do
  use ClaudeAgentSDK.SupertesterCase

  alias ClaudeAgentSDK.{Mock, Options}
  alias ClaudeAgentSDK.Hooks.{Matcher, Output}

  setup do
    Mock.clear_responses()
    :ok
  end

  test "pre_tool_use hook can deny dangerous commands" do
    test_pid = self()

    deny_dangerous = fn input, _tool_use_id, _context ->
      send(test_pid, {:hook_check, input})

      case input do
        %{"tool_name" => "Bash", "tool_input" => %{"command" => cmd}} ->
          if String.contains?(cmd, "rm -rf") do
            Output.deny("Dangerous command blocked")
          else
            Output.allow()
          end
        _ -> Output.allow()
      end
    end

    options = %Options{
      hooks: %{
        pre_tool_use: [Matcher.new("Bash", [deny_dangerous])]
      }
    }

    # Set up mock response with tool use
    Mock.set_response("delete files", [
      %{
        "type" => "assistant",
        "message" => %{
          "content" => [
            %{
              "type" => "tool_use",
              "name" => "Bash",
              "input" => %{"command" => "rm -rf /"}
            }
          ]
        }
      }
    ])

    _messages = ClaudeAgentSDK.query("delete files", options) |> Enum.to_list()

    # Verify hook was called
    assert_receive {:hook_check, %{"tool_name" => "Bash"}}
  end

  test "recording hook tracks all tool invocations" do
    test_pid = self()
    invocations = :ets.new(:invocations, [:set, :public])

    recording_hook = fn input, tool_use_id, _context ->
      :ets.insert(invocations, {tool_use_id, input["tool_name"]})
      send(test_pid, {:tool_used, input["tool_name"]})
      Output.allow()
    end

    options = %Options{
      hooks: %{
        pre_tool_use: [Matcher.new("*", [recording_hook])]
      }
    }

    # Execute query and verify recording
    _messages = ClaudeAgentSDK.query("list files", options) |> Enum.to_list()

    # Check recorded tools
    recorded = :ets.tab2list(invocations)
    assert length(recorded) > 0
  end
end
```

### Hook Output Types

```elixir
alias ClaudeAgentSDK.Hooks.Output

# Allow execution
Output.allow()
Output.allow("Approved")

# Allow with input modification
Output.allow(updated_input: %{...})

# Deny execution
Output.deny("Reason")
Output.deny("Reason", interrupt: true)  # Stop entire session

# Add context to prompt
Output.add_context("UserPromptSubmit", "Additional context here")

# Add system message
output = Output.allow() |> Output.with_system_message("Logged")
```

---

## Testing Permission Callbacks

Permission callbacks provide fine-grained control over tool execution.

### Permission Context

```elixir
alias ClaudeAgentSDK.Permission.Context

# Context struct contains:
%Context{
  tool_name: "Write",
  tool_input: %{"file_path" => "/etc/hosts", "content" => "..."},
  session_id: "session-123",
  cwd: "/project"
}
```

### Permission Results

```elixir
alias ClaudeAgentSDK.Permission.Result

# Allow
Result.allow()

# Allow with modified input
Result.allow(updated_input: %{"file_path" => "/safe/path"})

# Deny
Result.deny("Reason")

# Deny with interrupt (stops session)
Result.deny("Critical error", interrupt: true)

# Allow with permission updates
Result.allow(updated_permissions: [
  %{"type" => "setMode", "mode" => "plan", "destination" => "session"}
])
```

### Creating Test Permission Callbacks

```elixir
alias ClaudeAgentSDK.Permission.Result

# Allow all
allow_all = fn _context -> Result.allow() end

# Deny all
deny_all = fn _context -> Result.deny("All denied") end

# Recording callback for testing
test_pid = self()
recording_callback = fn context ->
  send(test_pid, {:permission_check, context})
  Result.allow()
end

# Path restriction callback
path_restriction = fn context ->
  allowed_dirs = ["/tmp", "/home/user/project"]

  if context.tool_name in ["Read", "Write"] do
    file_path = context.tool_input["file_path"] || ""

    if Enum.any?(allowed_dirs, &String.starts_with?(file_path, &1)) do
      Result.allow()
    else
      Result.deny("File path outside allowed directories")
    end
  else
    Result.allow()
  end
end
```

### Testing Permission Callbacks

```elixir
defmodule MyApp.PermissionsTest do
  use ClaudeAgentSDK.SupertesterCase

  alias ClaudeAgentSDK.{Mock, Options}
  alias ClaudeAgentSDK.Permission.{Context, Result}

  setup do
    Mock.clear_responses()
    :ok
  end

  test "permission callback can redirect file writes" do
    redirect_callback = fn context ->
      if context.tool_name == "Write" do
        file_path = context.tool_input["file_path"]

        if String.starts_with?(file_path, "/etc/") do
          safe_path = "/tmp/safe/" <> Path.basename(file_path)
          Result.allow(
            updated_input: Map.put(context.tool_input, "file_path", safe_path)
          )
        else
          Result.allow()
        end
      else
        Result.allow()
      end
    end

    options = %Options{
      can_use_tool: redirect_callback,
      permission_mode: :default
    }

    # Test that writes to /etc/ are redirected
    # ...
  end

  test "permission callback receives correct context" do
    test_pid = self()

    recording_callback = fn context ->
      send(test_pid, {:context_received, context})
      Result.allow()
    end

    options = %Options{
      can_use_tool: recording_callback,
      permission_mode: :default
    }

    # can_use_tool with query uses the control client (string or streaming prompts)
    prompts = [
      %{"type" => "user", "message" => %{"role" => "user", "content" => "write a file"}}
    ]

    _messages = ClaudeAgentSDK.query(prompts, options) |> Enum.to_list()

    assert_receive {:context_received, context}
    assert is_binary(context.tool_name)
    assert is_map(context.tool_input)
  end
end
```

### Permission Modes

```elixir
%Options{
  permission_mode: :default          # CLI default permission flow
}

%Options{
  permission_mode: :auto             # CLI auto permission flow
}

%Options{
  permission_mode: :accept_edits     # Edit operations auto-allowed
}

%Options{
  permission_mode: :plan             # Show plan, execute after approval
}

%Options{
  permission_mode: :bypass_permissions  # All tools allowed (use with caution)
}

%Options{
  permission_mode: :dont_ask         # No permission prompts
}
```

---

## Testing with SupertesterCase

The SDK provides `SupertesterCase` for advanced OTP testing with deterministic behavior.

### Basic Usage

```elixir
defmodule MyApp.SDKTest do
  use ClaudeAgentSDK.SupertesterCase

  # Imports available:
  # - Supertester.OTPHelpers
  # - Supertester.GenServerHelpers
  # - Supertester.Assertions
  # - Supertester.SupervisorHelpers
  # - Supertester.PerformanceHelpers
  # - Supertester.ChaosHelpers

  test "basic SDK operation" do
    # Your test code
  end
end
```

### Eventually Helper

For async assertions that require polling:

```elixir
test "eventually receives expected message" do
  # Start some async operation
  spawn(fn ->
    Process.sleep(50)
    send(self(), {:completed, :value})
  end)

  # Wait for the result
  result = SupertesterCase.eventually(fn ->
    receive do
      {:completed, value} -> value
    after
      0 -> nil
    end
  end, timeout: 1_000, interval: 25)

  assert result == :value
end
```

### Using The Fake CLI Harness

For testing the control protocol client on the finished Stage 3 boundary:

```elixir
alias ClaudeAgentSDK.Client
alias ClaudeAgentSDK.TestSupport.FakeCLI

test "client handles messages correctly" do
  fake_cli = FakeCLI.new!()
  on_exit(fn -> FakeCLI.cleanup(fake_cli) end)

  {:ok, client} = Client.start_link(FakeCLI.options(fake_cli, %Options{}))
  on_exit(fn -> Client.stop(client) end)

  assert :ok = FakeCLI.wait_until_started(fake_cli, 1_000)
  _request_id = FakeCLI.respond_initialize_success!(fake_cli)
  assert :ok = Client.await_initialized(client, 1_000)

  FakeCLI.push_message(fake_cli, %{
    "type" => "assistant",
    "message" => %{"content" => "Hello"}
  })

  assert {:ok, [%ClaudeAgentSDK.Message{type: :assistant} | _]} =
           Client.receive_response(client)
end
```

---

## Integration Testing Patterns

### Live Smoke Test Pattern

For end-to-end testing with real API:

```elixir
defmodule MyApp.LiveSmokeTest do
  use ClaudeAgentSDK.SupertesterCase

  @moduletag :live  # Tag for live tests

  test "basic query works end-to-end" do
    messages = ClaudeAgentSDK.query(
      "Say exactly: live smoke ok",
      %Options{max_turns: 1, output_format: :stream_json}
    ) |> Enum.to_list()

    assert Enum.any?(messages, &(&1.type == :assistant))
    assert Enum.any?(messages, &match?(%{type: :result, subtype: :success}, &1))
  end
end
```

### Running Integration Tests

```bash
# Run all tests (mocks enabled)
mix test

# Run only live tests against real API
LIVE_TESTS=true mix test --only live

# Run integration tests
mix test --only integration

# Run with verbose output
mix test --trace
```

### Testing SDK MCP Tools

```elixir
defmodule MyApp.MCPToolsTest do
  use ClaudeAgentSDK.SupertesterCase

  alias ClaudeAgentSDK.{Mock, Options}
  alias ClaudeAgentSDK.Tool.Registry

  setup do
    Mock.clear_responses()
    :ok
  end

  test "SDK MCP server executes tools correctly" do
    # Create server with test tools
    server = ClaudeAgentSDK.create_sdk_mcp_server(
      name: "calc",
      version: "1.0.0",
      tools: [
        ClaudeAgentSDK.TestSupport.CalculatorTools.Add,
        ClaudeAgentSDK.TestSupport.CalculatorTools.GreetUser
      ]
    )

    assert server.type == :sdk
    assert server.name == "calc"
    assert is_pid(server.registry_pid)

    # Execute tool directly
    {:ok, result} = Registry.execute_tool(
      server.registry_pid,
      "add",
      %{"a" => 5, "b" => 3}
    )

    assert result["content"] |> hd() |> Map.get("text") =~ "8"
  end

  test "SDK MCP tools work in query" do
    server = ClaudeAgentSDK.create_sdk_mcp_server(
      name: "math",
      version: "1.0.0",
      tools: [ClaudeAgentSDK.TestSupport.CalculatorTools.Add]
    )

    options = %Options{
      mcp_servers: %{"math" => server},
      allowed_tools: ["mcp__math__add"]
    }

    # Set mock to trigger tool use
    Mock.set_response("calculate", [
      %{
        "type" => "assistant",
        "message" => %{
          "content" => [
            %{
              "type" => "tool_use",
              "name" => "mcp__math__add",
              "input" => %{"a" => 10, "b" => 20}
            }
          ]
        }
      },
      %{
        "type" => "result",
        "subtype" => "success"
      }
    ])

    messages = ClaudeAgentSDK.query("calculate 10 + 20", options) |> Enum.to_list()
    assert Enum.any?(messages, &(&1.type == :result))
  end
end
```

### Testing Async MCP Dispatch

Long-running SDK MCP tool calls should not block the `Client` process mailbox.
Keep a regression test that proves other requests are still handled while a slow tool runs.

```elixir
test "slow tools/call does not block client responsiveness" do
  # 1) Trigger a slow tools/call request
  # 2) While it is running, send a fast control request (or ping)
  # 3) Assert the fast request returns before the slow tool finishes
end
```

### Testing Strict TaskSupervisor Mode

When strict mode is enabled, unavailable supervisors should fail fast with a stable error tuple:

```elixir
test "strict mode returns explicit unavailable supervisor error" do
  Application.put_env(:claude_agent_sdk, :task_supervisor, Missing.Supervisor)
  Application.put_env(:claude_agent_sdk, :task_supervisor_strict, true)

  assert {:error, {:task_supervisor_unavailable, Missing.Supervisor}} =
           ClaudeAgentSDK.TaskSupervisor.start_child(fn -> :ok end)
end
```

### Testing Streaming

```elixir
defmodule MyApp.StreamingTest do
  use ClaudeAgentSDK.SupertesterCase

  alias ClaudeAgentSDK.{Options, Streaming}

  @moduletag :live

  test "streaming session works" do
    options = %Options{model: "haiku", max_turns: 1, allowed_tools: []}

    {:ok, session} = Streaming.start_session(options)

    try do
      result = Streaming.send_message(session, "Say hello in five words.")
      |> Enum.reduce_while(%{chunks: 0, stopped?: false}, fn
        %{type: :text_delta, text: _chunk}, acc ->
          {:cont, %{acc | chunks: acc.chunks + 1}}

        %{type: :message_stop}, acc ->
          {:halt, %{acc | stopped?: true}}

        %{type: :error, error: reason}, _acc ->
          {:halt, %{error: reason}}

        _event, acc -> {:cont, acc}
      end)

      assert result.stopped? == true
      assert result.chunks > 0
    after
      Streaming.close_session(session)
    end
  end
end
```

---

## Environment Configuration for Tests

### Test Configuration (config/test.exs)

```elixir
import Config

config :claude_agent_sdk,
  use_mock: true,
  cli_command: "claude",
  # Keep tool timeout short in tests that exercise Tool.Registry timeouts
  tool_execution_timeout_ms: 5_000

# Optional: Customize timeouts for tests
config :claude_agent_sdk,
  timeout_ms: 30_000
```

When testing SessionStore startup behavior, remember cache hydration is deferred.
Use bounded retries/assertions for `list/search` immediately after start, or assert via `load_session/1` (disk fallback path).

For transport failures, assert normalized reasons (`:not_connected`, `:cli_not_found`)
instead of transport-specific internals (for example `{:command_not_found, "claude"}`).

For transport/session startup lifecycle tests, `startup_mode: :lazy` still lets
you assert post-init launch failures deterministically. `Streaming.Session.start_link/2`
keeps normal linked-GenServer semantics, so trap exits before you assert
deterministic config failures such as missing cwd/command directly from
`start_link`.

### Hermetic Git Fixtures

If a test creates temporary git repositories, do not let developer-local git
configuration leak into the fixture. Global hooks, signing settings, pagers, or
prompts can turn a simple `git commit` into a slow or hanging test on one
machine while it passes on another.

Prefer an isolated git environment for fixtures:

```elixir
global_config_path = Path.join(tmp_dir, ".git-test-global-config")
File.write!(global_config_path, "")

env = [
  {"GIT_CONFIG_NOSYSTEM", "1"},
  {"GIT_CONFIG_GLOBAL", global_config_path},
  {"GIT_TERMINAL_PROMPT", "0"}
]

System.cmd("git", ["-c", "commit.gpgsign=false", "commit", "-m", "initial"],
  cd: repo_path,
  env: env,
  stderr_to_stdout: true
)
```

Use the same pattern for `git worktree` integration tests. If you need to stub
`git` itself by prepending a fake binary to `PATH`, restore `PATH` after the
test with a bounded helper such as `ClaudeAgentSDK.TestEnvHelpers.with_system_env/2`
or `with_system_and_app_env/4`.

### Development Configuration (config/dev.exs)

```elixir
import Config

config :claude_agent_sdk,
  use_mock: false,
  cli_command: "claude"
```

### Production Configuration (config/prod.exs)

```elixir
import Config

config :claude_agent_sdk,
  use_mock: false,
  cli_command: "claude"
```

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `LIVE_TESTS` | Enable live API tests | `false` |
| `ANTHROPIC_API_KEY` | API key for live tests | - |
| `CLAUDE_AGENT_OAUTH_TOKEN` | OAuth token (alternative auth) | - |

---

## Best Practices

### 1. Always Clear Mocks in Setup

```elixir
setup do
  ClaudeAgentSDK.Mock.clear_responses()
  :ok
end
```

### 2. Use Specific Patterns for Mock Responses

```elixir
# Good: Specific pattern
Mock.set_response("analyze security", security_analysis_response)

# Avoid: Too generic
Mock.set_response("a", generic_response)
```

### 3. Test Both Mock and Live Paths

```elixir
# Unit tests with mocks (fast, predictable)
describe "with mocks" do
  test "handles response correctly" do
    Mock.set_response("test", expected_response)
    # ...
  end
end

# Integration tests with live API (periodic validation)
@tag :live
describe "live integration" do
  test "works with real API" do
    # ...
  end
end
```

### 4. Keep Mock Responses Realistic

```elixir
# Good: Complete, realistic response
Mock.set_response("hello", [
  %{"type" => "system", "subtype" => "init", "session_id" => "mock-123"},
  %{"type" => "assistant", "message" => %{"content" => "Hello!"}},
  %{"type" => "result", "subtype" => "success", "total_cost_usd" => 0.001}
])

# Avoid: Minimal response that may not test all code paths
Mock.set_response("hello", [%{"type" => "assistant"}])
```

### 5. Use Test Fixtures for Common Patterns

```elixir
alias ClaudeAgentSDK.TestSupport.TestFixtures

test "with standard fixtures" do
  hook = TestFixtures.allow_all_hook()
  options = TestFixtures.options_with_hooks(hook)
  # ...
end
```

### 6. Test Error Handling

```elixir
test "handles API errors gracefully" do
  Mock.set_response("error case", [
    %{
      "type" => "assistant",
      "message" => %{"content" => "Error"},
      "error" => %{"code" => "rate_limit"}
    },
    %{
      "type" => "result",
      "subtype" => "error_during_execution",
      "is_error" => true
    }
  ])

  messages = ClaudeAgentSDK.query("error case") |> Enum.to_list()
  assert Enum.any?(messages, &(&1.type == :result and &1.subtype == :error_during_execution))
end
```

### 7. Use Eventually for Async Operations

```elixir
test "async operation completes" do
  start_async_operation()

  result = SupertesterCase.eventually(fn ->
    check_completion_status()
  end, timeout: 5_000)

  assert result == :completed
end
```

### 8. Document Test Purposes

```elixir
@moduledoc """
Tests for the permission callback system.

These tests verify that:
- Permission callbacks receive correct context
- Allow/deny decisions are respected
- Input modifications are applied
- Interrupt flags stop execution
"""

@doc """
Verifies that dangerous bash commands are blocked
by the permission callback.
"""
test "blocks rm -rf commands" do
  # ...
end
```

### 9. Isolate Test State

```elixir
setup do
  # Clear global state
  Mock.clear_responses()

  # Create isolated resources
  {:ok, registry} = Tool.Registry.start_link([])

  on_exit(fn ->
    GenServer.stop(registry)
  end)

  {:ok, registry: registry}
end
```

### 10. Run Live Tests Periodically

```bash
# In CI, run live tests on a schedule (e.g., nightly)
LIVE_TESTS=true mix test --only live

# Locally, verify before major releases
LIVE_TESTS=true mix test --only live --trace
```

---

## Test Support Files Reference

| File | Purpose |
|------|---------|
| `test/test_helper.exs` | Test initialization, mock setup |
| `test/support/supertester_case.ex` | OTP testing foundation |
| `test/support/mock_transport.ex` | Transport mock for client tests |
| `test/support/test_tools.ex` | Reusable tool definitions |
| `test/support/test_fixtures.ex` | Common test fixtures |

---

## Troubleshooting

### Mock Not Working

```elixir
# Verify mock is enabled
IO.inspect(Application.get_env(:claude_agent_sdk, :use_mock))
# Should be: true

# Verify mock server is running
IO.inspect(Process.whereis(ClaudeAgentSDK.Mock))
# Should be: a PID
```

### Wrong Response Returned

```elixir
# Check if pattern matches
# Mock uses String.contains? for matching
Mock.get_response("your actual prompt")
# Compare with expected response
```

### Tests Timing Out

```elixir
# Increase timeout for slow operations
options = %Options{timeout_ms: 60_000}

# Or use eventually with longer timeout
SupertesterCase.eventually(fn -> ... end, timeout: 10_000)
```

### Live Tests Failing

```bash
# Verify authentication
claude --version
claude auth status

# Check environment variables
echo $ANTHROPIC_API_KEY

# Run with verbose output
LIVE_TESTS=true mix test --only live --trace
```
