459 lines
14 KiB
Rust
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());
|
|
}
|