sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
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());
|
||||
}
|
||||
Reference in New Issue
Block a user