Testing

Input

Inline test input descriptor modeling an event payload.

Usage

Source

Input(
    *,
    command=None,
    file=None,
    content=None,
    old=None,
    tool=None,
    tool_input=None,
    prompt=None,
    script=None,
    agent_type=None,
    agent_id=None,
    model=None,
    output=None,
    error=None,
    reason=None,
    source=None,
    permission_mode=None,
    cwd=None,
    session_id=None,
    skip_permissions=None,
    disallowed_tools=None,
    offset=None,
    limit=None,
    transcript=None,
    root_transcript=None,
    tasks=None,
    background_tasks=None,
    llm=None,
    decide=None,
    seen=None,
    commands=None,
    state=None,
    env=None,
    tickets=None
)

Set the fields the target event reads; all default to None.

Parameters

command: str | None = None

Bash command for PreToolUse/PostToolUse.

file: str | FileFixture | None = None

The tool’s file path. A FileFixture materializes a real temp file so size/stat-based guards run; a str stays a virtual path.

content: str | None = None

New file content for Edit/Write.

old: str | None = None

Pre-edit content for Edit and diff_lint.

tool: str | None = None

Tool name when no condition pins it.

tool_input: dict[str, Any] | None = None

Verbatim raw tool-input mapping — an escape hatch that wins over the input synthesized from the other fields.

prompt: str | None = None

An Agent/Task call’s prompt, or UserPromptSubmit text.

script: str | None = None

A Workflow tool’s script source (synthesizes a Workflow call).

agent_type: str | None = None

The payload’s agent_type on every event (an Agent/Task call’s subagent_type too).

agent_id: str | None = None

Subagent/teammate id, set in the payload on every event — a non-empty value makes evt.is_subagent true; each id also gets its own max_fires budget.

model: str | None = None

Model for an Agent/Task call’s model input field.

output: str | None = None

The tool result surfaced to PostToolUse as evt.tool_response.

error: str | None = None

The failure text surfaced to PostToolUseFailure as evt.error.

reason: str | None = None

SessionEnd reason.

source: str | None = None

SessionStart source (startup/resume/clear/compact).

permission_mode: str | None = None

Permission mode, e.g. "plan" for plan-mode gating.

cwd: str | None = None

The session working directory surfaced as evt.cwd, for hooks that resolve relative tool paths.

session_id: str | None = None

The payload’s session_id, surfaced as evt.session_id.

skip_permissions: bool | None = None

Pre-seeds evt.skip_permissions (normally the process-tree walk for --dangerously-skip-permissions); None leaves the real walk in place.

disallowed_tools: tuple[str, …] | None = None

Pre-seeds evt.disallowed_tools (normally the process-tree walk for --disallowedTools); None seeds an empty set, so a test never inherits the runner’s own launch flags.

offset: int | None = None

Read call offset.

limit: int | None = None

Read call limit.

transcript: Path | TranscriptFixture | list[dict[str, Any]] | None = None

Session history for transcript conditions — a path, a TranscriptFixture, or a raw list of transcript-line dicts. A path also lands in the payload as evt.transcript_path, on every event.

root_transcript: Path | TranscriptFixture | list[dict[str, Any]] | None = None

The root session a lane’s event reads as evt.ctx.root_transcript, in any form transcript takes; pair it with agent_id.

tasks: list[dict[str, Any]] | None = None

The native task list read via evt.tasks.

background_tasks: list[dict[str, Any]] | None = None

Background-task mappings for the Stop/SubagentStop payload, surfaced as evt.background_tasks — the shape that makes a session ~captain_hook.types.Waiting.

llm: dict[str, Any] | None = None

Per-test LLM stub overrides merged over the default stub verdict (fire/block/action/reasoning), e.g. llm={"fire": False} to exercise an LLM hook’s judge-declines path, whether Jev or the LLM judges; an error key holding an exception raises it from the model call, e.g. llm={"error": TimeoutError()}.

decide: dict[str, Any] | None = None

The answers evt.decide returns, keyed by question id (decide={"rollback": BinaryAnswer(p_yes=0.97, confidence=0.94)}); an error key holding an exception raises it from the call, e.g. decide={"error": TimeoutError()}. A question with no stubbed answer raises KeyError, so no test reaches the network.

seen: dict[str, list[str]] | None = None

Keys already observed this session, per once/unseen scope (seen={"scope": ["key"]}; the unscoped call site is ""). Backed by a real temporary session directory, so evt.ctx.s.once dedups exactly as it does in production and “the repeat is silent” is one Input.

commands: dict[str, str | tuple[int, str]] | None = None

Subprocess stubs keyed by argv prefix (commands={"gh pr view": "DRAFT"}); a subprocess.run whose argv starts with a key returns that text as stdout with exit 0, the way llm stubs the model call, so a hook that shells out to gh or git can reach its fire path. A (returncode, stderr) pair makes the command fail with that exit code and stderr and no stdout. Unmatched argv runs for real.

env: dict[str, str] | None = None

The request environment, exactly: every key captain_hook.util.reqenv.getenv() forwards per request (CLAUDE_*, ORCA_*, CAPT_HOOK_*, …) resolves from this mapping alone, never the runner’s own environment, as it does for a real request’s env. reqenv.env_map() hands the same mapping to subprocesses. Defaults to empty.

tickets: list[Ticket] | None = None

Tickets to mint into an otherwise empty ticket store before the test runs.

state: list[BaseModel] | None = None
Session state models to seed (state=[ReviewState(intent="x")]), each written to a real temporary session directory under its own class, so ReviewState.load(evt) and evt.ctx.s read it exactly as in production. Shares the directory seen seeds.

T

Namespaced transcript-fixture builders for Input(transcript=[...]) in inline tests.

Usage

Source

T()

Every builder returns a plain dict — a bare transcript line or a content block — that ~captain_hook.testing.types.TranscriptFixture accepts as-is, so T.* builders and hand-written raw dicts mix freely in one transcript list. Line builders (user(), assistant()) emit the envelope-free shape the fixture loader completes; block builders delegate to cc_transcript.synthetic so the shapes cannot drift from the parser.

Example

>>> from captain_hook.testing.types import Input
>>> Input(transcript=[
...     T.user("Re-enter plan mode, don't do any more work."),
...     T.assistant(T.tool("EnterPlanMode")),
...     *T.tool_turn("Bash", result="ModuleNotFoundError", is_error=True, command="uv run pytest"),
... ])

Input(transcript=…)

Methods

Name Description
assistant() An assistant transcript line whose message carries content.
result() A tool_result content block; of correlates it to the call it answers.
thinking() A thinking content block for assistant().
tool() A tool_use content block invoking name; keyword arguments become its input.
tool_turn() A paired assistant tool-call line and user tool-result line with matched ids.
user() A user transcript line whose message carries content.
assistant()

An assistant transcript line whose message carries content.

Usage

Source

assistant(*content, **meta)
Parameters
content: str | dict[str, Any] = ()

Text strings (wrapped as text blocks) or raw content-block dicts, typically a tool() call block.

meta: Any = {}
Envelope fields merged onto the line.
Example
>>> T.assistant("planning the change")

{‘type’: ‘assistant’, ‘message’: {‘content’: [{‘type’: ‘text’, ‘text’: ‘planning the change’}]}}

result()

A tool_result content block; of correlates it to the call it answers.

Usage

Source

result(content="ok", *, of=None, is_error=False)
Parameters
content: str = "ok"

The result text surfaced to the tool caller.

of: dict[str, Any] | str | None = None

The call this result answers — a tool() block (its id is read), a raw tool_use id string, or None to mint a fresh unpaired id.

is_error: bool = False
Marks a failed call, lifting to a failure in the parsed session.
Example
>>> call = T.tool("Bash", command="uv run pytest")
>>> T.result("ModuleNotFoundError", of=call, is_error=True)

{‘type’: ‘tool_result’, ‘tool_use_id’: …, ‘content’: ‘ModuleNotFoundError’, ‘is_error’: True}

thinking()

A thinking content block for assistant().

Usage

Source

thinking(text)
Example
>>> T.assistant(T.thinking("The user asked for a rename only."))

{‘type’: ‘assistant’, ‘message’: {‘content’: [{‘type’: ‘thinking’, ‘thinking’: …}]}}

tool()

A tool_use content block invoking name; keyword arguments become its input.

Usage

Source

tool(name, /, *, id=None, **input)
Parameters
name: str

The tool name, e.g. "Bash" or "EnterPlanMode".

id: str | None = None

The tool_use id; a fresh tu-N id is minted when omitted. Keep explicit ids outside the tu-<int> namespace auto-minting uses.

input: Any = {}
The tool input fields, e.g. command="uv run pytest".
Example
>>> T.tool("Bash", command="uv run pytest")

{‘type’: ‘tool_use’, ‘id’: …, ‘name’: ‘Bash’, ‘input’: {‘command’: ‘uv run pytest’}}

tool_turn()

A paired assistant tool-call line and user tool-result line with matched ids.

Usage

Source

tool_turn(name, /, *, result="ok", is_error=False, **input)

Splat the returned pair into a transcript list to model one full tool round-trip.

Parameters
name: str

The tool name, e.g. "Bash".

result: str = "ok"

The tool-result content joined back to the call.

is_error: bool = False

Marks the call as failed.

input: Any = {}
The tool input fields, e.g. command="uv run pytest".
Example
>>> T.tool_turn("Bash", result="ModuleNotFoundError", is_error=True, command="uv run pytest")

[{‘type’: ‘assistant’, …}, {‘type’: ‘user’, …}]

user()

A user transcript line whose message carries content.

Usage

Source

user(*content, **meta)
Parameters
content: str | dict[str, Any] = ()

Text strings (wrapped as text blocks) or raw content-block dicts.

meta: Any = {}
Envelope fields merged onto the line, e.g. isMeta=True for a meta turn.
Example
>>> T.user("ship it")

{‘type’: ‘user’, ‘message’: {‘content’: [{‘type’: ‘text’, ‘text’: ‘ship it’}]}}

Allow

Inline test expectation: the hook should allow (return None or action "allow").

Usage

Source

Allow(*, explicit=False, system_message=None)

explicit=True requires an actual allow result — None no longer matches, so e.g. a PermissionRequest hook must have answered the dialog itself. A regex system_message requires an allow result whose system_message it matches.

Parameter Attributes

explicit: bool = False
system_message: str | None = None

Block

Inline test expectation: the hook should block. Optional regex pattern matches the block message.

Usage

Source

Block(*, pattern=None, system_message=None)

Optional regex system_message matches the result’s system_message.

Parameter Attributes

pattern: str | None = None
system_message: str | None = None

Warn

Inline test expectation: the hook should warn. Optional regex pattern matches the warning message.

Usage

Source

Warn(*, pattern=None, system_message=None)

Optional regex system_message matches the result’s system_message.

Parameter Attributes

pattern: str | None = None
system_message: str | None = None

Rewrite

Inline test expectation: the hook should rewrite the tool input.

Usage

Source

Rewrite(pattern=None, **fields)

pattern is a substring matched against the rewritten command (updated_input["command"]) — substring, not regex, so an absolute path prefix in the rewritten command does not break the match. Additional keyword arguments substring-match against the named updated_input field, so an Agent auto-upgrade can assert Rewrite(model="sonnet"); all given fields (and pattern) must match.

Parameter Attributes

pattern: str | None = None
fields: str = {}

Ask

Inline test expectation: the hook returned no result (None).

Usage

Source

Ask()

For PermissionRequest hooks this means the permission dialog shows; a warn result fails Ask().

FileFixture

Inline-test file descriptor: materialized to a real temp file so size/stat-based guards run for real.

Usage

Source

FileFixture(*, size=None, content=None, name=None, home=False)

home=True materializes the file under a per-test temporary “home” directory instead of the shared fixture directory, and run_inline_tests swaps $HOME to that directory for the duration of that one test’s hook execution — restored afterward even on failure — so an os.path.expanduser-based hook resolves deterministically. Requires name.

Parameter Attributes

size: int | None = None
content: str | None = None
name: str | None = None
home: bool = False

TranscriptFixture

A lightweight transcript stub for use in inline tests.

Usage

Source

TranscriptFixture(messages)

Wraps a list of raw transcript-line dicts that get lifted into a query Session when the test runs; missing envelope fields are synthesized.

CONFIRMED

Inline-test answers that confirm a Confirm block, as in Input(..., decide=CONFIRMED): Block().

CONFIRMED={"protected": BinaryAnswer(p_yes=0.9, confidence=0.8)}

UNCONFIRMED

Inline-test answers that clear a Confirm block, as in Input(..., decide=UNCONFIRMED): Allow().

UNCONFIRMED={"protected": BinaryAnswer(p_yes=0.05, confidence=0.9)}