use super::*;
use crate::core::sm::providers::ChatMessage;
use crate::core::sm::providers::test_support::read_full_request;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
async fn spawn_mock(status_line: &'static str, body: String) -> String {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("addr");
tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.expect("accept");
read_full_request(&mut sock).await;
let resp = format!(
"{status_line}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = sock.write_all(resp.as_bytes()).await;
let _ = sock.shutdown().await;
});
format!("http://{addr}")
}
#[test]
fn new_rejects_empty_key() {
let result = OpenRouterProvider::new("", "anthropic/claude-sonnet-4-6");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, SmLlmError::AccessDenied(_)));
assert!(err.is_alarm());
}
#[test]
fn new_succeeds_with_valid_key() {
let p = OpenRouterProvider::new("sk-test", "anthropic/claude-haiku").expect("ok");
assert_eq!(p.name(), "openrouter");
}
#[tokio::test]
async fn complete_roundtrips_against_mock() {
let body = serde_json::json!({
"choices": [{"message": {"content": "delegated to engineer session"}}],
"usage": {"prompt_tokens": 1000, "completion_tokens": 500},
"model": "anthropic/claude-sonnet-4-6"
})
.to_string();
let base = spawn_mock("HTTP/1.1 200 OK", body).await;
let provider =
OpenRouterProvider::with_base_url("sk-test", "anthropic/claude-sonnet-4-6", base)
.expect("provider");
let resp = provider
.complete(LlmRequest {
model: "anthropic/claude-sonnet-4-6".to_string(),
system: "You are the SM.".to_string(),
messages: vec![ChatMessage {
role: "user".to_string(),
content: "plan this goal".to_string(),
}],
temperature: 0.3,
max_tokens: 1024,
})
.await
.expect("complete ok");
assert_eq!(resp.text, "delegated to engineer session");
assert_eq!(resp.input_tokens, 1000);
assert_eq!(resp.output_tokens, 500);
assert!(
(resp.cost_usd - 0.0105_f64).abs() < 1e-9,
"expected $0.0105, got {}",
resp.cost_usd
);
}
#[tokio::test]
async fn complete_maps_http_errors() {
let base = spawn_mock("HTTP/1.1 429 Too Many Requests", "rate limited".to_string()).await;
let provider = OpenRouterProvider::with_base_url("sk-test", "anthropic/claude-haiku", base)
.expect("provider");
let err = provider
.complete(LlmRequest {
model: "anthropic/claude-haiku".to_string(),
system: String::new(),
messages: vec![ChatMessage {
role: "user".to_string(),
content: "summarize".to_string(),
}],
temperature: 0.3,
max_tokens: 256,
})
.await
.expect_err("429 should error");
assert!(matches!(err, SmLlmError::RateLimited));
assert!(err.is_retryable());
assert!(!err.is_alarm());
}
#[tokio::test]
async fn complete_maps_400_to_non_retryable_validation() {
let base = spawn_mock("HTTP/1.1 400 Bad Request", "bad request body".to_string()).await;
let provider = OpenRouterProvider::with_base_url("sk-test", "anthropic/claude-haiku", base)
.expect("provider");
let err = provider
.complete(LlmRequest {
model: "anthropic/claude-haiku".to_string(),
system: String::new(),
messages: vec![ChatMessage {
role: "user".to_string(),
content: "summarize".to_string(),
}],
temperature: 0.3,
max_tokens: 256,
})
.await
.expect_err("400 should error");
assert!(matches!(err, SmLlmError::Validation(_)));
assert!(!err.is_retryable());
}