use super::LlmProvider;
use anyhow::Result;
use async_trait::async_trait;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(test)]
pub struct MockLlmProvider {
responses: Vec<String>,
call_count: Arc<AtomicUsize>,
}
#[cfg(test)]
impl MockLlmProvider {
pub fn new(responses: Vec<impl Into<String>>) -> Self {
Self {
responses: responses.into_iter().map(Into::into).collect(),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
pub fn single(response: impl Into<String>) -> Self {
Self::new(vec![response])
}
pub fn call_count(&self) -> usize {
self.call_count.load(Ordering::SeqCst)
}
}
#[cfg(test)]
#[async_trait]
impl LlmProvider for MockLlmProvider {
async fn complete(&self, _prompt: &str, _json_mode: bool) -> Result<String> {
if self.responses.is_empty() {
anyhow::bail!("MockLlmProvider: no responses configured");
}
let idx = self.call_count.fetch_add(1, Ordering::SeqCst) % self.responses.len();
Ok(self.responses[idx].clone())
}
fn name(&self) -> &str {
"mock"
}
fn default_model(&self) -> &str {
"mock-model"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::semantic::chat_session::{ChatSession, MessageRole};
use crate::semantic::schema_agentic::{AgenticResponse, Phase, ToolCall};
#[tokio::test]
async fn test_single_turn_returns_configured_response() {
let payload = r#"{"queries": [{"command": "query \"fn main\"", "order": 1, "merge": true}]}"#;
let mock = MockLlmProvider::single(payload);
let response = mock.complete("find the main function", false).await.unwrap();
assert!(response.contains("fn main"));
assert_eq!(mock.call_count(), 1);
}
#[tokio::test]
async fn test_multi_turn_conversation_accumulates_history() {
let mock = MockLlmProvider::new(vec![
r#"{"queries": [{"command": "query \"foo\"", "order": 1, "merge": true}]}"#,
r#"{"queries": [{"command": "query \"bar\"", "order": 1, "merge": true}]}"#,
]);
let mut session = ChatSession::new("mock".to_string(), "mock-model".to_string());
session.add_user_message("Find foo".to_string());
let r1 = mock.complete("Find foo", false).await.unwrap();
session.add_answer_message(r1.clone());
session.add_user_message("Now find bar".to_string());
let r2 = mock.complete("Now find bar", false).await.unwrap();
session.add_answer_message(r2.clone());
assert_eq!(session.messages().len(), 4);
assert_eq!(session.messages()[0].role, MessageRole::User);
assert_eq!(session.messages()[1].role, MessageRole::AssistantAnswer);
assert_eq!(session.messages()[2].role, MessageRole::User);
assert_eq!(session.messages()[3].role, MessageRole::AssistantAnswer);
assert_eq!(mock.call_count(), 2);
assert!(r1.contains("foo"));
assert!(r2.contains("bar"));
let ctx = session.build_context();
assert!(ctx.contains("Find foo"));
assert!(ctx.contains("Now find bar"));
}
#[tokio::test]
async fn test_agentic_tool_call_parsed_without_network_io() {
let agentic_json = r#"{
"phase": "assessment",
"reasoning": "I need to gather context about the project structure",
"needs_context": true,
"tool_calls": [
{
"type": "gather_context",
"structure": true,
"file_types": true,
"project_type": false,
"framework": false,
"entry_points": false,
"test_layout": false,
"config_files": false,
"depth": 2
}
],
"queries": [],
"confidence": 0.0
}"#;
let mock = MockLlmProvider::single(agentic_json);
let response = mock.complete("What is the project structure?", true).await.unwrap();
let parsed: AgenticResponse = serde_json::from_str(&response).unwrap();
assert_eq!(parsed.phase, Phase::Assessment);
assert!(parsed.needs_context);
assert_eq!(parsed.tool_calls.len(), 1);
match &parsed.tool_calls[0] {
ToolCall::GatherContext { params } => {
assert!(params.structure);
assert!(params.file_types);
assert!(!params.project_type);
}
other => panic!("Expected GatherContext, got {:?}", other),
}
assert_eq!(mock.call_count(), 1);
assert_eq!(mock.name(), "mock");
assert_eq!(mock.default_model(), "mock-model");
}
#[tokio::test]
async fn test_responses_cycle_when_exhausted() {
let mock = MockLlmProvider::new(vec!["alpha", "beta"]);
let r1 = mock.complete("q1", false).await.unwrap();
let r2 = mock.complete("q2", false).await.unwrap();
let r3 = mock.complete("q3", false).await.unwrap();
assert_eq!(r1, "alpha");
assert_eq!(r2, "beta");
assert_eq!(r3, "alpha");
assert_eq!(mock.call_count(), 3);
}
#[tokio::test]
async fn test_empty_mock_returns_error() {
let mock = MockLlmProvider::new(Vec::<String>::new());
let result = mock.complete("anything", false).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("no responses configured"));
}
}