wabot-testing 0.1.0

Test harnesses for Wabot: a scriptable LLM adapter plus chat-bot and agent harnesses that drive the real production paths.
Documentation
//! # wabot-testing
//!
//! Test support for the LLM half of the framework. Port of the
//! harnesses in `wabot-ts/src/testing`.
//!
//! Without these, testing a mindset means hand-rolling a fake adapter,
//! an in-RAM memory and the container wiring — which is enough work
//! that the tests don't get written, and enough duplication that each
//! one drifts from what production does. (This crate exists because
//! the port's own tests kept rebuilding exactly that scaffolding.)
//!
//! ```ignore
//! let harness = ChatBotHarness::builder(Arc::new(SupportMindset))
//!     .tools(OrderTools::register_tools(&container))
//!     .container(container)
//!     .build();
//!
//! harness.adapter().call_tool("read_order", json!({ "id": 7 }));
//! harness.adapter().reply("It shipped yesterday.");
//!
//! let turn = harness.send("where is my order?").await?;
//! assert_eq!(turn.text(), "It shipped yesterday.");
//! assert!(turn.called("read_order"));
//! ```
//!
//! ## The one design rule
//!
//! **A harness wires production types together; it never
//! reimplements them.** [`ChatBotHarness`] holds a real `ChatBot` and
//! a real `MindsetOperator`; [`AgentHarness::for_agent`] returns the
//! very `AgentBuilder` an application uses. A harness that
//! reimplemented the loop would be a second implementation, free to
//! drift from the one that ships — and a test passing against the
//! drifted copy is worse than no test.
//!
//! Only two things are substitutes, and both are deliberate: the model
//! ([`MockChatAdapter`], because a real one is neither deterministic
//! nor free) and storage ([`TestChatMemory`], which is the in-memory
//! implementation plus the ability to read it back).
//!
//! ## REST
//!
//! ```ignore
//! let harness = RestHarness::new(UserController::register_routes(&container, Router::new()));
//! harness.post("/users").json(&body).send().await.assert_status(StatusCode::CREATED);
//! ```
//!
//! No port is bound — axum's router is a `tower::Service`, so the
//! request is driven straight through it. The stack it drives is built
//! by the same function `run_rest_controllers` uses, so the harness
//! can't accidentally test an application the deployment doesn't have.
//!
//! ## UI
//!
//! ```ignore
//! let page = harness.get("/notes").await;
//! page.assert_contains("<h1>Notes</h1>");
//! assert_eq!(page.island_props("notes-form"), Some(json!({ "count": 2 })));
//! ```
//!
//! Islands aren't hydrated — there is no browser. What is checked is
//! the server's half of the contract: the host element, its id and its
//! props. A mismatch there is what "the island silently never
//! appeared" looks like from the server side.
//!
//! ## Async jobs
//!
//! ```ignore
//! let harness = AsyncHarness::builder().command(entry).build();
//! harness.execute(&SendEmail { … }).await.assert_succeeded();
//! ```
//!
//! No polling workers and no database, but the **real `JobRunner`** —
//! so the state transitions, the retry decision and the restored audit
//! actor are the production ones. TS calls the handler directly and
//! skips all of that.
//!
//! ## Add it under dev-dependencies
//!
//! ```toml
//! [dev-dependencies]
//! wabot-testing = "0.1"
//! ```
//!
//! Through the umbrella it is `wabot::testing`, behind the `testing`
//! feature — off by default, so nothing ships a mock adapter to
//! production by accident.

pub mod agent;
/// The async/cron harness. Behind `async-jobs` (on by default).
#[cfg(feature = "async-jobs")]
pub mod async_jobs;
pub mod chat_bot;
pub mod conformance;
pub mod llm_judge;
pub mod memory;
pub mod mock_adapter;
/// The REST harness. Behind the `rest` feature (on by default) so a
/// project testing only its chat stack doesn't compile axum and tower
/// into its test binaries.
#[cfg(feature = "rest")]
pub mod rest;
/// The UI harness — pages, islands, boosted navigation and actions.
/// Behind the `ui` feature (on by default).
#[cfg(feature = "ui")]
pub mod ui;

pub use agent::{AgentHarness, AgentHarnessBuilder};
#[cfg(feature = "async-jobs")]
pub use async_jobs::{AsyncHarness, AsyncHarnessBuilder, FinishedJob};
pub use chat_bot::{ChatBotHarness, ChatBotHarnessBuilder, ChatTurn, IntoChatMessage};
pub use llm_judge::{render_transcript, JudgeError, LlmJudge, Transcript, Verdict};
pub use memory::TestChatMemory;
pub use mock_adapter::{MockChatAdapter, NoArgs, RecordedRequest, ScriptedTurn, ToArguments};
#[cfg(feature = "rest")]
pub use rest::{RequestBuilder, RestHarness, TestResponse};
#[cfg(feature = "ui")]
pub use ui::{Fragment, Page, UiHarness};

#[cfg(test)]
mod tests;