windlass.testing¶
testing
¶
Testing helpers for applications built on Windlass.
Testing an AI application is hard for a specific reason: the model is non-deterministic, slow and costs money. The fix is to make every component replaceable with a deterministic double — which is exactly what the framework's architecture already provides.
This module packages that into helpers you can use directly::
from windlass.testing import scripted_llm, isolated_registry, capture_spans
def test_agent_calls_the_search_tool():
llm = scripted_llm(["", "Found it."], tool_calls=[[call("search", q="x")], []])
agent = Windlass.agent().llm(llm).tool(search)
with capture_spans() as spans:
assert agent.run("find x").output == "Found it."
assert spans.count("tool") == 1
Everything here is dependency-free and offline.
RecordingTool
¶
A tool that records its invocations.
Use it to assert that an agent called what you expected, with the arguments you expected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Tool name. |
required |
result
|
Any
|
Value returned on every call. |
'ok'
|
description
|
str
|
Model-facing description. |
''
|
Attributes:
| Name | Type | Description |
|---|---|---|
calls |
list[dict[str, Any]]
|
One dict of arguments per invocation. |
Example
recorder = RecordingTool("lookup", result={"found": True}) agent = fake_agent( ... ["", "Found."], ... tools=[recorder.as_tool()], ... tool_calls=[[call("lookup", key="abc")], []], ... ) agent.run("look it up").output 'Found.' recorder.calls [{'key': 'abc'}]
Source code in src\windlass\testing.py
as_tool
¶
Return a bindable :class:~windlass.interfaces.tool.Tool.
Returns:
| Type | Description |
|---|---|
Any
|
A tool accepting arbitrary keyword arguments and returning |
Any
|
attr: |
Source code in src\windlass\testing.py
assert_called_with
¶
Assert that the tool was called with exactly these arguments at least once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**expected
|
Any
|
The expected arguments. |
{}
|
Raises:
| Type | Description |
|---|---|
AssertionError
|
When no recorded call matches. |
Source code in src\windlass\testing.py
scripted_llm
¶
scripted_llm(
responses: Sequence[str] | str,
*,
tool_calls: Sequence[Sequence[ToolCall]] | None = None,
handler: Callable[[list[Message], list[dict[str, Any]] | None], Any] | None = None,
**config: Any
) -> Any
Build a model that returns a fixed script.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
responses
|
Sequence[str] | str
|
Texts returned in order, one per call. |
required |
tool_calls
|
Sequence[Sequence[ToolCall]] | None
|
Tool calls emitted per turn. Use this to drive an agent through a specific tool-use path. |
None
|
handler
|
Callable[[list[Message], list[dict[str, Any]] | None], Any] | None
|
Callable receiving |
None
|
**config
|
Any
|
Forwarded to :class: |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Any
|
class: |
Any
|
records every prompt it received. |
Example
llm = scripted_llm(["first", "second"]) llm.complete("a").content, llm.complete("b").content ('first', 'second') llm.calls[0][-1].content 'a'
Source code in src\windlass\testing.py
call
¶
Build a :class:~windlass.core.types.ToolCall for a scripted model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Tool name. |
required |
**arguments
|
Any
|
Tool arguments. |
{}
|
Returns:
| Type | Description |
|---|---|
ToolCall
|
The tool call. |
Example
call("search", query="rag").arguments
Source code in src\windlass\testing.py
fake_rag
¶
fake_rag(
documents: Sequence[str] | None = None,
*,
answers: Sequence[str] | None = None,
**config: Any
) -> Any
Build a fully offline RAG pipeline, optionally pre-loaded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
documents
|
Sequence[str] | None
|
Texts to ingest immediately. |
None
|
answers
|
Sequence[str] | None
|
Scripted answers for the model. |
None
|
**config
|
Any
|
Forwarded to :meth: |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Any
|
class: |
Any
|
embeddings and the in-memory store — no network, no keys, deterministic. |
Example
rag = fake_rag(["Paris is the capital of France."], answers=["Paris."]) rag.ask("What is the capital of France?").answer 'Paris.'
Source code in src\windlass\testing.py
fake_agent
¶
fake_agent(
answers: Sequence[str] | None = None,
*,
tools: Sequence[Any] = (),
tool_calls: Sequence[Sequence[ToolCall]] | None = None,
**config: Any
) -> Any
Build a fully offline agent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
answers
|
Sequence[str] | None
|
Scripted model responses, in order. |
None
|
tools
|
Sequence[Any]
|
Tools to bind. |
()
|
tool_calls
|
Sequence[Sequence[ToolCall]] | None
|
Tool calls emitted per turn. |
None
|
**config
|
Any
|
Forwarded to :meth: |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
Any
|
class: |
Example
agent = fake_agent(["All done."]) agent.run("do the thing").output 'All done.'
Source code in src\windlass\testing.py
isolated_registry
¶
Run a block against a private copy of the component registry.
Registering a component is a global side effect. Tests that register custom components would otherwise leak into each other; this snapshots the registry on entry and restores it on exit.
Yields:
| Type | Description |
|---|---|
Any
|
The global :class: |
Example
from windlass import register with isolated_registry() as reg: ... @register.chunker("temporary") ... class Temp: ... pass ... reg.has("chunker", "temporary") True REGISTRY.has("chunker", "temporary") False
Source code in src\windlass\testing.py
capture_spans
¶
Collect the traces emitted inside the block.
Binds a :class:~windlass.providers.observability.console.MemoryTracer into
the root container, so every builder created inside the block traces into it.
Yields:
| Type | Description |
|---|---|
Any
|
The tracer. Assert with |
Any
|
|
Example
from windlass import Windlass with capture_spans() as spans: ... agent = Windlass.agent().llm("fake", responses=["hi"]).observe("memory") ... _ = agent.run("hello") spans.count() >= 0 True
Source code in src\windlass\testing.py
make_documents
¶
Build documents from plain strings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*texts
|
str
|
Document contents. |
()
|
**metadata
|
Any
|
Metadata applied to every document. |
{}
|
Returns:
| Type | Description |
|---|---|
list[Document]
|
The documents, each with a |
Example
docs = make_documents("one", "two", corpus="unit-test") len(docs), docs[0].metadata["corpus"] (2, 'unit-test')
Source code in src\windlass\testing.py
make_chunks
¶
Build chunks from plain strings, optionally embedded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*texts
|
str
|
Chunk contents. |
()
|
embed
|
bool
|
Attach deterministic hash embeddings, so the chunks can be written straight to a vector store. |
True
|
dimensions
|
int
|
Embedding dimensionality. |
64
|
Returns:
| Type | Description |
|---|---|
list[Chunk]
|
The chunks. |
Example
chunks = make_chunks("alpha", "beta", dimensions=8) len(chunks), len(chunks[0].embedding) (2, 8)
Source code in src\windlass\testing.py
assert_answer_uses_context
¶
Assert that a RAG answer's retrieved context mentions expected.
Testing the answer text is testing the model, which is not what a pipeline test should assert on. Testing the retrieved context is testing your retrieval — which is yours, and is deterministic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
answer
|
Any
|
A :class: |
required |
expected
|
str
|
A substring that should appear in the retrieved context. |
required |
Raises:
| Type | Description |
|---|---|
AssertionError
|
When no retrieved chunk contains |
Example
rag = fake_rag(["The tower is 330 metres tall."]) assert_answer_uses_context(rag.ask("How tall is the tower?"), "330")