use crate::types::{RunLog, StepKind};
use klieo_core::test_utils::{FakeLlmClient, FakeLlmStep};
pub fn fake_llm_from_runlog(name: impl Into<String>, log: &RunLog) -> FakeLlmClient {
let steps: Vec<FakeLlmStep> = log
.steps
.iter()
.filter(|s| s.kind == StepKind::LlmCall)
.map(|s| {
let text = s
.output
.as_str()
.map(String::from)
.unwrap_or_else(|| s.output.to_string());
FakeLlmStep::Text(text)
})
.collect();
FakeLlmClient::new(name).with_steps(steps)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{RunStatus, Step, Usage};
use chrono::Utc;
use klieo_core::llm::ChatRequest;
use klieo_core::{LlmClient, RunId};
fn step(idx: u32, kind: StepKind, output: serde_json::Value) -> Step {
Step {
idx,
kind,
name: None,
prompt_tokens: None,
completion_tokens: None,
cost_usd: None,
input: serde_json::Value::Null,
output,
error: None,
latency: std::time::Duration::ZERO,
span_id: None,
}
}
fn run_log_with_mixed_steps() -> RunLog {
let now = Utc::now();
RunLog {
run_id: RunId::new(),
agent: "t".into(),
started_at: now,
finished_at: Some(now),
status: RunStatus::Completed,
steps: vec![
step(0, StepKind::LlmCall, serde_json::json!("hello")),
step(1, StepKind::ToolCall, serde_json::json!({"hit": true})),
step(2, StepKind::LlmCall, serde_json::json!("world")),
],
tokens: Usage::default(),
cost_estimate: None,
}
}
#[tokio::test]
async fn fake_llm_from_runlog_filters_to_llm_call_steps_only() {
let log = run_log_with_mixed_steps();
let fake = fake_llm_from_runlog("replay-test", &log);
assert_eq!(LlmClient::name(&fake), "replay-test");
let r1 = fake.complete(ChatRequest::new(vec![])).await.unwrap();
assert_eq!(
r1.message.content, "hello",
"first replay = first LlmCall.output"
);
let r2 = fake.complete(ChatRequest::new(vec![])).await.unwrap();
assert_eq!(
r2.message.content, "world",
"second replay = second LlmCall.output (skipping ToolCall)"
);
let err = fake.complete(ChatRequest::new(vec![])).await;
assert!(
err.is_err(),
"script of two responses must exhaust on third call"
);
}
#[tokio::test]
async fn fake_llm_from_runlog_serialises_non_string_output_as_json() {
let now = Utc::now();
let log = RunLog {
run_id: RunId::new(),
agent: "t".into(),
started_at: now,
finished_at: Some(now),
status: RunStatus::Completed,
steps: vec![step(0, StepKind::LlmCall, serde_json::json!({"k": 1}))],
tokens: Usage::default(),
cost_estimate: None,
};
let fake = fake_llm_from_runlog("replay-json", &log);
let r = fake.complete(ChatRequest::new(vec![])).await.unwrap();
assert_eq!(
r.message.content, "{\"k\":1}",
"non-string output must serialise back to its JSON text"
);
}
}