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

1066 lines
35 KiB
Rust

/// Comprehensive integration tests for OpenCode event → SessionUpdate translation
/// Tests realistic event sequences with proper ordering, metadata, and tool lifecycle tracking
use dirigent_protocol::adapters::{OpenCodeAdapter, TranslationError};
use dirigent_protocol::types::{ContentBlock, SessionUpdate, ToolCallStatus};
use opencode_client::types as oc;
use serde_json::json;
// ===== Helper Functions =====
/// Create a text part for testing
fn create_text_part(
session_id: &str,
message_id: &str,
part_id: &str,
text: &str,
) -> oc::Part {
oc::Part::Text(oc::TextPart {
id: part_id.to_string(),
session_id: session_id.to_string(),
message_id: message_id.to_string(),
text: text.to_string(),
synthetic: None,
time: Some(oc::PartTime {
start: 1000,
end: None,
}),
})
}
/// Create a reasoning part for testing
fn create_reasoning_part(
session_id: &str,
message_id: &str,
part_id: &str,
text: &str,
) -> oc::Part {
oc::Part::Reasoning(oc::ReasoningPart {
id: part_id.to_string(),
session_id: session_id.to_string(),
message_id: message_id.to_string(),
text: text.to_string(),
time: Some(oc::PartTime {
start: 1000,
end: None,
}),
})
}
/// Create a tool part with given state
fn create_tool_part(
session_id: &str,
message_id: &str,
part_id: &str,
tool: &str,
state: oc::ToolState,
) -> oc::Part {
oc::Part::Tool(oc::ToolPart {
id: part_id.to_string(),
session_id: session_id.to_string(),
message_id: message_id.to_string(),
call_id: format!("call_{}", part_id),
tool: tool.to_string(),
state,
metadata: None,
})
}
/// Create a file part for testing
fn create_file_part(
session_id: &str,
message_id: &str,
part_id: &str,
filename: &str,
url: &str,
mime: &str,
) -> oc::Part {
oc::Part::File(oc::FilePart {
id: part_id.to_string(),
session_id: session_id.to_string(),
message_id: message_id.to_string(),
mime: mime.to_string(),
filename: Some(filename.to_string()),
url: url.to_string(),
source: None,
})
}
/// Create a MessagePartUpdated event
fn create_part_event(part: oc::Part, delta: Option<String>) -> oc::Event {
oc::Event::MessagePartUpdated {
properties: oc::MessagePartEventInfo { part, delta },
}
}
// ===== Test Case 1: Text Streaming =====
/// Test text streaming produces AgentMessageChunk sequence with proper metadata
#[test]
fn test_text_streaming_to_agent_message_chunks() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_text";
let message_id = "msg_text";
// Simulate streaming text in multiple chunks
let chunks = vec![
("part_1", "Hello", "Hello"),
("part_2", "Hello world", " world"),
("part_3", "Hello world!", "!"),
];
let mut updates = vec![];
for (part_id, full_text, delta) in chunks {
let part = create_text_part(session_id, message_id, part_id, full_text);
let event = create_part_event(part, Some(delta.to_string()));
let result = adapter.translate_to_session_update(event);
assert!(result.is_ok(), "Translation should succeed");
let update = result.unwrap();
assert!(update.is_some(), "Should return an update");
updates.push(update.unwrap());
}
// Verify all updates are AgentMessageChunk
assert_eq!(updates.len(), 3);
for (i, update) in updates.iter().enumerate() {
match update {
SessionUpdate::AgentMessageChunk {
message_id: msg_id,
content,
_meta,
} => {
assert_eq!(msg_id, message_id);
// Verify content is Text
match content {
ContentBlock::Text { text } => {
// Each chunk should contain the full text up to that point
assert!(!text.is_empty());
}
_ => panic!("Expected Text content"),
}
// Verify metadata is present
assert!(_meta.is_some(), "Metadata should be present on chunk {}", i);
let meta = _meta.as_ref().unwrap();
assert!(meta.provider.is_some(), "Provider metadata should be present");
let provider = meta.provider.as_ref().unwrap();
assert_eq!(provider.name, "opencode");
assert!(provider.original_ids.is_some());
let ids = provider.original_ids.as_ref().unwrap();
assert_eq!(ids.get("session_id").unwrap(), session_id);
assert_eq!(ids.get("message_id").unwrap(), message_id);
assert!(ids.contains_key("part_id"));
}
_ => panic!("Expected AgentMessageChunk at index {}", i),
}
}
}
/// Test that chunks are created in order
#[test]
fn test_text_chunks_preserve_order() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_order";
let message_id = "msg_order";
let expected_order = vec!["First", "Second", "Third", "Fourth"];
for (i, text) in expected_order.iter().enumerate() {
let part_id = format!("part_{}", i);
let part = create_text_part(session_id, message_id, &part_id, text);
let event = create_part_event(part, Some(text.to_string()));
let result = adapter.translate_to_session_update(event);
assert!(result.is_ok());
let update = result.unwrap().unwrap();
match update {
SessionUpdate::AgentMessageChunk { content, .. } => {
if let ContentBlock::Text { text: chunk_text } = content {
assert_eq!(chunk_text, *text);
} else {
panic!("Expected Text content");
}
}
_ => panic!("Expected AgentMessageChunk"),
}
}
}
// ===== Test Case 2: Reasoning Streaming =====
/// Test reasoning streaming produces AgentThoughtChunk sequence with metadata
#[test]
fn test_reasoning_streaming_to_agent_thought_chunks() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_reason";
let message_id = "msg_reason";
// Simulate streaming reasoning in chunks
// Each chunk: (part_id, full_text, delta, expected_delta)
let chunks = vec![
("reason_1", "I need to", "I need to"),
("reason_2", "I need to analyze", " analyze"),
("reason_3", "I need to analyze the problem", " the problem"),
];
let mut updates = vec![];
for (part_id, full_text, delta) in chunks.iter() {
let part = create_reasoning_part(session_id, message_id, part_id, full_text);
let event = create_part_event(part, Some(delta.to_string()));
let result = adapter.translate_to_session_update(event);
assert!(result.is_ok());
let update = result.unwrap();
assert!(update.is_some());
updates.push((update.unwrap(), delta));
}
// Verify all are AgentThoughtChunk with metadata
assert_eq!(updates.len(), 3);
for (i, (update, expected_delta)) in updates.iter().enumerate() {
match update {
SessionUpdate::AgentThoughtChunk {
message_id: msg_id,
content,
_meta,
} => {
assert_eq!(msg_id, message_id);
// Verify content contains the delta, not the full accumulated text
match content {
ContentBlock::Text { text } => {
assert_eq!(text, *expected_delta, "Chunk {} should contain delta", i);
}
_ => panic!("Expected Text content"),
}
// Verify metadata
assert!(_meta.is_some(), "Metadata should be present on chunk {}", i);
let meta = _meta.as_ref().unwrap();
assert!(meta.provider.is_some());
let provider = meta.provider.as_ref().unwrap();
assert_eq!(provider.name, "opencode");
assert!(provider.original_ids.is_some());
let ids = provider.original_ids.as_ref().unwrap();
assert_eq!(ids.get("session_id").unwrap(), session_id);
assert_eq!(ids.get("message_id").unwrap(), message_id);
}
_ => panic!("Expected AgentThoughtChunk at index {}", i),
}
}
}
// ===== Test Case 3: Tool Lifecycle (Pending → Running → Completed) =====
/// Test complete tool lifecycle produces ToolCall followed by ToolCallUpdate events
#[test]
fn test_tool_lifecycle_pending_to_completed() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_tool";
let message_id = "msg_tool";
let part_id = "tool_1";
let tool_name = "bash";
let input = json!({"command": "ls -la"});
let output = "file1.txt\nfile2.txt";
// Step 1: Pending state → ToolCall
let pending_part = create_tool_part(
session_id,
message_id,
part_id,
tool_name,
oc::ToolState::Pending,
);
let pending_event = create_part_event(pending_part, None);
let result = adapter.translate_to_session_update(pending_event);
assert!(result.is_ok());
let update = result.unwrap().unwrap();
match &update {
SessionUpdate::ToolCall {
message_id: msg_id,
tool_call,
_meta,
} => {
assert_eq!(msg_id, message_id);
assert_eq!(tool_call.id, part_id);
assert_eq!(tool_call.tool_name, tool_name);
assert_eq!(tool_call.status, ToolCallStatus::Pending);
assert!(tool_call.raw_input.is_none());
assert!(tool_call.raw_output.is_none());
assert!(tool_call.error.is_none());
// Verify metadata
assert!(_meta.is_some());
let meta = _meta.as_ref().unwrap();
assert!(meta.provider.is_some());
let provider = meta.provider.as_ref().unwrap();
assert_eq!(provider.name, "opencode");
}
_ => panic!("Expected ToolCall for pending state"),
}
// Step 2: Running state → ToolCallUpdate
let running_part = create_tool_part(
session_id,
message_id,
part_id,
tool_name,
oc::ToolState::Running {
input: input.clone(),
title: None,
metadata: None,
time: oc::PartTime {
start: 1000,
end: None,
},
},
);
let running_event = create_part_event(running_part, Some("running".to_string()));
let result = adapter.translate_to_session_update(running_event);
assert!(result.is_ok());
let update = result.unwrap().unwrap();
match &update {
SessionUpdate::ToolCallUpdate {
message_id: msg_id,
tool_call_id,
tool_call,
_meta,
} => {
assert_eq!(msg_id, message_id);
assert_eq!(tool_call_id, part_id);
assert_eq!(tool_call.id, part_id);
assert_eq!(tool_call.tool_name, tool_name);
assert_eq!(tool_call.status, ToolCallStatus::Running);
assert_eq!(tool_call.raw_input, Some(input.clone()));
assert!(tool_call.raw_output.is_none());
assert!(tool_call.error.is_none());
// Verify metadata throughout lifecycle
assert!(_meta.is_some());
let meta = _meta.as_ref().unwrap();
assert!(meta.provider.is_some());
}
_ => panic!("Expected ToolCallUpdate for running state"),
}
// Step 3: Completed state → ToolCallUpdate
let completed_part = create_tool_part(
session_id,
message_id,
part_id,
tool_name,
oc::ToolState::Completed {
input: input.clone(),
output: output.to_string(),
title: "bash command".to_string(),
metadata: serde_json::Value::Null,
time: oc::PartTime {
start: 1000,
end: Some(2000),
},
attachments: None,
},
);
let completed_event = create_part_event(completed_part, Some(output.to_string()));
let result = adapter.translate_to_session_update(completed_event);
assert!(result.is_ok());
let update = result.unwrap().unwrap();
match &update {
SessionUpdate::ToolCallUpdate {
message_id: msg_id,
tool_call_id,
tool_call,
_meta,
} => {
assert_eq!(msg_id, message_id);
assert_eq!(tool_call_id, part_id);
assert_eq!(tool_call.id, part_id);
assert_eq!(tool_call.tool_name, tool_name);
assert_eq!(tool_call.status, ToolCallStatus::Completed);
assert_eq!(tool_call.raw_input, Some(input));
assert_eq!(
tool_call.raw_output,
Some(serde_json::Value::String(output.to_string()))
);
assert!(tool_call.error.is_none());
// Verify metadata in final state
assert!(_meta.is_some());
}
_ => panic!("Expected ToolCallUpdate for completed state"),
}
}
/// Test status transitions are properly tracked
#[test]
fn test_tool_status_transitions() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_status";
let message_id = "msg_status";
let part_id = "tool_status";
let expected_statuses = vec![
(oc::ToolState::Pending, ToolCallStatus::Pending),
(
oc::ToolState::Running {
input: json!({}),
title: None,
metadata: None,
time: oc::PartTime {
start: 1000,
end: None,
},
},
ToolCallStatus::Running,
),
(
oc::ToolState::Completed {
input: json!({}),
output: "done".to_string(),
title: "".to_string(),
metadata: serde_json::Value::Null,
time: oc::PartTime {
start: 1000,
end: Some(2000),
},
attachments: None,
},
ToolCallStatus::Completed,
),
];
for (i, (oc_state, expected_status)) in expected_statuses.into_iter().enumerate() {
let part = create_tool_part(session_id, message_id, part_id, "test", oc_state);
let event = create_part_event(part, Some("delta".to_string()));
let result = adapter.translate_to_session_update(event);
assert!(result.is_ok());
let update = result.unwrap().unwrap();
let actual_status = match &update {
SessionUpdate::ToolCall { tool_call, .. } => &tool_call.status,
SessionUpdate::ToolCallUpdate { tool_call, .. } => &tool_call.status,
_ => panic!("Expected tool update at step {}", i),
};
assert_eq!(actual_status, &expected_status, "Status mismatch at step {}", i);
}
}
// ===== Test Case 4: Tool Error =====
/// Test tool error produces ToolCallUpdate with Error status and error field
#[test]
fn test_tool_error_to_tool_call_update() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_error";
let message_id = "msg_error";
let part_id = "tool_error";
let error_msg = "Command not found: invalid_cmd";
let input = json!({"command": "invalid_cmd"});
// First, create the tool with pending state
let pending_part = create_tool_part(
session_id,
message_id,
part_id,
"bash",
oc::ToolState::Pending,
);
let pending_event = create_part_event(pending_part, None);
let _ = adapter.translate_to_session_update(pending_event);
// Now send error state
let error_part = create_tool_part(
session_id,
message_id,
part_id,
"bash",
oc::ToolState::Error {
input: input.clone(),
error: error_msg.to_string(),
metadata: None,
time: oc::PartTime {
start: 1000,
end: Some(2000),
},
},
);
let error_event = create_part_event(error_part, Some(error_msg.to_string()));
let result = adapter.translate_to_session_update(error_event);
assert!(result.is_ok());
let update = result.unwrap().unwrap();
match &update {
SessionUpdate::ToolCallUpdate {
message_id: msg_id,
tool_call_id,
tool_call,
_meta,
} => {
assert_eq!(msg_id, message_id);
assert_eq!(tool_call_id, part_id);
assert_eq!(tool_call.status, ToolCallStatus::Error);
assert_eq!(tool_call.raw_input, Some(input));
assert!(tool_call.raw_output.is_none());
assert_eq!(tool_call.error, Some(error_msg.to_string()));
// Verify error message is captured
let err = tool_call.error.as_ref().unwrap();
assert!(err.contains("Command not found"));
// Verify metadata
assert!(_meta.is_some());
}
_ => panic!("Expected ToolCallUpdate for error state"),
}
}
// ===== Test Case 5: File Reference =====
/// Test file part produces ResourceLink in AgentMessageChunk
#[test]
fn test_file_reference_to_resource_link() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_file";
let message_id = "msg_file";
let part_id = "file_1";
let filename = "test_document.pdf";
let uri = "file:///home/user/documents/test_document.pdf";
let mime_type = "application/pdf";
let part = create_file_part(session_id, message_id, part_id, filename, uri, mime_type);
let event = create_part_event(part, None);
let result = adapter.translate_to_session_update(event);
assert!(result.is_ok());
let update = result.unwrap().unwrap();
match &update {
SessionUpdate::AgentMessageChunk {
message_id: msg_id,
content,
_meta,
} => {
assert_eq!(msg_id, message_id);
// Verify content is ResourceLink
match content {
ContentBlock::ResourceLink {
uri: content_uri,
name,
mime_type: content_mime,
} => {
assert_eq!(content_uri, uri);
assert_eq!(name.as_ref().unwrap(), filename);
assert_eq!(content_mime.as_ref().unwrap(), mime_type);
}
_ => panic!("Expected ResourceLink content"),
}
// Verify metadata
assert!(_meta.is_some());
let meta = _meta.as_ref().unwrap();
assert!(meta.provider.is_some());
let provider = meta.provider.as_ref().unwrap();
assert_eq!(provider.name, "opencode");
assert!(provider.original_ids.is_some());
let ids = provider.original_ids.as_ref().unwrap();
assert_eq!(ids.get("part_id").unwrap(), part_id);
}
_ => panic!("Expected AgentMessageChunk with ResourceLink"),
}
}
/// Test file with different mime types
#[test]
fn test_file_references_various_mime_types() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_files";
let message_id = "msg_files";
let test_cases = vec![
("file_txt", "data.txt", "file:///data.txt", "text/plain"),
("file_json", "config.json", "file:///config.json", "application/json"),
("file_img", "screenshot.png", "file:///screenshot.png", "image/png"),
];
for (part_id, filename, uri, mime_type) in test_cases {
let part = create_file_part(session_id, message_id, part_id, filename, uri, mime_type);
let event = create_part_event(part, None);
let result = adapter.translate_to_session_update(event);
assert!(result.is_ok());
let update = result.unwrap().unwrap();
match update {
SessionUpdate::AgentMessageChunk { content, .. } => {
if let ContentBlock::ResourceLink {
uri: content_uri,
name,
mime_type: content_mime,
} = content
{
assert_eq!(content_uri, uri);
assert_eq!(name.as_ref().unwrap(), filename);
assert_eq!(content_mime.as_ref().unwrap(), mime_type);
} else {
panic!("Expected ResourceLink for {}", filename);
}
}
_ => panic!("Expected AgentMessageChunk for {}", filename),
}
}
}
// ===== Test Case 6: Interleaved Updates Preserve Order =====
/// Test mix of text, reasoning, and tool updates maintain correct order
#[test]
fn test_interleaved_updates_preserve_order() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_interleaved";
let message_id = "msg_interleaved";
// Create a realistic sequence of interleaved events
let events = vec![
// Text chunk 1
(
"text",
create_part_event(
create_text_part(session_id, message_id, "part_1", "Let me think"),
Some("Let me think".to_string()),
),
),
// Reasoning chunk 1
(
"thought",
create_part_event(
create_reasoning_part(session_id, message_id, "reason_1", "Analyzing the task"),
Some("Analyzing the task".to_string()),
),
),
// Tool pending
(
"tool_pending",
create_part_event(
create_tool_part(session_id, message_id, "tool_1", "bash", oc::ToolState::Pending),
None,
),
),
// Text chunk 2
(
"text",
create_part_event(
create_text_part(session_id, message_id, "part_2", "Running command"),
Some("Running command".to_string()),
),
),
// Tool running
(
"tool_running",
create_part_event(
create_tool_part(
session_id,
message_id,
"tool_1",
"bash",
oc::ToolState::Running {
input: json!({"cmd": "ls"}),
title: None,
metadata: None,
time: oc::PartTime {
start: 1000,
end: None,
},
},
),
Some("running".to_string()),
),
),
// Reasoning chunk 2
(
"thought",
create_part_event(
create_reasoning_part(session_id, message_id, "reason_2", "Command is executing"),
Some("Command is executing".to_string()),
),
),
// Tool completed
(
"tool_completed",
create_part_event(
create_tool_part(
session_id,
message_id,
"tool_1",
"bash",
oc::ToolState::Completed {
input: json!({"cmd": "ls"}),
output: "file1.txt".to_string(),
title: "".to_string(),
metadata: serde_json::Value::Null,
time: oc::PartTime {
start: 1000,
end: Some(2000),
},
attachments: None,
},
),
Some("file1.txt".to_string()),
),
),
// Text chunk 3
(
"text",
create_part_event(
create_text_part(session_id, message_id, "part_3", "Done!"),
Some("Done!".to_string()),
),
),
];
let expected_types = vec![
"text",
"thought",
"tool_pending",
"text",
"tool_running",
"thought",
"tool_completed",
"text",
];
let mut updates = vec![];
for (i, (_, event)) in events.into_iter().enumerate() {
let result = adapter.translate_to_session_update(event);
assert!(result.is_ok(), "Event {} should translate successfully", i);
let update = result.unwrap();
assert!(update.is_some(), "Event {} should produce an update", i);
updates.push(update.unwrap());
}
// Verify order is preserved
assert_eq!(updates.len(), expected_types.len());
for (i, (update, expected_type)) in updates.iter().zip(expected_types.iter()).enumerate() {
match (update, *expected_type) {
(SessionUpdate::AgentMessageChunk { .. }, "text") => {}
(SessionUpdate::AgentThoughtChunk { .. }, "thought") => {}
(SessionUpdate::ToolCall { .. }, "tool_pending") => {}
(SessionUpdate::ToolCallUpdate { tool_call, .. }, "tool_running") => {
assert_eq!(tool_call.status, ToolCallStatus::Running);
}
(SessionUpdate::ToolCallUpdate { tool_call, .. }, "tool_completed") => {
assert_eq!(tool_call.status, ToolCallStatus::Completed);
}
_ => panic!(
"Type mismatch at position {}: expected {}, got {:?}",
i, expected_type, update
),
}
}
}
/// Test correct variant for each update type in sequence
#[test]
fn test_correct_variant_for_each_type() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_variants";
let message_id = "msg_variants";
// Test text → AgentMessageChunk
let text_event = create_part_event(
create_text_part(session_id, message_id, "p1", "text"),
Some("text".to_string()),
);
let result = adapter.translate_to_session_update(text_event).unwrap().unwrap();
assert!(matches!(result, SessionUpdate::AgentMessageChunk { .. }));
// Test reasoning → AgentThoughtChunk
let reason_event = create_part_event(
create_reasoning_part(session_id, message_id, "p2", "thinking"),
Some("thinking".to_string()),
);
let result = adapter.translate_to_session_update(reason_event).unwrap().unwrap();
assert!(matches!(result, SessionUpdate::AgentThoughtChunk { .. }));
// Test tool pending → ToolCall
let tool_pending_event = create_part_event(
create_tool_part(session_id, message_id, "p3", "test", oc::ToolState::Pending),
None,
);
let result = adapter.translate_to_session_update(tool_pending_event).unwrap().unwrap();
assert!(matches!(result, SessionUpdate::ToolCall { .. }));
// Test tool running → ToolCallUpdate
let tool_running_event = create_part_event(
create_tool_part(
session_id,
message_id,
"p3",
"test",
oc::ToolState::Running {
input: json!({}),
title: None,
metadata: None,
time: oc::PartTime {
start: 1000,
end: None,
},
},
),
Some("running".to_string()),
);
let result = adapter.translate_to_session_update(tool_running_event).unwrap().unwrap();
assert!(matches!(result, SessionUpdate::ToolCallUpdate { .. }));
// Test file → AgentMessageChunk with ResourceLink
let file_event = create_part_event(
create_file_part(session_id, message_id, "p4", "f.txt", "file:///f.txt", "text/plain"),
None,
);
let result = adapter.translate_to_session_update(file_event).unwrap().unwrap();
match result {
SessionUpdate::AgentMessageChunk { content, .. } => {
assert!(matches!(content, ContentBlock::ResourceLink { .. }));
}
_ => panic!("Expected AgentMessageChunk for file"),
}
}
// ===== Additional Edge Cases =====
/// Test duplicate detection for SessionUpdate translation
#[test]
fn test_duplicate_part_skipped_in_session_update() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_dup";
let message_id = "msg_dup";
let part_id = "part_dup";
// First event with delta
let part1 = create_text_part(session_id, message_id, part_id, "Hello");
let event1 = create_part_event(part1, Some("Hello".to_string()));
let result1 = adapter.translate_to_session_update(event1);
assert!(result1.is_ok());
assert!(result1.unwrap().is_some());
// Second event without delta (completion marker) - should be skipped
let part2 = create_text_part(session_id, message_id, part_id, "Hello");
let event2 = create_part_event(part2, None);
let result2 = adapter.translate_to_session_update(event2);
assert!(result2.is_ok());
assert!(result2.unwrap().is_none(), "Duplicate without delta should be skipped");
}
/// Test unsupported event types return error
#[test]
fn test_unsupported_events_return_error() {
let adapter = OpenCodeAdapter::new();
// SessionCreated is not supported in translate_to_session_update
let session_event = oc::Event::SessionCreated {
properties: oc::SessionEventInfo {
info: oc::Session {
id: "sess1".to_string(),
project_id: "proj1".to_string(),
directory: "/test".to_string(),
parent_id: None,
summary: None,
share: None,
title: "Test".to_string(),
version: "1.0".to_string(),
time: oc::SessionTime {
created: 1000,
updated: 1000,
compacting: None,
},
revert: None,
},
},
};
let result = adapter.translate_to_session_update(session_event);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), TranslationError::UnsupportedEvent));
}
/// Test unsupported part types return error
#[test]
fn test_unsupported_part_types_return_error() {
let adapter = OpenCodeAdapter::new();
// Snapshot part is not supported
let snapshot_part = oc::Part::Snapshot(oc::SnapshotPart {
id: "snap1".to_string(),
session_id: "sess1".to_string(),
message_id: "msg1".to_string(),
snapshot: "snapshot_data".to_string(),
});
let event = create_part_event(snapshot_part, None);
let result = adapter.translate_to_session_update(event);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), TranslationError::UnsupportedPartType));
}
/// Test multiple concurrent tool calls tracked independently
#[test]
fn test_multiple_concurrent_tools_independent_tracking() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_multi";
let message_id = "msg_multi";
// Tool 1: Pending
let tool1_pending = create_part_event(
create_tool_part(session_id, message_id, "tool_1", "bash", oc::ToolState::Pending),
None,
);
let result1 = adapter.translate_to_session_update(tool1_pending).unwrap().unwrap();
assert!(matches!(result1, SessionUpdate::ToolCall { .. }));
// Tool 2: Pending (different tool)
let tool2_pending = create_part_event(
create_tool_part(session_id, message_id, "tool_2", "read", oc::ToolState::Pending),
None,
);
let result2 = adapter.translate_to_session_update(tool2_pending).unwrap().unwrap();
assert!(matches!(result2, SessionUpdate::ToolCall { .. }));
// Tool 1: Running → should be ToolCallUpdate
let tool1_running = create_part_event(
create_tool_part(
session_id,
message_id,
"tool_1",
"bash",
oc::ToolState::Running {
input: json!({}),
title: None,
metadata: None,
time: oc::PartTime {
start: 1000,
end: None,
},
},
),
Some("r".to_string()),
);
let result3 = adapter.translate_to_session_update(tool1_running).unwrap().unwrap();
match result3 {
SessionUpdate::ToolCallUpdate { tool_call_id, .. } => {
assert_eq!(tool_call_id, "tool_1");
}
_ => panic!("Expected ToolCallUpdate for tool_1"),
}
// Tool 2: Running → should also be ToolCallUpdate
let tool2_running = create_part_event(
create_tool_part(
session_id,
message_id,
"tool_2",
"read",
oc::ToolState::Running {
input: json!({}),
title: None,
metadata: None,
time: oc::PartTime {
start: 1000,
end: None,
},
},
),
Some("r".to_string()),
);
let result4 = adapter.translate_to_session_update(tool2_running).unwrap().unwrap();
match result4 {
SessionUpdate::ToolCallUpdate { tool_call_id, .. } => {
assert_eq!(tool_call_id, "tool_2");
}
_ => panic!("Expected ToolCallUpdate for tool_2"),
}
}
/// Test metadata present in all update types
#[test]
fn test_metadata_present_in_all_updates() {
let adapter = OpenCodeAdapter::new();
let session_id = "sess_meta";
let message_id = "msg_meta";
// Text
let text_event = create_part_event(
create_text_part(session_id, message_id, "p1", "text"),
Some("text".to_string()),
);
let text_update = adapter.translate_to_session_update(text_event).unwrap().unwrap();
match text_update {
SessionUpdate::AgentMessageChunk { _meta, .. } => {
assert!(_meta.is_some(), "Text chunk should have metadata");
}
_ => panic!("Expected AgentMessageChunk"),
}
// Reasoning
let reason_event = create_part_event(
create_reasoning_part(session_id, message_id, "p2", "reason"),
Some("reason".to_string()),
);
let reason_update = adapter.translate_to_session_update(reason_event).unwrap().unwrap();
match reason_update {
SessionUpdate::AgentThoughtChunk { _meta, .. } => {
assert!(_meta.is_some(), "Thought chunk should have metadata");
}
_ => panic!("Expected AgentThoughtChunk"),
}
// Tool
let tool_event = create_part_event(
create_tool_part(session_id, message_id, "p3", "bash", oc::ToolState::Pending),
None,
);
let tool_update = adapter.translate_to_session_update(tool_event).unwrap().unwrap();
match tool_update {
SessionUpdate::ToolCall { _meta, .. } => {
assert!(_meta.is_some(), "ToolCall should have metadata");
}
_ => panic!("Expected ToolCall"),
}
// File
let file_event = create_part_event(
create_file_part(session_id, message_id, "p4", "f.txt", "file:///f.txt", "text/plain"),
None,
);
let file_update = adapter.translate_to_session_update(file_event).unwrap().unwrap();
match file_update {
SessionUpdate::AgentMessageChunk { _meta, .. } => {
assert!(_meta.is_some(), "File chunk should have metadata");
}
_ => panic!("Expected AgentMessageChunk"),
}
}