use zeph_llm::any::AnyProvider;
use zeph_llm::mock::MockProvider;
use zeph_llm::provider::{ChatResponse, ToolUseRequest};
use zeph_tools::executor::ToolOutput;
use crate::agent::Agent;
use crate::agent::agent_tests::{MockChannel, MockToolExecutor, create_test_registry};
use crate::notifications::{TurnExitStatus, TurnSummary};
fn tool_output(name: &str, summary: &str) -> ToolOutput {
ToolOutput {
tool_name: name.into(),
summary: summary.to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
claim_source: None,
..Default::default()
}
}
fn tool_use_batch(n: usize) -> ChatResponse {
ChatResponse::ToolUse {
text: None,
tool_calls: (0..n)
.map(|i| ToolUseRequest {
id: format!("call-{i}"),
name: format!("tool_{i}").into(),
input: serde_json::json!({"arg": i}),
})
.collect(),
thinking_blocks: vec![],
}
}
#[tokio::test]
async fn text_only_turn_reports_zero_tool_calls_and_one_llm_request() {
let (mock, _counter) =
MockProvider::default().with_tool_use(vec![ChatResponse::Text("hello".into())]);
let provider = AnyProvider::Mock(mock);
let channel = MockChannel::new(vec![]);
let registry = create_test_registry();
let executor = MockToolExecutor::no_tools();
let mut agent = Agent::new(provider, channel, registry, None, 5, executor);
agent
.process_user_message("no tools needed".to_owned(), vec![])
.await
.unwrap();
assert_eq!(agent.runtime.lifecycle.turn_tool_calls, 0);
assert_eq!(agent.runtime.lifecycle.turn_llm_requests, 1);
}
#[tokio::test]
async fn single_turn_with_multiple_tool_calls_counts_full_batch() {
let (mock, _counter) = MockProvider::default().with_tool_use(vec![
tool_use_batch(3),
ChatResponse::Text("all done".into()),
]);
let provider = AnyProvider::Mock(mock);
let channel = MockChannel::new(vec![]);
let registry = create_test_registry();
let executor = MockToolExecutor::new(vec![
Ok(Some(tool_output("tool_0", "result-0"))),
Ok(Some(tool_output("tool_1", "result-1"))),
Ok(Some(tool_output("tool_2", "result-2"))),
]);
let mut agent = Agent::new(provider, channel, registry, None, 5, executor);
agent
.process_user_message("run three tools".to_owned(), vec![])
.await
.unwrap();
assert_eq!(agent.runtime.lifecycle.turn_tool_calls, 3);
assert_eq!(agent.runtime.lifecycle.turn_llm_requests, 2);
}
#[tokio::test]
async fn tool_call_counter_resets_between_turns_not_accumulated() {
let (mock, _counter) = MockProvider::default().with_tool_use(vec![
tool_use_batch(2),
ChatResponse::Text("first turn done".into()),
ChatResponse::Text("second turn done".into()),
]);
let provider = AnyProvider::Mock(mock);
let channel = MockChannel::new(vec![]);
let registry = create_test_registry();
let executor = MockToolExecutor::new(vec![
Ok(Some(tool_output("tool_0", "r0"))),
Ok(Some(tool_output("tool_1", "r1"))),
]);
let mut agent = Agent::new(provider, channel, registry, None, 5, executor);
agent
.process_user_message("first: run two tools".to_owned(), vec![])
.await
.unwrap();
assert_eq!(agent.runtime.lifecycle.turn_tool_calls, 2);
assert_eq!(agent.runtime.lifecycle.turn_llm_requests, 2);
agent
.process_user_message("second: no tools".to_owned(), vec![])
.await
.unwrap();
assert_eq!(
agent.runtime.lifecycle.turn_tool_calls, 0,
"turn 2 dispatched no tools; a leaked/accumulated counter would show 2 here"
);
assert_eq!(
agent.runtime.lifecycle.turn_llm_requests, 1,
"turn 2 made exactly one LLM round-trip; an accumulated counter would show 3 here"
);
}
#[test]
fn hook_env_includes_tool_calls_count() {
let summary = TurnSummary {
duration_ms: 1234,
preview: "done".to_owned(),
tool_calls: 3,
llm_requests: 2,
exit_status: TurnExitStatus::Success,
};
let env = crate::agent::build_turn_hook_env(&summary, false);
assert_eq!(env.get("ZEPH_TURN_TOOL_CALLS"), Some(&"3".to_owned()));
assert_eq!(env.get("ZEPH_TURN_LLM_REQUESTS"), Some(&"2".to_owned()));
assert_eq!(env.get("ZEPH_TURN_DURATION_MS"), Some(&"1234".to_owned()));
assert_eq!(env.get("ZEPH_TURN_STATUS"), Some(&"success".to_owned()));
}