Skip to main content

MockApiClient

Struct MockApiClient 

Source
pub struct MockApiClient { /* private fields */ }
Expand description

A mock ApiClient that returns preconfigured streaming responses.

Use this in unit tests to exercise agent logic without making real API calls. Configure what the client returns using the builder-style methods:

For multi-turn scenarios, chain multiple MockResponse values via with_responses. Each call to stream_messages pops the next response; once exhausted the last response is repeated.

§Construction

use loopctl::testing::MockApiClient;

let client = MockApiClient::new("test-model")
    .with_text_response("I'm here to help!");

§Multi-turn

use loopctl::testing::{MockApiClient, MockResponse, MockToolCall};
use serde_json::json;

let client = MockApiClient::new("test-model").with_responses(vec![
    MockResponse {
        text: "Let me look that up.".into(),
        tool_call: Some(MockToolCall {
            id: "call_1".into(),
            name: "search".into(),
            input: json!({"query": "rust"}),
        }),
        stop_reason: "tool_use".into(),
    },
    MockResponse {
        text: "Here is what I found.".into(),
        tool_call: None,
        stop_reason: "end_turn".into(),
    },
]);

Implementations§

Source§

impl MockApiClient

Construction and builder methods for MockApiClient.

The builder pattern lets you configure mock responses fluently. All builder methods consume and return Self, so you can chain them directly after MockApiClient::new.

§Response lifecycle

new preloads the client with a single default response (text "Hello!", stop reason "end_turn"). The builder methods then mutate or replace that queue:

Source

pub fn new(model: &str) -> Self

Create a new mock client with the given model name.

Returns a client preloaded with a single default response: text "Hello!" with stop reason "end_turn" and no tool call. Use the builder methods to customize before passing the client to the code under test.

The default response is simple so that most tests only need to call with_text_response to get started.

§Example
use loopctl::api::ApiClient;
use loopctl::testing::MockApiClient;

let client = MockApiClient::new("test-model");
assert_eq!(client.model(), "test-model");
Source

pub fn with_text_response(self, text: &str) -> Self

Set the text response for the first (or only) turn.

Overwrites the text field on the initial MockResponse created by new. Simplest way to configure a single-turn mock — the model will “say” the given text and stop.

If you need to set text for multiple turns, use with_responses instead.

§Example
use loopctl::testing::MockApiClient;

let client = MockApiClient::new("test-model")
    .with_text_response("I am a test assistant.");
Source

pub fn with_tool_call(self, id: &str, name: &str, input: Value) -> Self

Add a tool call to the first (or only) response.

The mock will emit a tool_use content block and set the stop reason to "tool_use" so the agent loop knows to execute the tool. The id, name, and input parameters map directly to the fields on MockToolCall.

The tool name should match a tool registered in the agent’s ToolRegistry — otherwise the agent loop will fail when it tries to dispatch the call.

§Example
use loopctl::testing::MockApiClient;
use serde_json::json;

let client = MockApiClient::new("test-model")
    .with_tool_call("call_1", "bash", json!({"command": "ls"}));
Source

pub fn with_stop_reason(self, reason: &str) -> Self

Override the stop reason on the first (or only) response.

Common values are "end_turn" (default) and "tool_use".

Note that with_tool_call automatically sets the stop reason to "tool_use", so this method should be used when you want a different value.

§Example
use loopctl::testing::MockApiClient;

let client = MockApiClient::new("test-model")
    .with_stop_reason("max_tokens");
Source

pub fn with_responses(self, responses: Vec<MockResponse>) -> Self

Set the full response queue for multi-turn behaviour.

Each call to stream_messages pops the front entry. When only one entry remains it is cloned and reused, so the mock never panics on an empty queue.

If responses is empty the call is a no-op (the default response is retained). Recommended way to set up complex multi-turn scenarios where the model needs to reply differently across successive turns.

§Example
use loopctl::testing::{MockApiClient, MockResponse};

let client = MockApiClient::new("test-model").with_responses(vec![
    MockResponse {
        text: "First reply".into(),
        tool_call: None,
        stop_reason: "end_turn".into(),
    },
    MockResponse {
        text: "Second reply".into(),
        tool_call: None,
        stop_reason: "end_turn".into(),
    },
]);
Source

pub fn with_error(self, error: &str) -> Self

Simulate an API error on every call.

Once set, both stream_messages and create_message will return an ApiError immediately. This overrides any response configuration.

Useful for exercising agent error-handling and retry paths. The error message is passed through to the ApiError so tests can assert on the specific error string.

§Example
use loopctl::testing::MockApiClient;

let client = MockApiClient::new("test-model")
    .with_error("rate limit exceeded");

Trait Implementations§

Source§

impl ApiClient for MockApiClient

Source§

fn model(&self) -> String

Return the model name this mock was created with.

Called by the framework to identify which model is being used throughout the session. Always returns the string passed to MockApiClient::new — the value is not validated against any real model registry, so any string is acceptable for testing.

§Example
use loopctl::api::ApiClient;
use loopctl::testing::MockApiClient;

let client = MockApiClient::new("my-test-model");
assert_eq!(client.model(), "my-test-model");
Source§

fn set_model(&self, model: &str) -> bool

Hot-swap the mock’s model name at runtime.

Unlike the trait default (which returns false), the mock stores its model behind a mutex and updates it in place so tests can exercise BareLoop::switch_model and verify the new name is observed by subsequent model calls. Returns false (no-op) when model is empty or whitespace, matching the trait contract that an empty model is not a valid switch.

Source§

fn stream_messages( &self, _request: &StreamRequest, ) -> Pin<Box<dyn Stream<Item = Result<StreamEvent, ApiError>> + Send + 'static>>

Stream a canned sequence of StreamEvents for the next response.

Called by the agent loop to obtain the model’s reply. The mock translates the current MockResponse into the standard event sequence:

MessageStart → PartStart(text) → IndexedDelta(text) → MessagePartStop
             → [PartStart(tool_use) → IndexedDelta(input_json)
                → MessagePartStop]   (if tool_call is set)
             → MessageDelta(stop_reason, usage) → MessageStop

If with_error was called the stream contains a single ApiError event instead.

The _messages, _system, and _tools parameters are accepted for trait compatibility but ignored — the mock always returns the preconfigured response.

§Usage tokens

The mock always reports 50 input tokens and 25 output tokens in the MessageDelta event. This lets tests assert on usage data without needing a real model response.

§Example
use loopctl::api::{ApiClient, StreamRequest};
use loopctl::testing::MockApiClient;

let client = MockApiClient::new("test-model").with_text_response("Hi!");
let stream = client.stream_messages(&StreamRequest::new(vec![]));
let events: Vec<_> = futures::StreamExt::collect(stream).await;
assert!(events.len() >= 4);
Source§

fn stream_messages_with_options( &self, _request: &StreamRequest, options: RequestOptions, ) -> Pin<Box<dyn Stream<Item = Result<StreamEvent, ApiError>> + Send + 'static>>

Streaming variant that honors the per-request model override.

Accepts a response_format by serving the canned response unchanged — the mock cannot enforce a schema (it has no model), so the canned text is the structured answer, well-formed or deliberately malformed. Rejects tool_constraint loudly (a constraint shapes the tool-calling path the mock scripts). Serves the response under options.model when set — the mock’s stand-in for the wire-level model switch real clients perform, so fallback-routing tests built on the mock observe the routed model instead of silently receiving the constructor’s name.

§Example
use futures::StreamExt;
use loopctl::api::{ApiClient, StreamRequest};
use loopctl::structured::RequestOptions;
use loopctl::testing::MockApiClient;

let client = MockApiClient::new("primary").with_text_response("Hi!");
let opts = RequestOptions::default().with_model("fallback");
let stream = client.stream_messages_with_options(&StreamRequest::new(vec![]), opts);
let events: Vec<_> = stream.collect().await;
Source§

fn create_message( &self, _request: &StreamRequest, ) -> Pin<Box<dyn Future<Output = Result<NonStreamingResponse, ApiError>> + Send + '_>>

Return a canned non-streaming NonStreamingResponse.

Called by code paths that use the non-streaming API. The mock translates the current MockResponse into a typed NonStreamingResponse carrying an assistant Message built from the response’s text and optional tool call, plus the stop reason parsed from MockResponse::stop_reason.

If with_error was called the future resolves to an ApiError instead, bypassing the response queue entirely.

Usage mirrors the streaming twin: the response reports 50 input tokens and 25 output tokens, so tests assert the same usage on either path.

The _request parameter is accepted for trait compatibility but ignored.

§Example
use loopctl::api::{ApiClient, StreamRequest};
use loopctl::testing::MockApiClient;

let client = MockApiClient::new("test-model").with_text_response("Hi!");
let result = client.create_message(&StreamRequest::new(vec![])).await;
assert_eq!(result.unwrap().message.text_content(), "Hi!");
Source§

fn create_message_with_options( &self, request: &StreamRequest, options: RequestOptions, ) -> Pin<Box<dyn Future<Output = Result<NonStreamingResponse, ApiError>> + Send + '_>>

Non-streaming variant that accepts the per-request model override.

Accepts a response_format by serving the canned response unchanged (see the streaming twin for why), and rejects tool_constraint loudly. The per-request model is accepted without changing the response: a NonStreamingResponse carries no model field, so there is nothing to vary — accepting the override keeps fallback-routing tests on the non-streaming path running instead of failing on a field the mock does forward on its streaming twin.

Source§

fn base_url(&self) -> String

The provider’s base URL, used as the per-provider rate-limit bucket key. Read more
Source§

fn extract_structured(&self, message: &Message) -> Value

Extract the structured-output payload from an assistant message. Read more
Source§

impl Clone for MockApiClient

Source§

fn clone(&self) -> MockApiClient

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more