/// Comprehensive integration tests for OpenCode event → SessionUpdate translation /// Tests realistic event sequences with proper ordering, metadata, and tool lifecycle tracking use dirigent_protocol::adapters::{OpenCodeAdapter, TranslationError}; use dirigent_protocol::types::{ContentBlock, SessionUpdate, ToolCallStatus}; use opencode_client::types as oc; use serde_json::json; // ===== Helper Functions ===== /// Create a text part for testing fn create_text_part( session_id: &str, message_id: &str, part_id: &str, text: &str, ) -> oc::Part { oc::Part::Text(oc::TextPart { id: part_id.to_string(), session_id: session_id.to_string(), message_id: message_id.to_string(), text: text.to_string(), synthetic: None, time: Some(oc::PartTime { start: 1000, end: None, }), }) } /// Create a reasoning part for testing fn create_reasoning_part( session_id: &str, message_id: &str, part_id: &str, text: &str, ) -> oc::Part { oc::Part::Reasoning(oc::ReasoningPart { id: part_id.to_string(), session_id: session_id.to_string(), message_id: message_id.to_string(), text: text.to_string(), time: Some(oc::PartTime { start: 1000, end: None, }), }) } /// Create a tool part with given state fn create_tool_part( session_id: &str, message_id: &str, part_id: &str, tool: &str, state: oc::ToolState, ) -> oc::Part { oc::Part::Tool(oc::ToolPart { id: part_id.to_string(), session_id: session_id.to_string(), message_id: message_id.to_string(), call_id: format!("call_{}", part_id), tool: tool.to_string(), state, metadata: None, }) } /// Create a file part for testing fn create_file_part( session_id: &str, message_id: &str, part_id: &str, filename: &str, url: &str, mime: &str, ) -> oc::Part { oc::Part::File(oc::FilePart { id: part_id.to_string(), session_id: session_id.to_string(), message_id: message_id.to_string(), mime: mime.to_string(), filename: Some(filename.to_string()), url: url.to_string(), source: None, }) } /// Create a MessagePartUpdated event fn create_part_event(part: oc::Part, delta: Option) -> oc::Event { oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part, delta }, } } // ===== Test Case 1: Text Streaming ===== /// Test text streaming produces AgentMessageChunk sequence with proper metadata #[test] fn test_text_streaming_to_agent_message_chunks() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_text"; let message_id = "msg_text"; // Simulate streaming text in multiple chunks let chunks = vec![ ("part_1", "Hello", "Hello"), ("part_2", "Hello world", " world"), ("part_3", "Hello world!", "!"), ]; let mut updates = vec![]; for (part_id, full_text, delta) in chunks { let part = create_text_part(session_id, message_id, part_id, full_text); let event = create_part_event(part, Some(delta.to_string())); let result = adapter.translate_to_session_update(event); assert!(result.is_ok(), "Translation should succeed"); let update = result.unwrap(); assert!(update.is_some(), "Should return an update"); updates.push(update.unwrap()); } // Verify all updates are AgentMessageChunk assert_eq!(updates.len(), 3); for (i, update) in updates.iter().enumerate() { match update { SessionUpdate::AgentMessageChunk { message_id: msg_id, content, _meta, } => { assert_eq!(msg_id, message_id); // Verify content is Text match content { ContentBlock::Text { text } => { // Each chunk should contain the full text up to that point assert!(!text.is_empty()); } _ => panic!("Expected Text content"), } // Verify metadata is present assert!(_meta.is_some(), "Metadata should be present on chunk {}", i); let meta = _meta.as_ref().unwrap(); assert!(meta.provider.is_some(), "Provider metadata should be present"); let provider = meta.provider.as_ref().unwrap(); assert_eq!(provider.name, "opencode"); assert!(provider.original_ids.is_some()); let ids = provider.original_ids.as_ref().unwrap(); assert_eq!(ids.get("session_id").unwrap(), session_id); assert_eq!(ids.get("message_id").unwrap(), message_id); assert!(ids.contains_key("part_id")); } _ => panic!("Expected AgentMessageChunk at index {}", i), } } } /// Test that chunks are created in order #[test] fn test_text_chunks_preserve_order() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_order"; let message_id = "msg_order"; let expected_order = vec!["First", "Second", "Third", "Fourth"]; for (i, text) in expected_order.iter().enumerate() { let part_id = format!("part_{}", i); let part = create_text_part(session_id, message_id, &part_id, text); let event = create_part_event(part, Some(text.to_string())); let result = adapter.translate_to_session_update(event); assert!(result.is_ok()); let update = result.unwrap().unwrap(); match update { SessionUpdate::AgentMessageChunk { content, .. } => { if let ContentBlock::Text { text: chunk_text } = content { assert_eq!(chunk_text, *text); } else { panic!("Expected Text content"); } } _ => panic!("Expected AgentMessageChunk"), } } } // ===== Test Case 2: Reasoning Streaming ===== /// Test reasoning streaming produces AgentThoughtChunk sequence with metadata #[test] fn test_reasoning_streaming_to_agent_thought_chunks() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_reason"; let message_id = "msg_reason"; // Simulate streaming reasoning in chunks // Each chunk: (part_id, full_text, delta, expected_delta) let chunks = vec![ ("reason_1", "I need to", "I need to"), ("reason_2", "I need to analyze", " analyze"), ("reason_3", "I need to analyze the problem", " the problem"), ]; let mut updates = vec![]; for (part_id, full_text, delta) in chunks.iter() { let part = create_reasoning_part(session_id, message_id, part_id, full_text); let event = create_part_event(part, Some(delta.to_string())); let result = adapter.translate_to_session_update(event); assert!(result.is_ok()); let update = result.unwrap(); assert!(update.is_some()); updates.push((update.unwrap(), delta)); } // Verify all are AgentThoughtChunk with metadata assert_eq!(updates.len(), 3); for (i, (update, expected_delta)) in updates.iter().enumerate() { match update { SessionUpdate::AgentThoughtChunk { message_id: msg_id, content, _meta, } => { assert_eq!(msg_id, message_id); // Verify content contains the delta, not the full accumulated text match content { ContentBlock::Text { text } => { assert_eq!(text, *expected_delta, "Chunk {} should contain delta", i); } _ => panic!("Expected Text content"), } // Verify metadata assert!(_meta.is_some(), "Metadata should be present on chunk {}", i); let meta = _meta.as_ref().unwrap(); assert!(meta.provider.is_some()); let provider = meta.provider.as_ref().unwrap(); assert_eq!(provider.name, "opencode"); assert!(provider.original_ids.is_some()); let ids = provider.original_ids.as_ref().unwrap(); assert_eq!(ids.get("session_id").unwrap(), session_id); assert_eq!(ids.get("message_id").unwrap(), message_id); } _ => panic!("Expected AgentThoughtChunk at index {}", i), } } } // ===== Test Case 3: Tool Lifecycle (Pending → Running → Completed) ===== /// Test complete tool lifecycle produces ToolCall followed by ToolCallUpdate events #[test] fn test_tool_lifecycle_pending_to_completed() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_tool"; let message_id = "msg_tool"; let part_id = "tool_1"; let tool_name = "bash"; let input = json!({"command": "ls -la"}); let output = "file1.txt\nfile2.txt"; // Step 1: Pending state → ToolCall let pending_part = create_tool_part( session_id, message_id, part_id, tool_name, oc::ToolState::Pending, ); let pending_event = create_part_event(pending_part, None); let result = adapter.translate_to_session_update(pending_event); assert!(result.is_ok()); let update = result.unwrap().unwrap(); match &update { SessionUpdate::ToolCall { message_id: msg_id, tool_call, _meta, } => { assert_eq!(msg_id, message_id); assert_eq!(tool_call.id, part_id); assert_eq!(tool_call.tool_name, tool_name); assert_eq!(tool_call.status, ToolCallStatus::Pending); assert!(tool_call.raw_input.is_none()); assert!(tool_call.raw_output.is_none()); assert!(tool_call.error.is_none()); // Verify metadata assert!(_meta.is_some()); let meta = _meta.as_ref().unwrap(); assert!(meta.provider.is_some()); let provider = meta.provider.as_ref().unwrap(); assert_eq!(provider.name, "opencode"); } _ => panic!("Expected ToolCall for pending state"), } // Step 2: Running state → ToolCallUpdate let running_part = create_tool_part( session_id, message_id, part_id, tool_name, oc::ToolState::Running { input: input.clone(), title: None, metadata: None, time: oc::PartTime { start: 1000, end: None, }, }, ); let running_event = create_part_event(running_part, Some("running".to_string())); let result = adapter.translate_to_session_update(running_event); assert!(result.is_ok()); let update = result.unwrap().unwrap(); match &update { SessionUpdate::ToolCallUpdate { message_id: msg_id, tool_call_id, tool_call, _meta, } => { assert_eq!(msg_id, message_id); assert_eq!(tool_call_id, part_id); assert_eq!(tool_call.id, part_id); assert_eq!(tool_call.tool_name, tool_name); assert_eq!(tool_call.status, ToolCallStatus::Running); assert_eq!(tool_call.raw_input, Some(input.clone())); assert!(tool_call.raw_output.is_none()); assert!(tool_call.error.is_none()); // Verify metadata throughout lifecycle assert!(_meta.is_some()); let meta = _meta.as_ref().unwrap(); assert!(meta.provider.is_some()); } _ => panic!("Expected ToolCallUpdate for running state"), } // Step 3: Completed state → ToolCallUpdate let completed_part = create_tool_part( session_id, message_id, part_id, tool_name, oc::ToolState::Completed { input: input.clone(), output: output.to_string(), title: "bash command".to_string(), metadata: serde_json::Value::Null, time: oc::PartTime { start: 1000, end: Some(2000), }, attachments: None, }, ); let completed_event = create_part_event(completed_part, Some(output.to_string())); let result = adapter.translate_to_session_update(completed_event); assert!(result.is_ok()); let update = result.unwrap().unwrap(); match &update { SessionUpdate::ToolCallUpdate { message_id: msg_id, tool_call_id, tool_call, _meta, } => { assert_eq!(msg_id, message_id); assert_eq!(tool_call_id, part_id); assert_eq!(tool_call.id, part_id); assert_eq!(tool_call.tool_name, tool_name); assert_eq!(tool_call.status, ToolCallStatus::Completed); assert_eq!(tool_call.raw_input, Some(input)); assert_eq!( tool_call.raw_output, Some(serde_json::Value::String(output.to_string())) ); assert!(tool_call.error.is_none()); // Verify metadata in final state assert!(_meta.is_some()); } _ => panic!("Expected ToolCallUpdate for completed state"), } } /// Test status transitions are properly tracked #[test] fn test_tool_status_transitions() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_status"; let message_id = "msg_status"; let part_id = "tool_status"; let expected_statuses = vec![ (oc::ToolState::Pending, ToolCallStatus::Pending), ( oc::ToolState::Running { input: json!({}), title: None, metadata: None, time: oc::PartTime { start: 1000, end: None, }, }, ToolCallStatus::Running, ), ( oc::ToolState::Completed { input: json!({}), output: "done".to_string(), title: "".to_string(), metadata: serde_json::Value::Null, time: oc::PartTime { start: 1000, end: Some(2000), }, attachments: None, }, ToolCallStatus::Completed, ), ]; for (i, (oc_state, expected_status)) in expected_statuses.into_iter().enumerate() { let part = create_tool_part(session_id, message_id, part_id, "test", oc_state); let event = create_part_event(part, Some("delta".to_string())); let result = adapter.translate_to_session_update(event); assert!(result.is_ok()); let update = result.unwrap().unwrap(); let actual_status = match &update { SessionUpdate::ToolCall { tool_call, .. } => &tool_call.status, SessionUpdate::ToolCallUpdate { tool_call, .. } => &tool_call.status, _ => panic!("Expected tool update at step {}", i), }; assert_eq!(actual_status, &expected_status, "Status mismatch at step {}", i); } } // ===== Test Case 4: Tool Error ===== /// Test tool error produces ToolCallUpdate with Error status and error field #[test] fn test_tool_error_to_tool_call_update() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_error"; let message_id = "msg_error"; let part_id = "tool_error"; let error_msg = "Command not found: invalid_cmd"; let input = json!({"command": "invalid_cmd"}); // First, create the tool with pending state let pending_part = create_tool_part( session_id, message_id, part_id, "bash", oc::ToolState::Pending, ); let pending_event = create_part_event(pending_part, None); let _ = adapter.translate_to_session_update(pending_event); // Now send error state let error_part = create_tool_part( session_id, message_id, part_id, "bash", oc::ToolState::Error { input: input.clone(), error: error_msg.to_string(), metadata: None, time: oc::PartTime { start: 1000, end: Some(2000), }, }, ); let error_event = create_part_event(error_part, Some(error_msg.to_string())); let result = adapter.translate_to_session_update(error_event); assert!(result.is_ok()); let update = result.unwrap().unwrap(); match &update { SessionUpdate::ToolCallUpdate { message_id: msg_id, tool_call_id, tool_call, _meta, } => { assert_eq!(msg_id, message_id); assert_eq!(tool_call_id, part_id); assert_eq!(tool_call.status, ToolCallStatus::Error); assert_eq!(tool_call.raw_input, Some(input)); assert!(tool_call.raw_output.is_none()); assert_eq!(tool_call.error, Some(error_msg.to_string())); // Verify error message is captured let err = tool_call.error.as_ref().unwrap(); assert!(err.contains("Command not found")); // Verify metadata assert!(_meta.is_some()); } _ => panic!("Expected ToolCallUpdate for error state"), } } // ===== Test Case 5: File Reference ===== /// Test file part produces ResourceLink in AgentMessageChunk #[test] fn test_file_reference_to_resource_link() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_file"; let message_id = "msg_file"; let part_id = "file_1"; let filename = "test_document.pdf"; let uri = "file:///home/user/documents/test_document.pdf"; let mime_type = "application/pdf"; let part = create_file_part(session_id, message_id, part_id, filename, uri, mime_type); let event = create_part_event(part, None); let result = adapter.translate_to_session_update(event); assert!(result.is_ok()); let update = result.unwrap().unwrap(); match &update { SessionUpdate::AgentMessageChunk { message_id: msg_id, content, _meta, } => { assert_eq!(msg_id, message_id); // Verify content is ResourceLink match content { ContentBlock::ResourceLink { uri: content_uri, name, mime_type: content_mime, } => { assert_eq!(content_uri, uri); assert_eq!(name.as_ref().unwrap(), filename); assert_eq!(content_mime.as_ref().unwrap(), mime_type); } _ => panic!("Expected ResourceLink content"), } // Verify metadata assert!(_meta.is_some()); let meta = _meta.as_ref().unwrap(); assert!(meta.provider.is_some()); let provider = meta.provider.as_ref().unwrap(); assert_eq!(provider.name, "opencode"); assert!(provider.original_ids.is_some()); let ids = provider.original_ids.as_ref().unwrap(); assert_eq!(ids.get("part_id").unwrap(), part_id); } _ => panic!("Expected AgentMessageChunk with ResourceLink"), } } /// Test file with different mime types #[test] fn test_file_references_various_mime_types() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_files"; let message_id = "msg_files"; let test_cases = vec![ ("file_txt", "data.txt", "file:///data.txt", "text/plain"), ("file_json", "config.json", "file:///config.json", "application/json"), ("file_img", "screenshot.png", "file:///screenshot.png", "image/png"), ]; for (part_id, filename, uri, mime_type) in test_cases { let part = create_file_part(session_id, message_id, part_id, filename, uri, mime_type); let event = create_part_event(part, None); let result = adapter.translate_to_session_update(event); assert!(result.is_ok()); let update = result.unwrap().unwrap(); match update { SessionUpdate::AgentMessageChunk { content, .. } => { if let ContentBlock::ResourceLink { uri: content_uri, name, mime_type: content_mime, } = content { assert_eq!(content_uri, uri); assert_eq!(name.as_ref().unwrap(), filename); assert_eq!(content_mime.as_ref().unwrap(), mime_type); } else { panic!("Expected ResourceLink for {}", filename); } } _ => panic!("Expected AgentMessageChunk for {}", filename), } } } // ===== Test Case 6: Interleaved Updates Preserve Order ===== /// Test mix of text, reasoning, and tool updates maintain correct order #[test] fn test_interleaved_updates_preserve_order() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_interleaved"; let message_id = "msg_interleaved"; // Create a realistic sequence of interleaved events let events = vec![ // Text chunk 1 ( "text", create_part_event( create_text_part(session_id, message_id, "part_1", "Let me think"), Some("Let me think".to_string()), ), ), // Reasoning chunk 1 ( "thought", create_part_event( create_reasoning_part(session_id, message_id, "reason_1", "Analyzing the task"), Some("Analyzing the task".to_string()), ), ), // Tool pending ( "tool_pending", create_part_event( create_tool_part(session_id, message_id, "tool_1", "bash", oc::ToolState::Pending), None, ), ), // Text chunk 2 ( "text", create_part_event( create_text_part(session_id, message_id, "part_2", "Running command"), Some("Running command".to_string()), ), ), // Tool running ( "tool_running", create_part_event( create_tool_part( session_id, message_id, "tool_1", "bash", oc::ToolState::Running { input: json!({"cmd": "ls"}), title: None, metadata: None, time: oc::PartTime { start: 1000, end: None, }, }, ), Some("running".to_string()), ), ), // Reasoning chunk 2 ( "thought", create_part_event( create_reasoning_part(session_id, message_id, "reason_2", "Command is executing"), Some("Command is executing".to_string()), ), ), // Tool completed ( "tool_completed", create_part_event( create_tool_part( session_id, message_id, "tool_1", "bash", oc::ToolState::Completed { input: json!({"cmd": "ls"}), output: "file1.txt".to_string(), title: "".to_string(), metadata: serde_json::Value::Null, time: oc::PartTime { start: 1000, end: Some(2000), }, attachments: None, }, ), Some("file1.txt".to_string()), ), ), // Text chunk 3 ( "text", create_part_event( create_text_part(session_id, message_id, "part_3", "Done!"), Some("Done!".to_string()), ), ), ]; let expected_types = vec![ "text", "thought", "tool_pending", "text", "tool_running", "thought", "tool_completed", "text", ]; let mut updates = vec![]; for (i, (_, event)) in events.into_iter().enumerate() { let result = adapter.translate_to_session_update(event); assert!(result.is_ok(), "Event {} should translate successfully", i); let update = result.unwrap(); assert!(update.is_some(), "Event {} should produce an update", i); updates.push(update.unwrap()); } // Verify order is preserved assert_eq!(updates.len(), expected_types.len()); for (i, (update, expected_type)) in updates.iter().zip(expected_types.iter()).enumerate() { match (update, *expected_type) { (SessionUpdate::AgentMessageChunk { .. }, "text") => {} (SessionUpdate::AgentThoughtChunk { .. }, "thought") => {} (SessionUpdate::ToolCall { .. }, "tool_pending") => {} (SessionUpdate::ToolCallUpdate { tool_call, .. }, "tool_running") => { assert_eq!(tool_call.status, ToolCallStatus::Running); } (SessionUpdate::ToolCallUpdate { tool_call, .. }, "tool_completed") => { assert_eq!(tool_call.status, ToolCallStatus::Completed); } _ => panic!( "Type mismatch at position {}: expected {}, got {:?}", i, expected_type, update ), } } } /// Test correct variant for each update type in sequence #[test] fn test_correct_variant_for_each_type() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_variants"; let message_id = "msg_variants"; // Test text → AgentMessageChunk let text_event = create_part_event( create_text_part(session_id, message_id, "p1", "text"), Some("text".to_string()), ); let result = adapter.translate_to_session_update(text_event).unwrap().unwrap(); assert!(matches!(result, SessionUpdate::AgentMessageChunk { .. })); // Test reasoning → AgentThoughtChunk let reason_event = create_part_event( create_reasoning_part(session_id, message_id, "p2", "thinking"), Some("thinking".to_string()), ); let result = adapter.translate_to_session_update(reason_event).unwrap().unwrap(); assert!(matches!(result, SessionUpdate::AgentThoughtChunk { .. })); // Test tool pending → ToolCall let tool_pending_event = create_part_event( create_tool_part(session_id, message_id, "p3", "test", oc::ToolState::Pending), None, ); let result = adapter.translate_to_session_update(tool_pending_event).unwrap().unwrap(); assert!(matches!(result, SessionUpdate::ToolCall { .. })); // Test tool running → ToolCallUpdate let tool_running_event = create_part_event( create_tool_part( session_id, message_id, "p3", "test", oc::ToolState::Running { input: json!({}), title: None, metadata: None, time: oc::PartTime { start: 1000, end: None, }, }, ), Some("running".to_string()), ); let result = adapter.translate_to_session_update(tool_running_event).unwrap().unwrap(); assert!(matches!(result, SessionUpdate::ToolCallUpdate { .. })); // Test file → AgentMessageChunk with ResourceLink let file_event = create_part_event( create_file_part(session_id, message_id, "p4", "f.txt", "file:///f.txt", "text/plain"), None, ); let result = adapter.translate_to_session_update(file_event).unwrap().unwrap(); match result { SessionUpdate::AgentMessageChunk { content, .. } => { assert!(matches!(content, ContentBlock::ResourceLink { .. })); } _ => panic!("Expected AgentMessageChunk for file"), } } // ===== Additional Edge Cases ===== /// Test duplicate detection for SessionUpdate translation #[test] fn test_duplicate_part_skipped_in_session_update() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_dup"; let message_id = "msg_dup"; let part_id = "part_dup"; // First event with delta let part1 = create_text_part(session_id, message_id, part_id, "Hello"); let event1 = create_part_event(part1, Some("Hello".to_string())); let result1 = adapter.translate_to_session_update(event1); assert!(result1.is_ok()); assert!(result1.unwrap().is_some()); // Second event without delta (completion marker) - should be skipped let part2 = create_text_part(session_id, message_id, part_id, "Hello"); let event2 = create_part_event(part2, None); let result2 = adapter.translate_to_session_update(event2); assert!(result2.is_ok()); assert!(result2.unwrap().is_none(), "Duplicate without delta should be skipped"); } /// Test unsupported event types return error #[test] fn test_unsupported_events_return_error() { let adapter = OpenCodeAdapter::new(); // SessionCreated is not supported in translate_to_session_update let session_event = oc::Event::SessionCreated { properties: oc::SessionEventInfo { info: oc::Session { id: "sess1".to_string(), project_id: "proj1".to_string(), directory: "/test".to_string(), parent_id: None, summary: None, share: None, title: "Test".to_string(), version: "1.0".to_string(), time: oc::SessionTime { created: 1000, updated: 1000, compacting: None, }, revert: None, }, }, }; let result = adapter.translate_to_session_update(session_event); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), TranslationError::UnsupportedEvent)); } /// Test unsupported part types return error #[test] fn test_unsupported_part_types_return_error() { let adapter = OpenCodeAdapter::new(); // Snapshot part is not supported let snapshot_part = oc::Part::Snapshot(oc::SnapshotPart { id: "snap1".to_string(), session_id: "sess1".to_string(), message_id: "msg1".to_string(), snapshot: "snapshot_data".to_string(), }); let event = create_part_event(snapshot_part, None); let result = adapter.translate_to_session_update(event); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), TranslationError::UnsupportedPartType)); } /// Test multiple concurrent tool calls tracked independently #[test] fn test_multiple_concurrent_tools_independent_tracking() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_multi"; let message_id = "msg_multi"; // Tool 1: Pending let tool1_pending = create_part_event( create_tool_part(session_id, message_id, "tool_1", "bash", oc::ToolState::Pending), None, ); let result1 = adapter.translate_to_session_update(tool1_pending).unwrap().unwrap(); assert!(matches!(result1, SessionUpdate::ToolCall { .. })); // Tool 2: Pending (different tool) let tool2_pending = create_part_event( create_tool_part(session_id, message_id, "tool_2", "read", oc::ToolState::Pending), None, ); let result2 = adapter.translate_to_session_update(tool2_pending).unwrap().unwrap(); assert!(matches!(result2, SessionUpdate::ToolCall { .. })); // Tool 1: Running → should be ToolCallUpdate let tool1_running = create_part_event( create_tool_part( session_id, message_id, "tool_1", "bash", oc::ToolState::Running { input: json!({}), title: None, metadata: None, time: oc::PartTime { start: 1000, end: None, }, }, ), Some("r".to_string()), ); let result3 = adapter.translate_to_session_update(tool1_running).unwrap().unwrap(); match result3 { SessionUpdate::ToolCallUpdate { tool_call_id, .. } => { assert_eq!(tool_call_id, "tool_1"); } _ => panic!("Expected ToolCallUpdate for tool_1"), } // Tool 2: Running → should also be ToolCallUpdate let tool2_running = create_part_event( create_tool_part( session_id, message_id, "tool_2", "read", oc::ToolState::Running { input: json!({}), title: None, metadata: None, time: oc::PartTime { start: 1000, end: None, }, }, ), Some("r".to_string()), ); let result4 = adapter.translate_to_session_update(tool2_running).unwrap().unwrap(); match result4 { SessionUpdate::ToolCallUpdate { tool_call_id, .. } => { assert_eq!(tool_call_id, "tool_2"); } _ => panic!("Expected ToolCallUpdate for tool_2"), } } /// Test metadata present in all update types #[test] fn test_metadata_present_in_all_updates() { let adapter = OpenCodeAdapter::new(); let session_id = "sess_meta"; let message_id = "msg_meta"; // Text let text_event = create_part_event( create_text_part(session_id, message_id, "p1", "text"), Some("text".to_string()), ); let text_update = adapter.translate_to_session_update(text_event).unwrap().unwrap(); match text_update { SessionUpdate::AgentMessageChunk { _meta, .. } => { assert!(_meta.is_some(), "Text chunk should have metadata"); } _ => panic!("Expected AgentMessageChunk"), } // Reasoning let reason_event = create_part_event( create_reasoning_part(session_id, message_id, "p2", "reason"), Some("reason".to_string()), ); let reason_update = adapter.translate_to_session_update(reason_event).unwrap().unwrap(); match reason_update { SessionUpdate::AgentThoughtChunk { _meta, .. } => { assert!(_meta.is_some(), "Thought chunk should have metadata"); } _ => panic!("Expected AgentThoughtChunk"), } // Tool let tool_event = create_part_event( create_tool_part(session_id, message_id, "p3", "bash", oc::ToolState::Pending), None, ); let tool_update = adapter.translate_to_session_update(tool_event).unwrap().unwrap(); match tool_update { SessionUpdate::ToolCall { _meta, .. } => { assert!(_meta.is_some(), "ToolCall should have metadata"); } _ => panic!("Expected ToolCall"), } // File let file_event = create_part_event( create_file_part(session_id, message_id, "p4", "f.txt", "file:///f.txt", "text/plain"), None, ); let file_update = adapter.translate_to_session_update(file_event).unwrap().unwrap(); match file_update { SessionUpdate::AgentMessageChunk { _meta, .. } => { assert!(_meta.is_some(), "File chunk should have metadata"); } _ => panic!("Expected AgentMessageChunk"), } }