Testing
Input
Inline test input descriptor modeling an event payload.
Usage
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
strstays a virtual path. content: str | None = None-
New file content for Edit/
Write. old: str | None = Nonetool: 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
UserPromptSubmittext. script: str | None = None-
A Workflow tool’s script source (synthesizes a Workflow call).
agent_type: str | None = None-
The payload’s
agent_typeon every event (an Agent/Task call’ssubagent_typetoo). agent_id: str | None = None-
Subagent/teammate id, set in the payload on every event — a non-empty value makes
evt.is_subagenttrue; each id also gets its ownmax_firesbudget. model: str | None = None-
Model for an Agent/Task call’s model input field.
output: str | None = None-
The tool result surfaced to
PostToolUseasevt.tool_response. error: str | None = None-
The failure text surfaced to
PostToolUseFailureasevt.error. reason: str | None = None-
SessionEndreason. source: str | None = None-
SessionStartsource (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 asevt.session_id. skip_permissions: bool | None = None-
Pre-seeds
evt.skip_permissions(normally the process-tree walk for--dangerously-skip-permissions);Noneleaves the real walk in place. disallowed_tools: tuple[str, …] | None = None-
Pre-seeds
evt.disallowed_tools(normally the process-tree walk for--disallowedTools);Noneseeds an empty set, so a test never inherits the runner’s own launch flags. offset: int | None = None-
Readcall offset. limit: int | None = None-
Readcall 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 formtranscripttakes; 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/SubagentStoppayload, surfaced asevt.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.decidereturns, 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 raisesKeyError, 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, soevt.ctx.s.oncededups 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"}); asubprocess.runwhose 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 toghorgitcan 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, soReviewState.load(evt)andevt.ctx.sread it exactly as in production. Shares the directoryseenseeds.
T
Namespaced transcript-fixture builders for Input(transcript=[...]) in inline tests.
Usage
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
assistant(*content, **meta)Parameters
content: str | dict[str, Any] = ()-
Text strings (wrapped as
textblocks) 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
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_useid string, orNoneto 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
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
tool(name, /, *, id=None, **input)Parameters
name: str-
The tool name, e.g.
"Bash"or"EnterPlanMode". id: str | None = None-
The
tool_useid; a freshtu-Nid is minted when omitted. Keep explicit ids outside thetu-<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
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
user(*content, **meta)Parameters
content: str | dict[str, Any] = ()-
Text strings (wrapped as
textblocks) or raw content-block dicts. meta: Any = {}-
Envelope fields merged onto the line, e.g.
isMeta=Truefor 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
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 = Falsesystem_message: str | None = None
Block
Inline test expectation: the hook should block. Optional regex pattern matches the block message.
Usage
Block(*, pattern=None, system_message=None)Optional regex system_message matches the result’s system_message.
Parameter Attributes
pattern: str | None = Nonesystem_message: str | None = None
Warn
Inline test expectation: the hook should warn. Optional regex pattern matches the warning message.
Usage
Warn(*, pattern=None, system_message=None)Optional regex system_message matches the result’s system_message.
Parameter Attributes
pattern: str | None = Nonesystem_message: str | None = None
Rewrite
Inline test expectation: the hook should rewrite the tool input.
Usage
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 = Nonefields: str = {}
Ask
Inline test expectation: the hook returned no result (None).
Usage
Ask()FileFixture
Inline-test file descriptor: materialized to a real temp file so size/stat-based guards run for real.
Usage
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 = Nonecontent: str | None = Nonename: str | None = Nonehome: bool = False
TranscriptFixture
A lightweight transcript stub for use in inline tests.
Usage
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)}