sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
use dirigent_protocol::adapters::OpenCodeAdapter;
|
||||
use dirigent_protocol::{Event, Message, MessagePart, MessageRole, MessageStatus, Session};
|
||||
use opencode_client::types as oc;
|
||||
|
||||
#[test]
|
||||
fn test_parse_opencode_events() {
|
||||
// Load sample events from fixture
|
||||
let fixture = include_str!("fixtures/sample_events.jsonl");
|
||||
|
||||
for (idx, line) in fixture.lines().enumerate() {
|
||||
let result = serde_json::from_str::<oc::Event>(line);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse OpenCode event at line {}: {:?}",
|
||||
idx + 1,
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translate_server_connected() {
|
||||
let adapter = OpenCodeAdapter::new();
|
||||
let oc_event = oc::Event::ServerConnected {
|
||||
properties: serde_json::json!({}),
|
||||
};
|
||||
|
||||
let result = adapter.translate_event(oc_event);
|
||||
assert!(result.is_ok());
|
||||
assert!(matches!(result.unwrap(), Event::Connected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translate_session_created() {
|
||||
let adapter = OpenCodeAdapter::new();
|
||||
let oc_session = oc::Session {
|
||||
id: "ses_test123".to_string(),
|
||||
project_id: "test_project".to_string(),
|
||||
directory: "/test/path".to_string(),
|
||||
parent_id: None,
|
||||
summary: None,
|
||||
share: None,
|
||||
title: "Test Session".to_string(),
|
||||
version: "0.15.31".to_string(),
|
||||
time: oc::SessionTime {
|
||||
created: 1700000000000,
|
||||
updated: 1700000000000,
|
||||
compacting: None,
|
||||
},
|
||||
revert: None,
|
||||
};
|
||||
|
||||
let oc_event = oc::Event::SessionCreated {
|
||||
properties: oc::SessionEventInfo {
|
||||
info: oc_session.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
let result = adapter.translate_event(oc_event);
|
||||
assert!(result.is_ok());
|
||||
|
||||
match result.unwrap() {
|
||||
Event::SessionCreated {
|
||||
connector_id: _,
|
||||
session,
|
||||
} => {
|
||||
assert_eq!(session.id, "ses_test123");
|
||||
assert_eq!(session.title, "Test Session");
|
||||
assert_eq!(session.metadata.project_path, "/test/path");
|
||||
}
|
||||
_ => panic!("Expected SessionCreated event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translate_user_message() {
|
||||
let adapter = OpenCodeAdapter::new();
|
||||
let oc_message = oc::Message::User(oc::UserMessage {
|
||||
id: "msg_user1".to_string(),
|
||||
session_id: "ses_test123".to_string(),
|
||||
time: oc::MessageTime {
|
||||
created: 1700000001000,
|
||||
},
|
||||
summary: None,
|
||||
});
|
||||
|
||||
let oc_event = oc::Event::MessageUpdated {
|
||||
properties: oc::MessageEventInfo { info: oc_message },
|
||||
};
|
||||
|
||||
let result = adapter.translate_event(oc_event);
|
||||
assert!(result.is_ok());
|
||||
|
||||
match result.unwrap() {
|
||||
Event::MessageCompleted {
|
||||
connector_id: _,
|
||||
message,
|
||||
} => {
|
||||
assert_eq!(message.id, "msg_user1");
|
||||
assert_eq!(message.session_id, "ses_test123");
|
||||
assert!(matches!(message.role, MessageRole::User));
|
||||
assert!(matches!(message.status, MessageStatus::Completed));
|
||||
}
|
||||
_ => panic!("Expected MessageCompleted event for user message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translate_assistant_message_streaming() {
|
||||
let oc_message = oc::Message::Assistant(oc::AssistantMessage {
|
||||
id: "msg_asst1".to_string(),
|
||||
session_id: "ses_test123".to_string(),
|
||||
time: oc::AssistantMessageTime {
|
||||
created: 1700000002000,
|
||||
completed: None, // Still streaming
|
||||
},
|
||||
error: None,
|
||||
system: vec![],
|
||||
parent_id: Some("msg_user1".to_string()),
|
||||
model_id: Some("gpt-4".to_string()),
|
||||
provider_id: Some("openai".to_string()),
|
||||
mode: None,
|
||||
path: None,
|
||||
summary: None,
|
||||
cost: 0.05,
|
||||
tokens: Default::default(),
|
||||
});
|
||||
|
||||
let oc_event = oc::Event::MessageUpdated {
|
||||
properties: oc::MessageEventInfo { info: oc_message },
|
||||
};
|
||||
|
||||
let adapter = OpenCodeAdapter::new();
|
||||
let result = adapter.translate_event(oc_event);
|
||||
assert!(result.is_ok());
|
||||
|
||||
match result.unwrap() {
|
||||
Event::MessageStarted {
|
||||
connector_id: _,
|
||||
message,
|
||||
} => {
|
||||
assert_eq!(message.id, "msg_asst1");
|
||||
assert!(matches!(message.role, MessageRole::Assistant));
|
||||
assert!(matches!(message.status, MessageStatus::Streaming));
|
||||
}
|
||||
_ => panic!("Expected MessageStarted event for streaming message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translate_assistant_message_completed() {
|
||||
let oc_message = oc::Message::Assistant(oc::AssistantMessage {
|
||||
id: "msg_asst1".to_string(),
|
||||
session_id: "ses_test123".to_string(),
|
||||
time: oc::AssistantMessageTime {
|
||||
created: 1700000002000,
|
||||
completed: Some(1700000005000), // Completed
|
||||
},
|
||||
error: None,
|
||||
system: vec![],
|
||||
parent_id: Some("msg_user1".to_string()),
|
||||
model_id: Some("gpt-4".to_string()),
|
||||
provider_id: Some("openai".to_string()),
|
||||
mode: None,
|
||||
path: None,
|
||||
summary: None,
|
||||
cost: 0.05,
|
||||
tokens: Default::default(),
|
||||
});
|
||||
|
||||
let oc_event = oc::Event::MessageUpdated {
|
||||
properties: oc::MessageEventInfo { info: oc_message },
|
||||
};
|
||||
|
||||
let adapter = OpenCodeAdapter::new();
|
||||
let result = adapter.translate_event(oc_event);
|
||||
assert!(result.is_ok());
|
||||
|
||||
match result.unwrap() {
|
||||
Event::MessageCompleted {
|
||||
connector_id: _,
|
||||
message,
|
||||
} => {
|
||||
assert_eq!(message.id, "msg_asst1");
|
||||
assert!(matches!(message.role, MessageRole::Assistant));
|
||||
assert!(matches!(message.status, MessageStatus::Completed));
|
||||
}
|
||||
_ => panic!("Expected MessageCompleted event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translate_text_part() {
|
||||
let oc_part = oc::Part::Text(oc::TextPart {
|
||||
id: "prt_text1".to_string(),
|
||||
session_id: "ses_test123".to_string(),
|
||||
message_id: "msg_user1".to_string(),
|
||||
text: "Hello, can you help me?".to_string(),
|
||||
synthetic: Some(false),
|
||||
time: None,
|
||||
});
|
||||
|
||||
let oc_event = oc::Event::MessagePartUpdated {
|
||||
properties: oc::MessagePartEventInfo {
|
||||
part: oc_part,
|
||||
delta: None,
|
||||
},
|
||||
};
|
||||
|
||||
let adapter = OpenCodeAdapter::new();
|
||||
let result = adapter.translate_event(oc_event);
|
||||
assert!(result.is_ok());
|
||||
|
||||
match result.unwrap() {
|
||||
Event::SessionUpdate {
|
||||
connector_id: _,
|
||||
session_id,
|
||||
update,
|
||||
} => {
|
||||
assert_eq!(session_id, "ses_test123");
|
||||
match update {
|
||||
dirigent_protocol::SessionUpdate::AgentMessageChunk {
|
||||
message_id,
|
||||
content,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(message_id, "msg_user1");
|
||||
match content {
|
||||
dirigent_protocol::ContentBlock::Text { text } => {
|
||||
assert_eq!(text, "Hello, can you help me?");
|
||||
}
|
||||
_ => panic!("Expected Text content block"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected AgentMessageChunk"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected SessionUpdate event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translate_reasoning_part() {
|
||||
let oc_part = oc::Part::Reasoning(oc::ReasoningPart {
|
||||
id: "prt_reasoning1".to_string(),
|
||||
session_id: "ses_test123".to_string(),
|
||||
message_id: "msg_asst1".to_string(),
|
||||
text: "Let me think about this...".to_string(),
|
||||
time: None,
|
||||
});
|
||||
|
||||
let oc_event = oc::Event::MessagePartUpdated {
|
||||
properties: oc::MessagePartEventInfo {
|
||||
part: oc_part,
|
||||
delta: Some(" more".to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
let adapter = OpenCodeAdapter::new();
|
||||
let result = adapter.translate_event(oc_event);
|
||||
assert!(result.is_ok());
|
||||
|
||||
match result.unwrap() {
|
||||
Event::SessionUpdate {
|
||||
connector_id: _,
|
||||
session_id,
|
||||
update,
|
||||
} => {
|
||||
assert_eq!(session_id, "ses_test123");
|
||||
match update {
|
||||
dirigent_protocol::SessionUpdate::AgentThoughtChunk {
|
||||
message_id,
|
||||
content,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(message_id, "msg_asst1");
|
||||
match content {
|
||||
dirigent_protocol::ContentBlock::Text { text } => {
|
||||
// Should use delta, not full text
|
||||
assert_eq!(text, " more");
|
||||
}
|
||||
_ => panic!("Expected Text content block"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected AgentThoughtChunk"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected SessionUpdate event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translate_tool_part_completed() {
|
||||
let oc_part = oc::Part::Tool(oc::ToolPart {
|
||||
id: "prt_tool1".to_string(),
|
||||
session_id: "ses_test123".to_string(),
|
||||
message_id: "msg_asst1".to_string(),
|
||||
call_id: "call_123".to_string(),
|
||||
tool: "Read".to_string(),
|
||||
state: oc::ToolState::Completed {
|
||||
input: serde_json::json!({"file_path": "/test/file.txt"}),
|
||||
output: "File contents here".to_string(),
|
||||
title: "Reading file".to_string(),
|
||||
metadata: serde_json::json!({}),
|
||||
time: oc::PartTime {
|
||||
start: 1700000003000,
|
||||
end: Some(1700000004000),
|
||||
},
|
||||
attachments: None,
|
||||
},
|
||||
metadata: None,
|
||||
});
|
||||
|
||||
let oc_event = oc::Event::MessagePartUpdated {
|
||||
properties: oc::MessagePartEventInfo {
|
||||
part: oc_part,
|
||||
delta: None,
|
||||
},
|
||||
};
|
||||
|
||||
let adapter = OpenCodeAdapter::new();
|
||||
let result = adapter.translate_event(oc_event);
|
||||
assert!(result.is_ok());
|
||||
|
||||
match result.unwrap() {
|
||||
Event::SessionUpdate {
|
||||
connector_id: _,
|
||||
session_id,
|
||||
update,
|
||||
} => {
|
||||
assert_eq!(session_id, "ses_test123");
|
||||
match update {
|
||||
dirigent_protocol::SessionUpdate::ToolCall {
|
||||
message_id,
|
||||
tool_call,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(message_id, "msg_asst1");
|
||||
assert_eq!(tool_call.tool_name, "Read");
|
||||
assert_eq!(
|
||||
tool_call.status,
|
||||
dirigent_protocol::ToolCallStatus::Completed
|
||||
);
|
||||
assert!(tool_call.raw_input.is_some());
|
||||
assert_eq!(
|
||||
tool_call.raw_input.unwrap().get("file_path").unwrap(),
|
||||
"/test/file.txt"
|
||||
);
|
||||
assert!(tool_call.raw_output.is_some());
|
||||
assert_eq!(
|
||||
tool_call.raw_output.unwrap().as_str().unwrap(),
|
||||
"File contents here"
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected ToolCall"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected SessionUpdate event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_event_stream() {
|
||||
let fixture = include_str!("fixtures/sample_events.jsonl");
|
||||
let adapter = OpenCodeAdapter::new(); // One adapter for entire stream
|
||||
let mut session_created = false;
|
||||
let mut message_count = 0;
|
||||
let mut part_count = 0;
|
||||
|
||||
for line in fixture.lines() {
|
||||
let oc_event: oc::Event =
|
||||
serde_json::from_str(line).expect("Failed to parse OpenCode event");
|
||||
let result = adapter.translate_event(oc_event);
|
||||
|
||||
match result {
|
||||
Ok(Event::SessionCreated { .. }) => session_created = true,
|
||||
Ok(Event::MessageStarted { .. }) | Ok(Event::MessageCompleted { .. }) => {
|
||||
message_count += 1
|
||||
}
|
||||
Ok(Event::SessionUpdate { .. }) => part_count += 1,
|
||||
Err(_) => {} // Some events might not translate (e.g., unknown types) or duplicates
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(session_created, "Session should be created");
|
||||
assert!(message_count > 0, "Should have messages");
|
||||
assert!(part_count > 0, "Should have message parts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dirigent_protocol_serialization() {
|
||||
// Test that Dirigent protocol types can be serialized and deserialized
|
||||
let session = Session {
|
||||
id: "ses_test".to_string(),
|
||||
title: "Test".to_string(),
|
||||
created_at: chrono::Utc::now(),
|
||||
|
||||
updated_at: chrono::Utc::now(),
|
||||
|
||||
metadata: dirigent_protocol::SessionMetadata {
|
||||
project_path: "/test".to_string(),
|
||||
|
||||
model: Some("gpt-4".to_string()),
|
||||
|
||||
total_messages: 5,
|
||||
|
||||
system_message: None,
|
||||
|
||||
current_mode_id: None,
|
||||
|
||||
_meta: None,
|
||||
|
||||
project_id: None,
|
||||
},
|
||||
cwd: None,
|
||||
models: None,
|
||||
modes: None,
|
||||
config_options: None,
|
||||
acp_client_id: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&session).expect("Failed to serialize");
|
||||
let deserialized: Session = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(session, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_message_protocol_serialization() {
|
||||
let message = Message {
|
||||
id: "msg_test".to_string(),
|
||||
|
||||
session_id: "ses_test".to_string(),
|
||||
|
||||
role: MessageRole::Assistant,
|
||||
|
||||
created_at: chrono::Utc::now(),
|
||||
|
||||
content: vec![
|
||||
MessagePart::Text {
|
||||
text: "Hello".to_string(),
|
||||
},
|
||||
MessagePart::Tool {
|
||||
tool: "Read".to_string(),
|
||||
tool_call_id: None,
|
||||
input: serde_json::json!({"file": "test.txt"}),
|
||||
|
||||
output: Some(serde_json::json!("content")),
|
||||
},
|
||||
],
|
||||
|
||||
status: MessageStatus::Completed,
|
||||
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&message).expect("Failed to serialize");
|
||||
|
||||
let deserialized: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(message, deserialized);
|
||||
}
|
||||
Reference in New Issue
Block a user