Wymcp. Testing
(Wymcp v0.1.1)
View Source
Conveniences for testing Wymcp tools from a consuming application's test suite.
Three groups of helpers:
Session setup —
build_session_opts/1builds the opts mapWymcp.Session.init/1requires, with a test default per key.Direct tool testing —
build_context/1plus theunwrap_*extractors: assert onrun/2's tagged return tuples (the return contract, including the classified error form, lives inWymcp.Tool), unit-testing a tool module in isolation.HTTP response testing — the
build_*_requestbuilders and*_responseextractors: buildtools/callbodies and pull content out of aPlug.Connresponse body, integration-testing throughWymcp.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
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.
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"}
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"
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"}
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
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.
Examples
iex> Wymcp.Testing.unwrap_json([%{"type" => "text", "text" => ~s({"id": 7})}])
%{"id" => 7}
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"}
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"