//! Trace format / infrastructure tests. //! //! These tests verify JSON deserialization and backward compatibility of the //! trace format. They do NOT require a rig, database, or the `libsql` feature. mod support; mod trace_format_tests { use crate::support::trace_llm::{LlmTrace, TraceExpects}; /// A trace with only user_input steps and no playable steps deserializes. #[test] fn all_user_input_steps() { let json = r#"{ "model_name": "recorded-all-user-input", "memory_snapshot": [], "steps": [ { "response": { "type": "user_input", "content": "hello" } }, { "response": { "type": "user_input", "content": "world" } } ] }"#; let trace: LlmTrace = serde_json::from_str(json).unwrap(); assert_eq!(trace.steps.len(), 2); assert_eq!(trace.playable_steps().len(), 0); } /// Backward compatibility: a trace without the new fields loads correctly. #[test] fn backward_compat_no_memory_snapshot() { let json = r#"{ "model_name": "old-format", "steps": [ { "response": { "type": "text", "content": "hello", "input_tokens": 10, "output_tokens": 5 } } ] }"#; let trace: LlmTrace = serde_json::from_str(json).unwrap(); assert!(trace.memory_snapshot.is_empty()); assert!(trace.http_exchanges.is_empty()); assert!(trace.expects.is_empty()); assert_eq!(trace.playable_steps().len(), 1); } /// Expects round-trips through JSON serialization. #[test] fn expects_deserialization() { let json = r#"{ "model_name": "expects-test", "expects": { "response_contains": ["hello", "world"], "tools_used": ["echo"], "all_tools_succeeded": true, "min_responses": 1, "tool_results_contain": { "echo": "greeting" } }, "steps": [ { "response": { "type": "text", "content": "hello world", "input_tokens": 10, "output_tokens": 5 } } ] }"#; let trace: LlmTrace = serde_json::from_str(json).unwrap(); assert!(!trace.expects.is_empty()); assert_eq!(trace.expects.response_contains, vec!["hello", "world"]); assert_eq!(trace.expects.tools_used, vec!["echo"]); assert_eq!(trace.expects.all_tools_succeeded, Some(true)); assert_eq!(trace.expects.min_responses, Some(1)); assert_eq!( trace .expects .tool_results_contain .get("echo") .map(|s| s.as_str()), Some("greeting") ); // Round-trip: serialize back and deserialize again. let serialized = serde_json::to_string(&trace).unwrap(); let trace2: LlmTrace = serde_json::from_str(&serialized).unwrap(); assert_eq!( trace2.expects.response_contains, trace.expects.response_contains ); assert_eq!(trace2.expects.tools_used, trace.expects.tools_used); } /// A trace without `expects` loads with empty defaults. #[test] fn expects_default_empty() { let json = r#"{ "model_name": "no-expects", "steps": [ { "response": { "type": "text", "content": "hi", "input_tokens": 1, "output_tokens": 1 } } ] }"#; let trace: LlmTrace = serde_json::from_str(json).unwrap(); assert!(trace.expects.is_empty()); } /// Per-turn expects deserializes correctly. #[test] fn per_turn_expects() { let json = r#"{ "model_name": "turn-expects", "turns": [ { "user_input": "hello", "expects": { "response_contains": ["greeting"], "tools_not_used": ["shell"] }, "steps": [ { "response": { "type": "text", "content": "greeting back", "input_tokens": 1, "output_tokens": 1 } } ] } ] }"#; let trace: LlmTrace = serde_json::from_str(json).unwrap(); assert_eq!(trace.turns.len(), 1); assert!(!trace.turns[0].expects.is_empty()); assert_eq!(trace.turns[0].expects.response_contains, vec!["greeting"]); assert_eq!(trace.turns[0].expects.tools_not_used, vec!["shell"]); } /// TraceExpects::is_empty() returns true for default. #[test] fn trace_expects_is_empty() { let e = TraceExpects::default(); assert!(e.is_empty()); } /// Flat steps with UserInput markers are split into multiple turns. #[test] fn recorded_multi_turn_splits_at_user_input() { let json = r#"{ "model_name": "test", "steps": [ { "response": { "type": "user_input", "content": "hello" } }, { "response": { "type": "text", "content": "hi", "input_tokens": 10, "output_tokens": 5 } }, { "response": { "type": "user_input", "content": "bye" } }, { "response": { "type": "text", "content": "goodbye", "input_tokens": 20, "output_tokens": 5 } } ] }"#; let trace: LlmTrace = serde_json::from_str(json).unwrap(); assert_eq!(trace.turns.len(), 2); assert_eq!(trace.turns[0].user_input, "hello"); assert_eq!(trace.turns[0].steps.len(), 1); assert_eq!(trace.turns[1].user_input, "bye"); assert_eq!(trace.turns[1].steps.len(), 1); } /// Steps before the first UserInput get placeholder input. #[test] fn steps_before_first_user_input_get_placeholder() { let json = r#"{ "model_name": "test", "steps": [ { "response": { "type": "text", "content": "preamble", "input_tokens": 5, "output_tokens": 3 } }, { "response": { "type": "user_input", "content": "hello" } }, { "response": { "type": "text", "content": "hi", "input_tokens": 10, "output_tokens": 5 } } ] }"#; let trace: LlmTrace = serde_json::from_str(json).unwrap(); assert_eq!(trace.turns.len(), 2); assert_eq!(trace.turns[0].user_input, "(test input)"); assert_eq!(trace.turns[0].steps.len(), 1); assert_eq!(trace.turns[1].user_input, "hello"); assert_eq!(trace.turns[1].steps.len(), 1); } }