Skip to main content

wabot_testing/
lib.rs

1//! # wabot-testing
2//!
3//! Test support for the LLM half of the framework. Port of the
4//! harnesses in `wabot-ts/src/testing`.
5//!
6//! Without these, testing a mindset means hand-rolling a fake adapter,
7//! an in-RAM memory and the container wiring — which is enough work
8//! that the tests don't get written, and enough duplication that each
9//! one drifts from what production does. (This crate exists because
10//! the port's own tests kept rebuilding exactly that scaffolding.)
11//!
12//! ```ignore
13//! let harness = ChatBotHarness::builder(Arc::new(SupportMindset))
14//!     .tools(OrderTools::register_tools(&container))
15//!     .container(container)
16//!     .build();
17//!
18//! harness.adapter().call_tool("read_order", json!({ "id": 7 }));
19//! harness.adapter().reply("It shipped yesterday.");
20//!
21//! let turn = harness.send("where is my order?").await?;
22//! assert_eq!(turn.text(), "It shipped yesterday.");
23//! assert!(turn.called("read_order"));
24//! ```
25//!
26//! ## The one design rule
27//!
28//! **A harness wires production types together; it never
29//! reimplements them.** [`ChatBotHarness`] holds a real `ChatBot` and
30//! a real `MindsetOperator`; [`AgentHarness::for_agent`] returns the
31//! very `AgentBuilder` an application uses. A harness that
32//! reimplemented the loop would be a second implementation, free to
33//! drift from the one that ships — and a test passing against the
34//! drifted copy is worse than no test.
35//!
36//! Only two things are substitutes, and both are deliberate: the model
37//! ([`MockChatAdapter`], because a real one is neither deterministic
38//! nor free) and storage ([`TestChatMemory`], which is the in-memory
39//! implementation plus the ability to read it back).
40//!
41//! ## REST
42//!
43//! ```ignore
44//! let harness = RestHarness::new(UserController::register_routes(&container, Router::new()));
45//! harness.post("/users").json(&body).send().await.assert_status(StatusCode::CREATED);
46//! ```
47//!
48//! No port is bound — axum's router is a `tower::Service`, so the
49//! request is driven straight through it. The stack it drives is built
50//! by the same function `run_rest_controllers` uses, so the harness
51//! can't accidentally test an application the deployment doesn't have.
52//!
53//! ## UI
54//!
55//! ```ignore
56//! let page = harness.get("/notes").await;
57//! page.assert_contains("<h1>Notes</h1>");
58//! assert_eq!(page.island_props("notes-form"), Some(json!({ "count": 2 })));
59//! ```
60//!
61//! Islands aren't hydrated — there is no browser. What is checked is
62//! the server's half of the contract: the host element, its id and its
63//! props. A mismatch there is what "the island silently never
64//! appeared" looks like from the server side.
65//!
66//! ## Async jobs
67//!
68//! ```ignore
69//! let harness = AsyncHarness::builder().command(entry).build();
70//! harness.execute(&SendEmail { … }).await.assert_succeeded();
71//! ```
72//!
73//! No polling workers and no database, but the **real `JobRunner`** —
74//! so the state transitions, the retry decision and the restored audit
75//! actor are the production ones. TS calls the handler directly and
76//! skips all of that.
77//!
78//! ## Add it under dev-dependencies
79//!
80//! ```toml
81//! [dev-dependencies]
82//! wabot-testing = "0.1"
83//! ```
84//!
85//! Through the umbrella it is `wabot::testing`, behind the `testing`
86//! feature — off by default, so nothing ships a mock adapter to
87//! production by accident.
88
89pub mod agent;
90/// The async/cron harness. Behind `async-jobs` (on by default).
91#[cfg(feature = "async-jobs")]
92pub mod async_jobs;
93pub mod chat_bot;
94pub mod conformance;
95pub mod llm_judge;
96pub mod memory;
97pub mod mock_adapter;
98/// The REST harness. Behind the `rest` feature (on by default) so a
99/// project testing only its chat stack doesn't compile axum and tower
100/// into its test binaries.
101#[cfg(feature = "rest")]
102pub mod rest;
103/// The UI harness — pages, islands, boosted navigation and actions.
104/// Behind the `ui` feature (on by default).
105#[cfg(feature = "ui")]
106pub mod ui;
107
108pub use agent::{AgentHarness, AgentHarnessBuilder};
109#[cfg(feature = "async-jobs")]
110pub use async_jobs::{AsyncHarness, AsyncHarnessBuilder, FinishedJob};
111pub use chat_bot::{ChatBotHarness, ChatBotHarnessBuilder, ChatTurn, IntoChatMessage};
112pub use llm_judge::{render_transcript, JudgeError, LlmJudge, Transcript, Verdict};
113pub use memory::TestChatMemory;
114pub use mock_adapter::{MockChatAdapter, NoArgs, RecordedRequest, ScriptedTurn, ToArguments};
115#[cfg(feature = "rest")]
116pub use rest::{RequestBuilder, RestHarness, TestResponse};
117#[cfg(feature = "ui")]
118pub use ui::{Fragment, Page, UiHarness};
119
120#[cfg(test)]
121mod tests;