use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use crate::config::LlmConfig;
use crate::llm::client::LlmClient;
use crate::llm::error::LlmError;
use crate::test_support::{cfg_for, sse};
#[test]
fn new_returns_not_configured_when_disabled() {
let cfg = LlmConfig {
enabled: false,
endpoint: Some("http://host/v1".into()),
model: Some("m".into()),
..LlmConfig::default()
};
let err = LlmClient::new(&cfg).unwrap_err();
assert!(
matches!(err, LlmError::NotConfigured(_)),
"expected NotConfigured, got {err:?}"
);
}
#[test]
fn new_returns_not_configured_when_endpoint_missing() {
let cfg = LlmConfig {
enabled: true,
endpoint: None,
model: Some("m".into()),
..LlmConfig::default()
};
let err = LlmClient::new(&cfg).unwrap_err();
assert!(
matches!(err, LlmError::NotConfigured(_)),
"expected NotConfigured, got {err:?}"
);
}
#[test]
fn new_returns_not_configured_when_model_missing() {
let cfg = LlmConfig {
enabled: true,
endpoint: Some("http://host/v1".into()),
model: None,
..LlmConfig::default()
};
let err = LlmClient::new(&cfg).unwrap_err();
assert!(
matches!(err, LlmError::NotConfigured(_)),
"expected NotConfigured, got {err:?}"
);
}
#[tokio::test]
async fn unset_max_tokens_is_absent_from_the_request() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/chat/completions"))
.respond_with(ResponseTemplate::new(200).set_body_raw(sse(&["{}"]), "text/event-stream"))
.mount(&server)
.await;
let cfg = cfg_for(&server, "m", 3);
assert_eq!(cfg.max_tokens, None, "fixture must leave the cap unset");
let client = LlmClient::new(&cfg).expect("client");
let _ = client.complete_json("sys", "user").await;
let reqs = server.received_requests().await.expect("log");
let body: serde_json::Value =
serde_json::from_slice(&reqs[0].body).expect("request body is JSON");
assert!(
body.get("max_tokens").is_none(),
"an unset cap must be absent from the wire entirely, got {:?}",
body.get("max_tokens")
);
}
#[tokio::test]
async fn a_configured_max_tokens_is_forwarded() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/chat/completions"))
.respond_with(ResponseTemplate::new(200).set_body_raw(sse(&["{}"]), "text/event-stream"))
.mount(&server)
.await;
let mut cfg = cfg_for(&server, "m", 3);
cfg.max_tokens = Some(1234);
let client = LlmClient::new(&cfg).expect("client");
let _ = client.complete_json("sys", "user").await;
let reqs = server.received_requests().await.expect("log");
let body: serde_json::Value =
serde_json::from_slice(&reqs[0].body).expect("request body is JSON");
assert_eq!(
body["max_tokens"].as_u64(),
Some(1234),
"a configured cap must reach the model"
);
}
#[test]
fn an_absent_protocol_builds_the_openai_client() {
let cfg = LlmConfig {
endpoint: Some("http://host/v1".into()),
model: Some("m".into()),
..LlmConfig::default()
};
let client = LlmClient::new(&cfg).expect("builds");
assert_eq!(client.protocol(), open_agent::ApiProtocol::OpenAiChat);
}
#[test]
fn a_named_protocol_reaches_the_client() {
let cfg = LlmConfig {
endpoint: Some("https://api.kimi.com/coding/v1".into()),
model: Some("k3".into()),
protocol: Some("anthropic".into()),
..LlmConfig::default()
};
let client = LlmClient::new(&cfg).expect("builds");
assert_eq!(client.protocol(), open_agent::ApiProtocol::Anthropic);
}
#[test]
fn an_unknown_protocol_is_not_configured_rather_than_defaulted() {
let cfg = LlmConfig {
endpoint: Some("http://host/v1".into()),
model: Some("m".into()),
protocol: Some("antropic".into()),
..LlmConfig::default()
};
let err = LlmClient::new(&cfg).unwrap_err();
assert!(
matches!(err, LlmError::NotConfigured(ref m) if m.contains("antropic")),
"expected NotConfigured naming the value, got {err:?}"
);
}
#[test]
fn an_unset_temperature_stays_unset_on_the_client() {
let cfg = LlmConfig {
endpoint: Some("http://host/v1".into()),
model: Some("m".into()),
..LlmConfig::default()
};
let client = LlmClient::new(&cfg).expect("builds");
assert_eq!(client.temperature(), None);
}