Files
dirigent/crates/dirigent_protocol/tests/deduplication_tests.rs
T
2026-05-08 01:59:04 +02:00

459 lines
14 KiB
Rust

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());
}