unified-agent-api-opencode 0.2.3

Async wrapper around the OpenCode CLI for canonical run --format json flows
Documentation
mod support_paths;

use opencode::{OpencodeRunJsonErrorCode, OpencodeRunJsonEvent, OpencodeRunJsonParser};

fn read_fixture(name: &str) -> String {
    std::fs::read_to_string(support_paths::opencode_run_json_fixtures_dir().join(name))
        .expect("read fixture")
}

fn parse_single_line(name: &str) -> OpencodeRunJsonEvent {
    let mut parser = OpencodeRunJsonParser::new();
    let text = read_fixture(name);
    let line = text
        .lines()
        .find(|line| !line.chars().all(|ch| ch.is_whitespace()))
        .expect("fixture contains one non-empty line");
    parser.parse_line(line).unwrap().unwrap()
}

#[test]
fn parses_step_start_text_and_step_finish() {
    assert!(matches!(
        parse_single_line("step_start.jsonl"),
        OpencodeRunJsonEvent::StepStart { .. }
    ));

    match parse_single_line("text.jsonl") {
        OpencodeRunJsonEvent::Text { text, .. } => assert_eq!(text, "OK"),
        other => panic!("expected text event, got {other:?}"),
    }

    assert!(matches!(
        parse_single_line("step_finish.jsonl"),
        OpencodeRunJsonEvent::StepFinish { .. }
    ));
}

#[test]
fn unknown_outer_type_is_unknown_event_not_error() {
    match parse_single_line("unknown_outer_type.jsonl") {
        OpencodeRunJsonEvent::Unknown { event_type, .. } => assert_eq!(event_type, "tool_call"),
        other => panic!("expected unknown event, got {other:?}"),
    }
}

#[test]
fn missing_required_text_field_is_typedparse() {
    let mut parser = OpencodeRunJsonParser::new();
    let line = read_fixture("missing_text_field.jsonl");
    let err = parser.parse_line(line.trim()).unwrap_err();
    assert_eq!(err.code, OpencodeRunJsonErrorCode::TypedParse);
}

#[test]
fn blank_lines_are_ignored_and_crlf_is_tolerated() {
    let mut parser = OpencodeRunJsonParser::new();
    let text = read_fixture("blank_lines.jsonl");
    let mut count = 0usize;
    for line in text.lines() {
        if let Ok(Some(_)) = parser.parse_line(line) {
            count += 1;
        }
    }
    assert_eq!(count, 3);

    let mut parser = OpencodeRunJsonParser::new();
    let text = read_fixture("crlf_lines.jsonl");
    for line in text.lines() {
        let output = parser.parse_line(&format!("{line}\r")).unwrap();
        assert!(output.is_some());
    }
}

#[test]
fn parser_carries_forward_session_id_across_known_events() {
    let mut parser = OpencodeRunJsonParser::new();
    let step_start = read_fixture("step_start.jsonl");
    let text = read_fixture("text_without_session.jsonl");

    let first = parser.parse_line(step_start.trim()).unwrap().unwrap();
    assert_eq!(first.session_id(), Some("session-test"));

    let second = parser.parse_line(text.trim()).unwrap().unwrap();
    assert_eq!(second.session_id(), Some("session-test"));
}