use dirigent_protocol::adapters::{OpenCodeAdapter, TranslationError}; use dirigent_protocol::Event; use opencode_client::types as oc; /// Test that duplicate MessageStarted events are filtered #[test] fn test_duplicate_message_started_filtered() { let adapter = OpenCodeAdapter::new(); // First message.updated event (streaming) let oc_message = oc::Message::Assistant(oc::AssistantMessage { id: "msg_test1".to_string(), session_id: "ses_test".to_string(), time: oc::AssistantMessageTime { created: 1700000000000, completed: None, }, error: None, system: vec![], parent_id: None, model_id: Some("gpt-4".to_string()), provider_id: Some("openai".to_string()), mode: None, path: None, summary: None, cost: 0.0, tokens: Default::default(), }); let event1 = oc::Event::MessageUpdated { properties: oc::MessageEventInfo { info: oc_message.clone(), }, }; // First event should succeed let result1 = adapter.translate_event(event1); assert!(result1.is_ok()); assert!(matches!(result1.unwrap(), Event::MessageStarted { .. })); // Second identical event should be filtered as duplicate let event2 = oc::Event::MessageUpdated { properties: oc::MessageEventInfo { info: oc_message }, }; let result2 = adapter.translate_event(event2); assert!(result2.is_err()); assert!(matches!(result2.unwrap_err(), TranslationError::Duplicate)); } /// Test that duplicate MessageCompleted events are filtered #[test] fn test_duplicate_message_completed_filtered() { let adapter = OpenCodeAdapter::new(); // Completed message let oc_message = oc::Message::Assistant(oc::AssistantMessage { id: "msg_test2".to_string(), session_id: "ses_test".to_string(), time: oc::AssistantMessageTime { created: 1700000000000, completed: Some(1700000005000), }, error: None, system: vec![], parent_id: None, model_id: Some("gpt-4".to_string()), provider_id: Some("openai".to_string()), mode: None, path: None, summary: None, cost: 0.0, tokens: Default::default(), }); let event1 = oc::Event::MessageUpdated { properties: oc::MessageEventInfo { info: oc_message.clone(), }, }; // First completed event should succeed let result1 = adapter.translate_event(event1); assert!(result1.is_ok()); assert!(matches!(result1.unwrap(), Event::MessageCompleted { .. })); // Second identical completed event should be filtered let event2 = oc::Event::MessageUpdated { properties: oc::MessageEventInfo { info: oc_message }, }; let result2 = adapter.translate_event(event2); assert!(result2.is_err()); assert!(matches!(result2.unwrap_err(), TranslationError::Duplicate)); } /// Test that part updates with delta: None are filtered after first occurrence #[test] fn test_duplicate_part_completion_filtered() { let adapter = OpenCodeAdapter::new(); // First part update with delta (streaming) let part = oc::Part::Text(oc::TextPart { id: "prt_test1".to_string(), session_id: "ses_test".to_string(), message_id: "msg_test".to_string(), text: "Hello world".to_string(), synthetic: None, time: None, }); let event1 = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: part.clone(), delta: Some("Hello".to_string()), }, }; // First event with delta should succeed let result1 = adapter.translate_event(event1); assert!(result1.is_ok()); assert!(matches!(result1.unwrap(), Event::SessionUpdate { .. })); // Second event for same part without delta (completion signal) should be filtered let event2 = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part, delta: None, }, }; let result2 = adapter.translate_event(event2); assert!(result2.is_err()); assert!(matches!(result2.unwrap_err(), TranslationError::Duplicate)); } /// Test that different parts with same message are NOT filtered #[test] fn test_different_parts_not_filtered() { let adapter = OpenCodeAdapter::new(); // Reasoning part let reasoning_part = oc::Part::Reasoning(oc::ReasoningPart { id: "prt_reasoning".to_string(), session_id: "ses_test".to_string(), message_id: "msg_test".to_string(), text: "Let me think...".to_string(), time: None, }); let event1 = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: reasoning_part, delta: Some("Let me".to_string()), }, }; let result1 = adapter.translate_event(event1); assert!(result1.is_ok()); assert!(matches!(result1.unwrap(), Event::SessionUpdate { .. })); // Text part (different part, same message) let text_part = oc::Part::Text(oc::TextPart { id: "prt_text".to_string(), session_id: "ses_test".to_string(), message_id: "msg_test".to_string(), text: "Answer".to_string(), synthetic: None, time: None, }); let event2 = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: text_part, delta: Some("Answer".to_string()), }, }; // Different part should not be filtered let result2 = adapter.translate_event(event2); assert!(result2.is_ok()); assert!(matches!(result2.unwrap(), Event::SessionUpdate { .. })); } /// Test streaming part updates are not filtered (same part_id, different delta) #[test] fn test_streaming_part_updates_not_filtered() { let adapter = OpenCodeAdapter::new(); // First update let part1 = oc::Part::Text(oc::TextPart { id: "prt_streaming".to_string(), session_id: "ses_test".to_string(), message_id: "msg_test".to_string(), text: "Hello".to_string(), synthetic: None, time: None, }); let event1 = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: part1, delta: Some("Hello".to_string()), }, }; let result1 = adapter.translate_event(event1); assert!(result1.is_ok()); // Second update with more text let part2 = oc::Part::Text(oc::TextPart { id: "prt_streaming".to_string(), session_id: "ses_test".to_string(), message_id: "msg_test".to_string(), text: "Hello world".to_string(), synthetic: None, time: None, }); let event2 = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: part2, delta: Some(" world".to_string()), }, }; // Second update with delta should NOT be filtered (streaming update) let result2 = adapter.translate_event(event2); assert!(result2.is_ok()); assert!(matches!(result2.unwrap(), Event::SessionUpdate { .. })); } /// Test full tit-tat flow with proper deduplication #[test] fn test_tit_tat_flow() { let adapter = OpenCodeAdapter::new(); // 1. User message arrives (completed) let user_msg = oc::Message::User(oc::UserMessage { id: "msg_user".to_string(), session_id: "ses_test".to_string(), time: oc::MessageTime { created: 1700000000000, }, summary: None, }); let user_event = oc::Event::MessageUpdated { properties: oc::MessageEventInfo { info: user_msg }, }; let result = adapter.translate_event(user_event); assert!(result.is_ok()); assert!(matches!(result.unwrap(), Event::MessageCompleted { .. })); // 2. Assistant message starts streaming let asst_msg_streaming = oc::Message::Assistant(oc::AssistantMessage { id: "msg_asst".to_string(), session_id: "ses_test".to_string(), time: oc::AssistantMessageTime { created: 1700000001000, completed: None, }, error: None, system: vec![], parent_id: Some("msg_user".to_string()), model_id: Some("grok-code".to_string()), provider_id: Some("opencode".to_string()), mode: None, path: None, summary: None, cost: 0.0, tokens: Default::default(), }); let asst_start_event = oc::Event::MessageUpdated { properties: oc::MessageEventInfo { info: asst_msg_streaming, }, }; let result = adapter.translate_event(asst_start_event); assert!(result.is_ok()); assert!(matches!(result.unwrap(), Event::MessageStarted { .. })); // 3. Reasoning part streams let reasoning_part_1 = oc::Part::Reasoning(oc::ReasoningPart { id: "prt_reasoning".to_string(), session_id: "ses_test".to_string(), message_id: "msg_asst".to_string(), text: "First".to_string(), time: None, }); let reasoning_event_1 = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: reasoning_part_1, delta: Some("First".to_string()), }, }; let result = adapter.translate_event(reasoning_event_1); assert!(result.is_ok()); // More reasoning updates... let reasoning_part_2 = oc::Part::Reasoning(oc::ReasoningPart { id: "prt_reasoning".to_string(), session_id: "ses_test".to_string(), message_id: "msg_asst".to_string(), text: "First, the user is saying \"tit?\"".to_string(), time: None, }); let reasoning_event_2 = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: reasoning_part_2, delta: Some(", the user is saying \"tit?\"".to_string()), }, }; let result = adapter.translate_event(reasoning_event_2); assert!(result.is_ok()); // 4. Reasoning completes (delta: None) - should be filtered let reasoning_part_complete = oc::Part::Reasoning(oc::ReasoningPart { id: "prt_reasoning".to_string(), session_id: "ses_test".to_string(), message_id: "msg_asst".to_string(), text: "First, the user is saying \"tit?\" which seems like they're continuing the game.".to_string(), time: None, }); let reasoning_complete_event = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: reasoning_part_complete, delta: None, // Completion signal }, }; let result = adapter.translate_event(reasoning_complete_event); assert!(result.is_err()); // Should be filtered as duplicate assert!(matches!(result.unwrap_err(), TranslationError::Duplicate)); // 5. Text part starts streaming let text_part_1 = oc::Part::Text(oc::TextPart { id: "prt_text".to_string(), session_id: "ses_test".to_string(), message_id: "msg_asst".to_string(), text: "tat".to_string(), synthetic: None, time: None, }); let text_event_1 = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: text_part_1, delta: Some("tat".to_string()), }, }; let result = adapter.translate_event(text_event_1); assert!(result.is_ok()); // 6. Text completes (delta: None) - should be filtered let text_part_complete = oc::Part::Text(oc::TextPart { id: "prt_text".to_string(), session_id: "ses_test".to_string(), message_id: "msg_asst".to_string(), text: "tat".to_string(), synthetic: None, time: None, }); let text_complete_event = oc::Event::MessagePartUpdated { properties: oc::MessagePartEventInfo { part: text_part_complete, delta: None, // Completion signal }, }; let result = adapter.translate_event(text_complete_event); assert!(result.is_err()); // Should be filtered as duplicate assert!(matches!(result.unwrap_err(), TranslationError::Duplicate)); // 7. Message completes let asst_msg_complete = oc::Message::Assistant(oc::AssistantMessage { id: "msg_asst".to_string(), session_id: "ses_test".to_string(), time: oc::AssistantMessageTime { created: 1700000001000, completed: Some(1700000010000), }, error: None, system: vec![], parent_id: Some("msg_user".to_string()), model_id: Some("grok-code".to_string()), provider_id: Some("opencode".to_string()), mode: None, path: None, summary: None, cost: 0.0, tokens: Default::default(), }); let asst_complete_event = oc::Event::MessageUpdated { properties: oc::MessageEventInfo { info: asst_msg_complete, }, }; let result = adapter.translate_event(asst_complete_event); assert!(result.is_ok()); assert!(matches!(result.unwrap(), Event::MessageCompleted { .. })); } /// Test adapter state is independent across instances #[test] fn test_adapter_state_independence() { let adapter1 = OpenCodeAdapter::new(); let adapter2 = OpenCodeAdapter::new(); let oc_message = oc::Message::Assistant(oc::AssistantMessage { id: "msg_test".to_string(), session_id: "ses_test".to_string(), time: oc::AssistantMessageTime { created: 1700000000000, completed: None, }, error: None, system: vec![], parent_id: None, model_id: Some("gpt-4".to_string()), provider_id: Some("openai".to_string()), mode: None, path: None, summary: None, cost: 0.0, tokens: Default::default(), }); let event1 = oc::Event::MessageUpdated { properties: oc::MessageEventInfo { info: oc_message.clone(), }, }; let event2 = oc::Event::MessageUpdated { properties: oc::MessageEventInfo { info: oc_message }, }; // First adapter processes event let result1 = adapter1.translate_event(event1); assert!(result1.is_ok()); // Second adapter (with independent state) should also process successfully let result2 = adapter2.translate_event(event2); assert!(result2.is_ok()); }