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
//! Drive a real agent in a test. Port of
//! `wabot-ts/src/testing/agentHarness.ts`.

use std::sync::Arc;

use wabot_core::injection::Container;
use wabot_feature_agent::{register_agents, Agent, AgentBuilder, AgentFactory, AgentSession};
use wabot_feature_chat_bot::ChatAdapter;

use crate::mock_adapter::MockChatAdapter;

/// Runs a real agent — real [`AgentFactory`], real session loop, real
/// tool gating and answer validation — against a scriptable adapter.
///
/// It is a **thin wrapper over the production path**: [`for_agent`]
/// hands back the same [`AgentBuilder`] production uses, so there is no
/// parallel session logic that could drift from what ships. That is the
/// property worth protecting; everything else here is convenience.
///
/// ```ignore
/// let harness = AgentHarness::new(triage_agent);
/// harness.adapter().call_tool(ANSWER_TOOL_NAME, json!({ "urgency": "high" }));
///
/// let triage: Triage = harness
///     .for_agent()
///     .for_mindset()               // the delegation path, gating and all
///     .allow_tools(["read_order"])
///     .session()
///     .await
///     .ask("How urgent is this?")
///     .await?;
/// ```
///
/// [`for_agent`]: AgentHarness::for_agent
pub struct AgentHarness {
    agent: Arc<dyn Agent>,
    adapter: Arc<MockChatAdapter>,
    factory: Arc<AgentFactory>,
    container: Container,
}

impl AgentHarness {
    pub fn new(agent: Arc<dyn Agent>) -> Self {
        Self::builder(agent).build()
    }

    pub fn builder(agent: Arc<dyn Agent>) -> AgentHarnessBuilder {
        AgentHarnessBuilder {
            agent,
            container: None,
            adapter: None,
        }
    }

    pub fn adapter(&self) -> &Arc<MockChatAdapter> {
        &self.adapter
    }

    /// The container the agent's tools resolve from — also where the
    /// agent-tools provider is registered, so a mindset built from it
    /// can delegate.
    pub fn container(&self) -> &Container {
        &self.container
    }

    pub fn factory(&self) -> &Arc<AgentFactory> {
        &self.factory
    }

    /// The production builder for this agent: chain `for_mindset()`,
    /// `allow_tools()`, `deny_tools()`, `with_budget()`,
    /// `with_context()`, then `session()`.
    pub fn for_agent(&self) -> AgentBuilder {
        self.factory.for_agent(self.agent.clone())
    }

    /// Shortcut for a session with default gating and budget.
    pub async fn session(&self) -> AgentSession {
        self.for_agent().session().await
    }
}

pub struct AgentHarnessBuilder {
    agent: Arc<dyn Agent>,
    container: Option<Container>,
    adapter: Option<Arc<MockChatAdapter>>,
}

impl AgentHarnessBuilder {
    /// The container the agent's tools resolve from. Register their
    /// dependencies there before building.
    pub fn container(mut self, container: Container) -> Self {
        self.container = Some(container);
        self
    }

    pub fn adapter(mut self, adapter: Arc<MockChatAdapter>) -> Self {
        self.adapter = Some(adapter);
        self
    }

    pub fn build(self) -> AgentHarness {
        let container = self.container.unwrap_or_default();
        let adapter = self.adapter.unwrap_or_else(MockChatAdapter::arc);
        // The real installer, so a harness-built container can also
        // back a delegating mindset — the same call an app makes.
        let factory = register_agents(&container, adapter.clone() as Arc<dyn ChatAdapter>);

        AgentHarness {
            agent: self.agent,
            adapter,
            factory,
            container,
        }
    }
}