Wymcp.Testing (Wymcp v0.1.1)

View Source

Conveniences for testing Wymcp tools from a consuming application's test suite.

Three groups of helpers:

  1. Session setupbuild_session_opts/1 builds the opts map Wymcp.Session.init/1 requires, with a test default per key.

  2. Direct tool testingbuild_context/1 plus the unwrap_* extractors: assert on run/2's tagged return tuples (the return contract, including the classified error form, lives in Wymcp.Tool), unit-testing a tool module in isolation.

  3. HTTP response testing — the build_*_request builders and *_response extractors: build tools/call bodies and pull content out of a Plug.Conn response body, integration-testing through Wymcp.Router.

Direct tool testing

ctx = Wymcp.Testing.build_context()
assert {:ok, content} = MyTool.run(ctx, %{"action" => "create", "data" => %{"name" => "x"}})
assert "expected" = Wymcp.Testing.unwrap_text(content)

A generated run/2 takes the whole arguments object, whose key set is closed to action and data — an action's own fields go inside data. A key outside that set is answered by a dispatch gate, not run; see "Dispatch errors and self-correction" in Wymcp.Tool.

With assigns

ctx = Wymcp.Testing.build_context(assigns: %{count: 0})
assert {:ok, content, %{count: 1}} = CounterTool.run(ctx, %{})

HTTP response testing

conn = call_router(body)
assert "expected" = Wymcp.Testing.text_response(conn)

Summary

Functions

image_response/1 for audio content: extracts the single content item of a successful tool response.

Builds a tools/call request body for an action-dispatched tool: wraps the action name and optional data into the arguments object. An empty data is omitted rather than sent as an empty object.

Builds a complete tools/call JSON-RPC request body for POSTing through the router (request id fixed at 1).

Builds a Wymcp.Context for calling a tool's run/2 directly, without a router or a session. Every field defaults to a test-appropriate value — :tools defaults to [] — pass overrides for the ones a test cares about (most often :assigns). A context built without :session_pid serves tools that do not message the session; :session_pid is legacy-only — a live session exists only on the legacy lane, and the option goes at the legacy decommission.

Builds the opts map Wymcp.Session.init/1 requires — the session opts: protocol_version, client_capabilities, client_info, tools, and auth, plus the optional server, session_idle_timeout, and ack_window.

Extracts the text of an isError: true tool response from a router Plug.Conn — raising on a successful result, the mirror of text_response/1.

Extracts the single content item of a successful tool response — for image content, where the item map ("data", "mimeType") is the assertion target rather than a text field.

Like text_response/1, then decodes the text as JSON — for tools that answer with Wymcp.Context.json/1 content.

Extracts the text of a successful single-item tool response from a router Plug.Conn — raising if the result is isError: true, so a test failure names the wrong branch instead of asserting on error text as if it were data.

Decodes the text of a single-item text content array as JSON — for tools that answer with Wymcp.Context.json/1 content.

Unwraps a one-item content array, raising with the full content in the message when there are zero or several items — the failure names what actually arrived instead of a bare MatchError.

Extracts the text of a single-item text content array — the shape most tool responses have. Raises when the content is not exactly one text item.

Functions

audio_response(conn)

image_response/1 for audio content: extracts the single content item of a successful tool response.

build_action_request(tool_name, action, data \\ %{})

Builds a tools/call request body for an action-dispatched tool: wraps the action name and optional data into the arguments object. An empty data is omitted rather than sent as an empty object.

Examples

iex> Wymcp.Testing.build_action_request("tasks", "get", %{"id" => "7"})["params"]["arguments"]
%{"action" => "get", "data" => %{"id" => "7"}}

iex> Wymcp.Testing.build_action_request("tasks", "list")["params"]["arguments"]
%{"action" => "list"}

build_call_request(tool_name, arguments)

Builds a complete tools/call JSON-RPC request body for POSTing through the router (request id fixed at 1).

Examples

iex> Wymcp.Testing.build_call_request("tasks", %{"action" => "get"})["method"]
"tools/call"

build_context(opts \\ [])

Builds a Wymcp.Context for calling a tool's run/2 directly, without a router or a session. Every field defaults to a test-appropriate value — :tools defaults to [] — pass overrides for the ones a test cares about (most often :assigns). A context built without :session_pid serves tools that do not message the session; :session_pid is legacy-only — a live session exists only on the legacy lane, and the option goes at the legacy decommission.

Examples

iex> Wymcp.Testing.build_context().session_id
"test-session"

iex> Wymcp.Testing.build_context(assigns: %{user: "u1"}).assigns
%{user: "u1"}

build_session_opts(overrides \\ [])

Builds the opts map Wymcp.Session.init/1 requires — the session opts: protocol_version, client_capabilities, client_info, tools, and auth, plus the optional server, session_idle_timeout, and ack_window.

Every required key has a test-appropriate default; pass keyword overrides for the ones a test cares about. protocol_version defaults to Wymcp.ProtocolVersion.latest/0, so a session built here negotiates at whatever revision the library currently leads with. The three optional keys enter the map only when passed — Wymcp.Session.init/1 applies its own defaults to their absence (session_idle_timeout and ack_window in particular must be absent, not nil, to get theirs). ack_window is a test-only knob for the push-ack clock and may only shrink it: Wymcp.Session.init/1 refuses a value above its own default.

Callers keep their own start call: the same map serves Wymcp.Session.start_session/1 and Wymcp.Session.start_link({session_id, opts}) alike.

Examples

iex> Wymcp.Testing.build_session_opts().protocol_version
"2025-11-25"

iex> Wymcp.Testing.build_session_opts(tools: [MyTool]).tools
[MyTool]

iex> Map.has_key?(Wymcp.Testing.build_session_opts(), :server)
false

error_response(conn)

Extracts the text of an isError: true tool response from a router Plug.Conn — raising on a successful result, the mirror of text_response/1.

image_response(conn)

Extracts the single content item of a successful tool response — for image content, where the item map ("data", "mimeType") is the assertion target rather than a text field.

json_response(conn)

Like text_response/1, then decodes the text as JSON — for tools that answer with Wymcp.Context.json/1 content.

text_response(conn)

Extracts the text of a successful single-item tool response from a router Plug.Conn — raising if the result is isError: true, so a test failure names the wrong branch instead of asserting on error text as if it were data.

unwrap_json(content)

Decodes the text of a single-item text content array as JSON — for tools that answer with Wymcp.Context.json/1 content.

Examples

iex> Wymcp.Testing.unwrap_json([%{"type" => "text", "text" => ~s({"id": 7})}])
%{"id" => 7}

unwrap_single(content)

Unwraps a one-item content array, raising with the full content in the message when there are zero or several items — the failure names what actually arrived instead of a bare MatchError.

Examples

iex> Wymcp.Testing.unwrap_single([%{"type" => "image"}])
%{"type" => "image"}

unwrap_text(content)

Extracts the text of a single-item text content array — the shape most tool responses have. Raises when the content is not exactly one text item.

Examples

iex> Wymcp.Testing.unwrap_text([%{"type" => "text", "text" => "done"}])
"done"