use super::*; use core_test_support::assert_regex_match; use pretty_assertions::assert_eq; use serde_json::json; #[test] fn custom_tool_calls_should_roundtrip_as_custom_outputs() { let payload = ToolPayload::Custom { input: "patch".to_string(), }; let response = FunctionToolOutput::from_text("patched".to_string(), Some(true)) .to_response_item("call-42", &payload); match response { ResponseInputItem::CustomToolCallOutput { call_id, output, .. } => { assert_eq!(call_id, "call-42"); assert_eq!(output.content_items(), None); assert_eq!(output.body.to_text().as_deref(), Some("patched")); assert_eq!(output.success, Some(true)); } other => panic!("expected CustomToolCallOutput, got {other:?}"), } } #[test] fn function_payloads_remain_function_outputs() { let payload = ToolPayload::Function { arguments: "{}".to_string(), }; let response = FunctionToolOutput::from_text("ok".to_string(), Some(true)) .to_response_item("fn-1", &payload); match response { ResponseInputItem::FunctionCallOutput { call_id, output } => { assert_eq!(call_id, "fn-1"); assert_eq!(output.content_items(), None); assert_eq!(output.body.to_text().as_deref(), Some("ok")); assert_eq!(output.success, Some(true)); } other => panic!("expected FunctionCallOutput, got {other:?}"), } } #[test] fn mcp_code_mode_result_serializes_full_call_tool_result() { let output = CallToolResult { content: vec![serde_json::json!({ "type": "text", "text": "ignored", })], structured_content: Some(serde_json::json!({ "threadId": "thread_123", "content": "done", })), is_error: Some(false), meta: Some(serde_json::json!({ "source": "mcp", })), }; let result = output.code_mode_result(&ToolPayload::Mcp { server: "server".to_string(), tool: "tool".to_string(), raw_arguments: "{}".to_string(), }); assert_eq!( result, serde_json::json!({ "content": [{ "type": "text", "text": "ignored", }], "structuredContent": { "threadId": "thread_123", "content": "done", }, "isError": false, "_meta": { "source": "mcp", }, }) ); } #[test] fn custom_tool_calls_can_derive_text_from_content_items() { let payload = ToolPayload::Custom { input: "patch".to_string(), }; let response = FunctionToolOutput::from_content( vec![ FunctionCallOutputContentItem::InputText { text: "line 1".to_string(), }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), detail: None, }, FunctionCallOutputContentItem::InputText { text: "line 2".to_string(), }, ], Some(true), ) .to_response_item("call-99", &payload); match response { ResponseInputItem::CustomToolCallOutput { call_id, output, .. } => { let expected = vec![ FunctionCallOutputContentItem::InputText { text: "line 1".to_string(), }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), detail: None, }, FunctionCallOutputContentItem::InputText { text: "line 2".to_string(), }, ]; assert_eq!(call_id, "call-99"); assert_eq!(output.content_items(), Some(expected.as_slice())); assert_eq!(output.body.to_text().as_deref(), Some("line 1\nline 2")); assert_eq!(output.success, Some(true)); } other => panic!("expected CustomToolCallOutput, got {other:?}"), } } #[test] fn tool_search_payloads_roundtrip_as_tool_search_outputs() { let payload = ToolPayload::ToolSearch { arguments: SearchToolCallParams { query: "calendar".to_string(), limit: None, }, }; let response = ToolSearchOutput { tools: vec![ToolSearchOutputTool::Function( crate::client_common::tools::ResponsesApiTool { name: "create_event".to_string(), description: String::new(), strict: false, defer_loading: Some(true), parameters: crate::tools::spec::JsonSchema::Object { properties: Default::default(), required: None, additional_properties: None, }, output_schema: None, }, )], } .to_response_item("search-1", &payload); match response { ResponseInputItem::ToolSearchOutput { call_id, status, execution, tools, } => { assert_eq!(call_id, "search-1"); assert_eq!(status, "completed"); assert_eq!(execution, "client"); assert_eq!( tools, vec![json!({ "type": "function", "name": "create_event", "description": "", "strict": false, "defer_loading": true, "parameters": { "type": "object", "properties": {} } })] ); } other => panic!("expected ToolSearchOutput, got {other:?}"), } } #[test] fn log_preview_uses_content_items_when_plain_text_is_missing() { let output = FunctionToolOutput::from_content( vec![FunctionCallOutputContentItem::InputText { text: "preview".to_string(), }], Some(true), ); assert_eq!(output.log_preview(), "preview"); assert_eq!( function_call_output_content_items_to_text(&output.body), Some("preview".to_string()) ); } #[test] fn telemetry_preview_returns_original_within_limits() { let content = "short output"; assert_eq!(telemetry_preview(content), content); } #[test] fn telemetry_preview_truncates_by_bytes() { let content = "x".repeat(TELEMETRY_PREVIEW_MAX_BYTES + 8); let preview = telemetry_preview(&content); assert!(preview.contains(TELEMETRY_PREVIEW_TRUNCATION_NOTICE)); assert!( preview.len() <= TELEMETRY_PREVIEW_MAX_BYTES + TELEMETRY_PREVIEW_TRUNCATION_NOTICE.len() + 1 ); } #[test] fn telemetry_preview_truncates_by_lines() { let content = (0..(TELEMETRY_PREVIEW_MAX_LINES + 5)) .map(|idx| format!("line {idx}")) .collect::>() .join("\n"); let preview = telemetry_preview(&content); let lines: Vec<&str> = preview.lines().collect(); assert!(lines.len() <= TELEMETRY_PREVIEW_MAX_LINES + 1); assert_eq!(lines.last(), Some(&TELEMETRY_PREVIEW_TRUNCATION_NOTICE)); } #[test] fn exec_command_tool_output_formats_truncated_response() { let payload = ToolPayload::Function { arguments: "{}".to_string(), }; let response = ExecCommandToolOutput { event_call_id: "call-42".to_string(), chunk_id: "abc123".to_string(), wall_time: std::time::Duration::from_millis(1250), raw_output: b"token one token two token three token four token five".to_vec(), max_output_tokens: Some(4), process_id: None, exit_code: Some(0), original_token_count: Some(10), session_command: Some(vec![ "/bin/zsh".to_string(), "-lc".to_string(), "rm -rf /tmp/example.sqlite".to_string(), ]), } .to_response_item("call-42", &payload); match response { ResponseInputItem::FunctionCallOutput { call_id, output } => { assert_eq!(call_id, "call-42"); assert_eq!(output.success, Some(true)); let text = output .body .to_text() .expect("exec output should serialize as text"); assert_regex_match( r#"(?sx) ^Command:\ /bin/zsh\ -lc\ 'rm\ -rf\ /tmp/example\.sqlite' \nChunk\ ID:\ abc123 \nWall\ time:\ \d+\.\d{4}\ seconds \nProcess\ exited\ with\ code\ 0 \nOriginal\ token\ count:\ 10 \nOutput: \n.*tokens\ truncated.* $"#, &text, ); } other => panic!("expected FunctionCallOutput, got {other:?}"), } }