loom.testing¶
Functions
|
Resolve exports whose dependencies belong to optional extras on first access. |
|
Import |
|
Import one |
- class loom.testing.AgentHandleDouble(name)[source]¶
Bases:
objectIn-memory double for
AgentHandle.Bound to a use case’s parameter through
UseCaseTest.with_agent().Stands in for the handle a real
Agent()marker resolves to: no network, model or database call is ever made. Every run mode returns an answer scripted throughon_run()/on_run_text(); every grant view is a further double reached throughmcp()/sql()and cached, so scheduling its answers once configures every call the use case makes through it. Every call this handle itself receives is recorded onrun_calls/run_text_calls, so a test can assert what the use case asked of it.- Parameters:
name (str) – Agent name as the
Agent(name)marker declares it, named in this double’s refusal messages.
Example:
triage = AgentHandleDouble("incident-triage").on_run(SeverityAssessment(severity=5)) result = await ( UseCaseTest(TriageIncidentUseCase()) .with_caller(identity) .with_agent("incident-triage", triage) .with_params(incident_id="INC-1") .run() ) assert triage.run_calls[0].prompt == "Assess INC-1."
- on_run(output, *, usage=None, interaction_id=None)[source]¶
Schedule the next
run()call’s answer, declared or per-run shape alike.- Returns:
selffor chaining.- Parameters:
output (Any)
usage (AgentUsage | None)
interaction_id (str | None)
- Return type:
- on_run_text(text, *, usage=None, interaction_id=None)[source]¶
Schedule the next
run_text()call’s answer.- Returns:
selffor chaining.- Parameters:
text (str)
usage (AgentUsage | None)
interaction_id (str | None)
- Return type:
- async run(prompt, *, expect=None, conversation_id=None, state=None)[source]¶
Record the call and return the answer scripted through
on_run().- Raises:
AssertionError – If no answer was scheduled through
on_run().- Parameters:
- Return type:
- async run_text(prompt, *, conversation_id=None, state=None)[source]¶
Record the call and return the answer scripted through
on_run_text().- Raises:
AssertionError – If no answer was scheduled through
on_run_text().- Parameters:
- Return type:
- mcp(server)[source]¶
Return this agent’s own double for the
mcpgrant named server.Built once and cached: repeated calls with the same name return the same double.
- Parameters:
server (str)
- Return type:
- sql(connection)[source]¶
Return this agent’s own double for the
sqlgrant named connection.Built once and cached: repeated calls with the same name return the same double.
- Parameters:
connection (str)
- Return type:
- with_grants(*names)[source]¶
Declare the grant names this double reports, and refuse the rest.
Without it the double reports what has been reached, which is not what the real handle promises: there, the listing names what the artefact declares, whether or not anything used it. A test that asserts a grant is available would pass against the double and prove nothing about production, so declaring the set here makes the two agree.
- Parameters:
names (str) – Every server and connection this agent declares.
- Returns:
This double, for chaining.
- Return type:
- class loom.testing.ContractScenario(*, expected_output=None, events=None, error_code=None)[source]¶
Bases:
LoomFrozenStructEngine behaviour one contract check requires.
An
AgentPlandeclares structure, not behaviour, while every contract check needs a scripted behaviour: a success run with its events, or a failure with its coded error. The scenario is therefore the right seam — the suite hands this neutral description and the adapter under test builds an engine exhibiting it: the fake maps it onto a script, and a real-engine adapter can map it onto a stubbed provider (FR-048).- Parameters:
expected_output (object)
events (tuple[TextDeltaEvent | ToolCallEvent | ToolResultEvent | ErrorEvent | FinalEvent, ...] | None)
error_code (AgentRunErrorCode | None)
- expected_output¶
Output
run()and the terminalFinalEventmust produce in a success scenario.- Type:
- events¶
Events the engine may replay in a success scenario, ending in a
FinalEvent; the suite checks stream structure only, never that these exact events come back.Nonelets the engine choose its own events.
- error_code¶
When set, the scenario is a failure: the stream must end in an
ErrorEventwith this code, andeventsis ignored.- Type:
- class loom.testing.FakeAgentEngine(*, script=None, output=None)[source]¶
Bases:
objectDeterministic, offline
AgentEnginetest double.Replays a fixed event script: no network, no credentials, no clocks and no randomness, so two instances built from the same arguments produce byte-for-byte identical results and streams.
- Parameters:
script (Sequence[AgentEvent] | None) – Event sequence to replay. Must end in exactly one terminal event (
FinalEventorErrorEvent), with no terminal event before the last position. When omitted, a fixed default script ending in aFinalEventcarryingoutputis replayed.output (object | None) – Output of the default script’s
FinalEvent. Ignored whenscriptis provided.
- Raises:
ValueError – If
scriptis empty, does not end in a terminal event, or contains a terminal event before the last position.
Example:
engine = FakeAgentEngine(output={"answer": 42}) result = await engine.run("question", identity=identity)
- async run(prompt, *, identity, conversation=None, state=None)[source]¶
Replay the script to completion.
- Parameters:
prompt (str) – Caller prompt; ignored, the script is fixed.
identity (Identity) – Verified caller; ignored, the script is fixed.
conversation (Conversation | None) – Conversation to continue; ignored, the script is fixed.
state (object | None) – This run’s state; ignored, the script is fixed.
- Returns:
The terminal
FinalEvent’s output, usage and messages.- Raises:
FakeAgentRunError – If the script ends in an
ErrorEvent.- Return type:
- run_stream(prompt, *, identity, conversation=None, state=None)[source]¶
Replay the script as an event stream.
The returned context manager closes the iterator on exit via
aclose()— deterministically, never left to the garbage collector — mirroring how a real engine must release its provider connection.- Parameters:
prompt (str) – Caller prompt; ignored, the script is fixed.
identity (Identity) – Verified caller; ignored, the script is fixed.
conversation (Conversation | None) – Conversation to continue; ignored, the script is fixed.
state (object | None) – This run’s state; ignored, the script is fixed.
- Returns:
An async context manager yielding the scripted event stream.
- Return type:
AbstractAsyncContextManager[AsyncIterator[TextDeltaEvent | ToolCallEvent | ToolResultEvent | ErrorEvent | FinalEvent]]
- class loom.testing.GoldenHarness[source]¶
Bases:
objectExecutes use cases in isolation with fake repositories.
Allows injecting fake repo instances, forcing errors on specific methods, and asserting performance baselines — all without a real database.
Example:
harness = GoldenHarness() harness.inject_repo(IProductRepo, FakeProductRepo(), model=Product) harness.force_error(IProductRepo, "create", Conflict("duplicate")) result = await harness.run(CreateProductUseCase, payload={"name": "X"})
- inject_repo(interface, fake_instance, *, model=None)[source]¶
Register a fake repository instance for an interface type.
- Parameters:
- Return type:
None
- force_error(interface, method, error)[source]¶
Force a specific repository method to raise an error.
- simulate_system_error(interface, method)[source]¶
Simulate a
SystemErroron a specific repository method.
- async run(use_case_type, *, params=None, payload=None, identity=None)[source]¶
Execute a use case with injected fake repositories.
- Parameters:
use_case_type (type[UseCase[Any, Any, RepoFor[Any]]]) – UseCase subclass to execute.
params (dict[str, Any] | None) – Primitive parameter values keyed by name.
payload (dict[str, Any] | None) – Raw dict for
Input()command construction.identity (Identity | None) – Caller the execution runs as. Required by use cases declaring
Caller(); passANONYMOUSto pin the unauthenticated path.
- Returns:
Result produced by the use case.
- Raises:
loom.core.errors.Unauthenticated – If the use case declares
Caller()and no identity is given.- Return type:
- async run_with_baseline(use_case_type, *, params=None, payload=None, identity=None, name, max_ms, baseline_dir)[source]¶
Execute a use case and assert it completes within a time baseline.
Writes a
<name>.jsonfile tobaseline_dirrecording the measured duration. RaisesAssertionErrorif execution exceedsmax_ms.- Parameters:
use_case_type (type[UseCase[Any, Any, RepoFor[Any]]]) – UseCase subclass to execute.
params (dict[str, Any] | None) – Primitive parameter values keyed by name.
payload (dict[str, Any] | None) – Raw dict for
Input()command construction.identity (Identity | None) – Caller the execution runs as.
name (str) – Baseline identifier used as the filename.
max_ms (float) – Maximum allowed execution duration in milliseconds.
baseline_dir (Path) – Directory where baseline JSON files are written.
- Returns:
Result produced by the use case.
- Raises:
AssertionError – If elapsed time exceeds
max_ms.- Return type:
- class loom.testing.InMemoryRepository(entity_type, *, id_field='id', creator=None)[source]¶
Bases:
Generic[T]Generic in-memory repository for testing any
msgspec.Structmodel.Stores entities in a plain dict keyed by their id field value. Provides the standard repository surface (
get_by_id,create,update,delete,list_paginated) without any database dependency.The
createmethod derives entity fields from the command automatically when nocreatorcallable is provided: fields present on the command are copied to the entity, and the id field is assigned from an internal auto-increment counter.- Parameters:
entity_type (type[T]) – The
msgspec.Structsubclass this repository stores.id_field (str) – Name of the identity field on the entity. Defaults to
"id".creator (Callable[[Any, int], T] | None) – Optional
(cmd, next_id) -> Tcallable used bycreate(). When provided, the automatic field-mapping is bypassed entirely.
Example:
repo = InMemoryRepository(Product, id_field="id") repo.seed(Product(id=1, name="Widget"), Product(id=2, name="Gadget")) harness = HttpTestHarness() harness.inject_repo(Product, repo) client = harness.build_app(interfaces=[ProductRestInterface])
- seed(*entities)[source]¶
Pre-load entities into the store.
The internal id counter is advanced past the highest integer id seen so that subsequent
create()calls do not collide.- Parameters:
*entities (T) – Entity instances to load.
- Return type:
None
Example:
repo.seed(Product(id=1, name="A"), Product(id=2, name="B"))
- async get_by_id(obj_id, profile='default')[source]¶
Return the entity with
obj_id, orNoneif not found.
- async create(cmd)[source]¶
Create and store a new entity from
cmd.If a
creatorcallable was provided at construction it is called ascreator(cmd, next_id). Otherwise, command attributes whose names match entity fields are copied automatically, and the id field is set from the internal auto-increment counter.- Parameters:
cmd (Any) – Command or payload object carrying the new entity’s data.
- Returns:
The created and stored entity.
- Return type:
T
- async update(obj_id, data)[source]¶
Update the entity at
obj_idwith fields fromdata.Only non-
Nonefields present on bothdataand the entity are overwritten; the id field is never changed.
- class loom.testing.McpHandleDouble(server)[source]¶
Bases:
objectIn-memory double for
McpHandle.No network call is ever made.
call()andcall_untyped()return a result scripted per tool name throughon_call()/on_call_untyped(); every invocation is recorded oncallsso a test can assert what the use case asked of it.- Parameters:
server (str) – Server name this double stands in for, named in its refusal messages.
- with_tools(*tools)[source]¶
Script the tool names
tools()returns.- Returns:
selffor chaining.- Parameters:
tools (str)
- Return type:
- on_call(tool, result)[source]¶
Script the decoded result the next typed
call()for tool returns.- Returns:
selffor chaining.- Parameters:
- Return type:
- on_call_untyped(tool, result)[source]¶
Script the raw result the next
call_untyped()for tool returns.- Returns:
selffor chaining.- Parameters:
- Return type:
- tools()[source]¶
Return the tool names scripted through
with_tools().
- class loom.testing.SqlGrantHandleDouble(connection)[source]¶
Bases:
objectIn-memory double for
SqlGrantHandle.No connection is ever opened.
query()returns rows scripted throughon_query(), and every call is recorded oncalls.- Parameters:
connection (str) – Connection name this double stands in for, named in its refusal message.
- on_query(rows)[source]¶
Script the rows every
query()call returns.- Returns:
selffor chaining.- Parameters:
- Return type:
- async query(statement, *, parameters=None)[source]¶
Record the call and return the rows scripted through
on_query().- Raises:
AssertionError – If no rows were scheduled through
on_query().- Parameters:
- Return type:
- class loom.testing.UseCaseTest(use_case)[source]¶
Bases:
Generic[ResultT]Fluent test harness for executing UseCases without HTTP or framework overhead.
Builds and runs the real ExecutionPlan — no shortcuts or mocking of the pipeline. Designed for unit and integration tests that must exercise computes, rules, and load steps in full.
- Parameters:
use_case (UseCase[Any, ResultT]) – Constructed UseCase instance to test.
Example:
result = await ( UseCaseTest(UpdateUserUseCase(repo=fake_repo)) .with_params(user_id=1) .with_input(email="new@example.com") .run() )
- with_params(**kwargs)[source]¶
Set primitive parameter values bound by name.
- Parameters:
**kwargs (Any) – Parameter names and values matching the UseCase’s non-Input, non-Load parameters.
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_input(**kwargs)[source]¶
Set raw payload fields for command construction.
The payload is passed to the Command’s
from_payloadmethod. Usewith_commandif you have a pre-built Command instance.- Parameters:
**kwargs (Any) – Payload fields matching the Command struct.
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_command(cmd)[source]¶
Set a pre-built Command instance as the execution payload.
Serializes the command via
msgspec.to_builtinsso it is compatible with the standardfrom_payloadpipeline.- Parameters:
cmd (Any) – A
Command(msgspec.Struct) instance.- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_loaded(entity_type, entity)[source]¶
Pre-load an entity, bypassing repository calls for this type.
- Parameters:
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_deps(entity_type, repo)[source]¶
Register a repository for a given entity type.
Used when the UseCase has
LoadById()steps that require a repo.with_loadedtakes precedence overwith_depsfor the same type.- Parameters:
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_caller(identity)[source]¶
Run the use case as identity, filling its
Caller()parameter.Without this call a use case declaring
Caller()fails closed, which is the point: an authorization test must state whose request it is.- Parameters:
identity (Identity) – Caller the execution runs as. Pass
ANONYMOUSto exercise the unauthenticated path explicitly.- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_agent(name, double)[source]¶
Bind double to the
Agent(name)marker parameter named name.Without this call a use case declaring
Agent(name)fails closed when run, naming the use case and the agent — the same fail-closed designwith_caller()applies toCaller(), extended to the whole handle: every run mode and every grant view, not only the execution itself, so nothing that reaches the agent can run without a network, a model or a database standing in.- Parameters:
name (str) – Agent name exactly as the
Agent(name)marker declares it in the use case under test.double (AgentHandleDouble) – Pre-built
AgentHandleDouble— script its answers withon_run()/on_run_text()and its grant views withmcp()/sql()before passing it here.
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_mcp(server, double)[source]¶
Bind double to the
Mcp(server, include=...)marker parameter named server.Without this call a use case declaring
Mcp(server, ...)fails closed when run, naming the use case and the server — the same fail-closed designwith_agent()applies toAgent(name).The double is narrowed to the resolving binding’s own
includebefore the use case ever sees it: a scripted tool outsideinclude, or inside it but never scripted throughwith_tools(), is refused with the sameAgentRunError(TOOL_UNKNOWN)production raises before any network call.- Parameters:
server (str) – Server name exactly as the
Mcp(server, ...)marker declares it in the use case under test.double (McpHandleDouble) – Pre-built
McpHandleDouble— script its tools and results withwith_tools()/on_call()/on_call_untyped()before passing it here.
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_main_repo(repo)[source]¶
Inject the main repository dependency into the UseCase instance.
This is useful for unit tests of
UseCase[TModel, TResult]where the core logic reads fromself.main_repo.- Parameters:
repo (RepoFor[Any]) – Repository instance compatible with the UseCase’s main model.
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- async run()[source]¶
Compile and execute the UseCase through the full pipeline.
- Returns:
The result produced by the UseCase.
- Raises:
loom.core.errors.RuleViolations – If one or more rule steps fail.
NotFound – If a Load step finds no entity.
loom.core.errors.Unauthenticated – If the UseCase declares
Caller(),Agent(name)orMcp(server, ...)and nowith_caller()was set.RuntimeError – If the UseCase declares
Agent(name)and no matchingwith_agent()call registered a double for name, or declaresMcp(server, ...)and no matchingwith_mcp()call registered a double for server.loom.core.engine.compiler.CompilationError – If the UseCase fails structural validation.
- Return type:
ResultT
- property plan: ExecutionPlan¶
Compile and return the ExecutionPlan for the UseCase.
Useful for asserting plan structure in advanced test scenarios.
- Returns:
The compiled
ExecutionPlan.
- loom.testing.agent_engine_contract_suite(engine_factory)[source]¶
Run the shared
AgentEnginecontract suite (FR-048).Every check exercises only the protocol surface and the
loom.aivalue types, never an engine’s internals, so the same suite validates the fake and any real engine adapter. Checks:run()result and usage shape, exactly-one-terminal streams for success and failure, usage carried only byFinalEvent, the full run-time error-code taxonomy with FR-028 retriability, deterministic stream closure on early exit, and a knownhealth()status.The function is synchronous by design: each check runs in its own fresh event loop via
asyncio.run, so streams closed by one check can never leak into the next. Call it from a synchronous test; calling it from an async test with a running loop raisesRuntimeError.- Parameters:
engine_factory (Callable[[ContractScenario], AgentEngine]) – Builds one engine exhibiting the behaviour a
ContractScenariodescribes; called once per check invocation.- Raises:
AssertionError – If a check fails; the message names the check.
- Return type:
None
- loom.testing.serialize_plan(plan)[source]¶
Produce a deterministic, JSON-serialisable snapshot of an
ExecutionPlan.All keys are sorted alphabetically so the output is stable across runs regardless of insertion order. Types and callables are encoded as their fully qualified
module.qualnamestring.- Parameters:
plan (ExecutionPlan) – Compiled execution plan to serialise.
- Returns:
Dictionary containing only JSON-primitive values (str, int, list, dict, None). Suitable for
json.dumpscomparison.- Return type:
Example:
snapshot = serialize_plan(compiler.get_plan(MyUseCase)) assert snapshot["use_case"] == "my_app.use_cases.MyUseCase"