Files
tolaria/src-tauri/src/opencode_events_tests.rs

238 lines
6.7 KiB
Rust
Raw Normal View History

2026-04-28 01:26:21 +02:00
use super::*;
fn dispatch_events(events: impl IntoIterator<Item = serde_json::Value>) -> Vec<AiAgentStreamEvent> {
let mut mapped = Vec::new();
for event in events {
dispatch_event(&event, &mut |event| mapped.push(event));
}
mapped
}
struct ToolExpectation<'a> {
tool_id: &'a str,
tool_name: &'a str,
input: Option<&'a str>,
output: Option<&'a str>,
}
fn assert_tool_pair(events: &[AiAgentStreamEvent], expected: ToolExpectation<'_>) {
assert!(matches!(
&events[0],
AiAgentStreamEvent::ToolStart {
tool_name: actual_name,
tool_id: actual_id,
input: actual_input,
} if actual_name == expected.tool_name
&& actual_id == expected.tool_id
&& actual_input.as_deref() == expected.input
));
assert!(matches!(
&events[1],
AiAgentStreamEvent::ToolDone {
tool_id: actual_id,
output: actual_output,
} if actual_id == expected.tool_id && actual_output.as_deref() == expected.output
));
}
#[test]
fn parse_line_reports_read_errors_and_skips_blank_or_invalid_lines() {
let mut events = Vec::new();
2026-04-29 01:04:13 +02:00
let read_error = parse_line(Err(std::io::Error::other("broken pipe")), &mut |event| {
events.push(event)
});
let blank = parse_line(Ok(" ".into()), &mut |event| events.push(event));
let invalid = parse_line(Ok("not json".into()), &mut |event| events.push(event));
assert!(read_error.is_none());
assert!(blank.is_none());
assert!(invalid.is_none());
assert!(matches!(
&events[0],
AiAgentStreamEvent::Error { message } if message.contains("broken pipe")
));
}
2026-04-28 01:26:21 +02:00
#[test]
fn dispatch_maps_session_reasoning_and_text() {
let mut events = Vec::new();
let started = serde_json::json!({ "type": "session", "sessionID": "ses_1" });
let reasoning = serde_json::json!({ "type": "reasoning", "text": "Checking links" });
let text = serde_json::json!({ "type": "message", "text": "Done" });
for event in [started, reasoning, text] {
dispatch_event(&event, &mut |event| events.push(event));
}
assert!(matches!(
&events[0],
AiAgentStreamEvent::Init { session_id } if session_id == "ses_1"
));
assert!(matches!(
&events[1],
AiAgentStreamEvent::ThinkingDelta { text } if text == "Checking links"
));
assert!(matches!(
&events[2],
AiAgentStreamEvent::TextDelta { text } if text == "Done"
));
}
2026-04-28 22:55:49 +02:00
#[test]
fn dispatch_maps_part_backed_reasoning_and_text() {
let mut events = Vec::new();
let reasoning = serde_json::json!({
"type": "reasoning",
"part": { "type": "reasoning", "text": "Checking links" }
});
let text = serde_json::json!({
"type": "text",
"part": { "type": "text", "text": "Done from OpenCode" }
});
for event in [reasoning, text] {
dispatch_event(&event, &mut |event| events.push(event));
}
assert!(matches!(
&events[0],
AiAgentStreamEvent::ThinkingDelta { text } if text == "Checking links"
));
assert!(matches!(
&events[1],
AiAgentStreamEvent::TextDelta { text } if text == "Done from OpenCode"
));
}
#[test]
fn dispatch_maps_final_text_after_tool_part_wrappers() {
let events = dispatch_events([
serde_json::json!({
"type": "part",
"part": {
"id": "prt_tool_1",
"type": "tool",
"tool": "webfetch",
"input": { "url": "https://example.com" }
}
}),
serde_json::json!({
"type": "part",
"part": {
"id": "prt_text_1",
"type": "text",
"text": "Final answer after tool output."
}
}),
serde_json::json!({
"type": "part",
"part": {
"id": "prt_finish_1",
"type": "step-finish",
"reason": "stop"
}
}),
]);
assert!(matches!(
&events[0],
AiAgentStreamEvent::ToolStart {
tool_name,
tool_id,
input,
} if tool_name == "webfetch"
&& tool_id == "prt_tool_1"
&& input.as_deref() == Some(r#"{"url":"https://example.com"}"#)
));
assert!(matches!(
&events[1],
AiAgentStreamEvent::TextDelta { text } if text == "Final answer after tool output."
));
assert_eq!(events.len(), 2);
}
2026-04-28 01:26:21 +02:00
#[test]
fn dispatch_maps_tool_events() {
let direct = dispatch_events([
serde_json::json!({
"type": "tool_use",
"id": "tool_1",
"name": "read",
"input": { "path": "Note.md" }
}),
serde_json::json!({ "type": "tool_result", "id": "tool_1", "output": "ok" }),
]);
let part_backed = dispatch_events([
serde_json::json!({
"type": "tool_use",
"part": {
"id": "prt_tool_1",
"tool": "read",
"input": { "path": "Note.md" }
}
}),
serde_json::json!({
"type": "tool_result",
"part": {
"id": "prt_tool_1",
"output": "ok"
}
}),
]);
2026-04-28 01:26:21 +02:00
assert_tool_pair(
&direct,
ToolExpectation {
tool_id: "tool_1",
tool_name: "read",
input: Some(r#"{"path":"Note.md"}"#),
output: Some("ok"),
},
);
assert_tool_pair(
&part_backed,
ToolExpectation {
tool_id: "prt_tool_1",
tool_name: "read",
input: Some(r#"{"path":"Note.md"}"#),
output: Some("ok"),
},
);
2026-04-28 01:26:21 +02:00
}
2026-04-28 22:55:49 +02:00
#[test]
fn dispatch_maps_error_events() {
2026-04-28 22:55:49 +02:00
let mut events = Vec::new();
let error = serde_json::json!({ "type": "error", "message": "provider failed" });
2026-04-28 22:55:49 +02:00
dispatch_event(&error, &mut |event| events.push(event));
2026-04-28 22:55:49 +02:00
assert!(matches!(
&events[0],
AiAgentStreamEvent::Error { message } if message == "provider failed"
2026-04-28 22:55:49 +02:00
));
}
2026-04-28 01:26:21 +02:00
#[test]
fn format_error_explains_missing_auth_or_provider_setup() {
let message = format_error(
"provider auth failed: please login".into(),
"exit status: 1".into(),
);
assert!(message.contains("OpenCode CLI is not authenticated"));
assert!(message.contains("opencode auth login"));
}
#[test]
fn format_error_uses_status_or_first_stderr_lines() {
let empty = format_error(String::new(), "exit status: 2".into());
let truncated = format_error("line 1\nline 2\nline 3\nline 4".into(), "ignored".into());
assert_eq!(empty, "opencode exited with status exit status: 2");
assert_eq!(truncated, "line 1\nline 2\nline 3");
}