use langchainrust::{
CallbackHandler, CallbackManager, FileCallbackHandler, LogFormat, Message, RunTree, RunType,
};
use std::sync::Arc;
use tempfile::NamedTempFile;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_handler_creates_log_file() {
let temp_file = NamedTempFile::new().unwrap();
let handler = FileCallbackHandler::new(temp_file.path());
assert!(handler.is_ok(), "应成功创建 FileCallbackHandler");
assert!(temp_file.path().exists(), "日志文件应存在");
}
#[test]
fn test_file_handler_format_configuration() {
let temp_file = NamedTempFile::new().unwrap();
let handler_default = FileCallbackHandler::new(temp_file.path()).unwrap();
let handler_json = FileCallbackHandler::new(temp_file.path())
.unwrap()
.with_format(LogFormat::Json);
let handler_plain = FileCallbackHandler::new(temp_file.path())
.unwrap()
.with_format(LogFormat::Plain);
assert!(format!("{:?}", handler_default).contains("JsonLines"));
assert!(format!("{:?}", handler_json).contains("Json"));
assert!(format!("{:?}", handler_plain).contains("Plain"));
}
#[tokio::test]
async fn test_json_lines_format_writes_llm_events() {
let temp_file = NamedTempFile::new().unwrap();
let handler = FileCallbackHandler::new(temp_file.path())
.unwrap()
.with_format(LogFormat::JsonLines);
let run = RunTree::new(
"llm_run_001",
RunType::Llm,
serde_json::json!({
"model": "gpt-4",
"prompt": "Hello"
}),
);
let messages = vec![Message::system("You are helpful"), Message::human("Hello")];
handler.on_llm_start(&run, &messages).await;
handler.on_llm_end(&run, "Hi there!").await;
let content = std::fs::read_to_string(temp_file.path()).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2, "应写入两行日志");
assert!(lines[0].contains("llm_start"));
assert!(lines[0].contains("\"timestamp\""));
assert!(lines[0].contains("\"run_name\":\"llm_run_001\""));
assert!(lines[1].contains("llm_end"));
assert!(lines[1].contains("\"response_length\":9")); }
#[tokio::test]
async fn test_plain_format_writes_human_readable_text() {
let temp_file = NamedTempFile::new().unwrap();
let handler = FileCallbackHandler::new(temp_file.path())
.unwrap()
.with_format(LogFormat::Plain);
let run = RunTree::new("tool_run", RunType::Tool, serde_json::json!({}));
handler.on_tool_start(&run, "calculator", "1 + 2").await;
let content = std::fs::read_to_string(temp_file.path()).unwrap();
assert!(content.contains("tool_start"), "应包含事件名称");
assert!(content.contains("tool_run"), "应包含运行名称");
assert!(content.contains("["), "应包含时间戳括号");
}
#[tokio::test]
async fn test_all_callback_events_written_to_file() {
let temp_file = NamedTempFile::new().unwrap();
let handler = FileCallbackHandler::new(temp_file.path()).unwrap();
let run = RunTree::new("test_run", RunType::Chain, serde_json::json!({}));
handler.on_run_start(&run).await;
handler.on_run_end(&run).await;
handler.on_run_error(&run, "test error").await;
handler.on_llm_start(&run, &[]).await;
handler.on_llm_end(&run, "response").await;
handler.on_llm_new_token(&run, "token").await;
handler.on_llm_error(&run, "llm error").await;
handler.on_chain_start(&run, &serde_json::json!({})).await;
handler.on_chain_end(&run, &serde_json::json!({})).await;
handler.on_chain_error(&run, "chain error").await;
handler.on_tool_start(&run, "tool", "input").await;
handler.on_tool_end(&run, "output").await;
handler.on_tool_error(&run, "tool error").await;
handler.on_retriever_start(&run, "query").await;
handler
.on_retriever_end(&run, &[serde_json::json!({})])
.await;
handler.on_retriever_error(&run, "retriever error").await;
let content = std::fs::read_to_string(temp_file.path()).unwrap();
assert!(content.contains("run_start"));
assert!(content.contains("run_end"));
assert!(content.contains("run_error"));
assert!(content.contains("llm_start"));
assert!(content.contains("llm_end"));
assert!(content.contains("llm_new_token"));
assert!(content.contains("llm_error"));
assert!(content.contains("chain_start"));
assert!(content.contains("chain_end"));
assert!(content.contains("chain_error"));
assert!(content.contains("tool_start"));
assert!(content.contains("tool_end"));
assert!(content.contains("tool_error"));
assert!(content.contains("retriever_start"));
assert!(content.contains("retriever_end"));
assert!(content.contains("retriever_error"));
}
#[tokio::test]
async fn test_append_mode_accumulates_logs() {
let temp_file = NamedTempFile::new().unwrap();
let handler = FileCallbackHandler::new(temp_file.path()).unwrap();
let run1 = RunTree::new("first_run", RunType::Llm, serde_json::json!({}));
let run2 = RunTree::new("second_run", RunType::Llm, serde_json::json!({}));
handler.on_llm_start(&run1, &[]).await;
handler.on_llm_start(&run2, &[]).await;
let content = std::fs::read_to_string(temp_file.path()).unwrap();
assert!(content.contains("first_run"), "应包含第一次运行的日志");
assert!(content.contains("second_run"), "应包含第二次运行的日志");
}
#[test]
fn test_file_handler_integrates_with_callback_manager() {
let temp_file = NamedTempFile::new().unwrap();
let file_handler = Arc::new(FileCallbackHandler::new(temp_file.path()).unwrap());
let manager = CallbackManager::new().add_handler(file_handler);
assert!(!manager.is_empty(), "CallbackManager 应有 handler");
assert_eq!(manager.handlers().len(), 1, "应有 1 个 handler");
}
#[tokio::test]
async fn test_timestamp_iso_8601_format() {
let temp_file = NamedTempFile::new().unwrap();
let handler = FileCallbackHandler::new(temp_file.path()).unwrap();
let run = RunTree::new("time_test", RunType::Llm, serde_json::json!({}));
handler.on_llm_start(&run, &[]).await;
let content = std::fs::read_to_string(temp_file.path()).unwrap();
assert!(content.contains("timestamp"), "应包含 timestamp 字段");
assert!(content.contains("202"), "应包含年份");
assert!(content.contains("-"), "应包含日期分隔符");
assert!(content.contains("T"), "应包含日期时间分隔符");
}
}