unified-agent-api 0.3.5

Agent-agnostic facade and registry for wrapper backends
Documentation
use super::support::*;

#[test]
fn claude_emits_handle_facet_once_when_first_event_is_status() {
    let adapter = new_adapter();

    let out1 = adapter.map_event(ClaudeBackendEvent::Stream(parse_stream_json_fixture(
        SYSTEM_INIT,
    )));
    let facet_events_1: Vec<_> = out1
        .iter()
        .filter(|event| handle_facet_schema(event) == Some(CAP_SESSION_HANDLE_V1))
        .collect();
    assert_eq!(facet_events_1.len(), 1);
    assert_eq!(facet_events_1[0].kind, AgentWrapperEventKind::Status);
    assert_eq!(
        facet_events_1[0]
            .data
            .as_ref()
            .and_then(|v| v.pointer("/session/id"))
            .and_then(|v| v.as_str()),
        Some("sess-1")
    );

    let out2 = adapter.map_event(ClaudeBackendEvent::Stream(parse_stream_json_fixture(
        SYSTEM_OTHER,
    )));
    assert!(
        out2.iter()
            .all(|event| handle_facet_schema(event) != Some(CAP_SESSION_HANDLE_V1)),
        "expected subsequent events to not re-emit the handle facet"
    );
}

#[test]
fn claude_emits_synthetic_status_handle_facet_when_first_event_is_not_status() {
    let adapter = new_adapter();

    let out = adapter.map_event(ClaudeBackendEvent::Stream(parse_stream_json_fixture(
        ASSISTANT_MESSAGE_TEXT,
    )));
    assert_eq!(out.len(), 2);
    assert_eq!(out[0].kind, AgentWrapperEventKind::TextOutput);
    assert_eq!(out[0].text.as_deref(), Some("hello"));

    assert_eq!(out[1].kind, AgentWrapperEventKind::Status);
    assert_eq!(handle_facet_schema(&out[1]), Some(CAP_SESSION_HANDLE_V1));
    assert_eq!(
        out[1]
            .data
            .as_ref()
            .and_then(|v| v.pointer("/session/id"))
            .and_then(|v| v.as_str()),
        Some("sess-1")
    );
}

#[test]
fn claude_completion_attaches_handle_facet_when_id_is_known() {
    let adapter = new_adapter();

    let _events = adapter.map_event(ClaudeBackendEvent::Stream(parse_stream_json_fixture(
        SYSTEM_INIT,
    )));

    let completion = adapter
        .map_completion(super::super::harness::ClaudeBackendCompletion {
            status: success_exit_status(),
            final_text: None,
            backend_error_message: None,
        })
        .expect("completion maps");

    assert_eq!(
        completion
            .data
            .as_ref()
            .and_then(|v| v.get("schema"))
            .and_then(|v| v.as_str()),
        Some(CAP_SESSION_HANDLE_V1)
    );
    assert_eq!(
        completion
            .data
            .as_ref()
            .and_then(|v| v.pointer("/session/id"))
            .and_then(|v| v.as_str()),
        Some("sess-1")
    );
}

#[test]
fn claude_whitespace_session_id_is_treated_as_unknown() {
    let adapter = new_adapter();

    let out = adapter.map_event(ClaudeBackendEvent::Stream(parse_single_line(
        r#"{"type":"system","subtype":"init","session_id":"   "}"#,
    )));
    assert!(
        out.iter()
            .all(|event| handle_facet_schema(event) != Some(CAP_SESSION_HANDLE_V1)),
        "expected whitespace-only session_id to not produce a handle facet"
    );

    let completion = adapter
        .map_completion(super::super::harness::ClaudeBackendCompletion {
            status: success_exit_status(),
            final_text: None,
            backend_error_message: None,
        })
        .expect("completion maps");
    assert!(completion.data.is_none());
}

#[test]
fn claude_oversize_session_id_is_omitted_and_warns_once() {
    let adapter = new_adapter();

    let oversize = "a".repeat(SESSION_HANDLE_ID_BOUND_BYTES + 1);
    let line = format!(r#"{{"type":"system","subtype":"init","session_id":"{oversize}"}}"#);
    let out1 = adapter.map_event(ClaudeBackendEvent::Stream(parse_single_line(&line)));

    assert!(
        out1.iter()
            .all(|event| handle_facet_schema(event) != Some(CAP_SESSION_HANDLE_V1)),
        "expected oversize session_id to omit the handle facet"
    );

    let warnings_1: Vec<_> = out1
        .iter()
        .filter(|event| event.message.as_deref() == Some(SESSION_HANDLE_OVERSIZE_WARNING))
        .collect();
    assert_eq!(warnings_1.len(), 1);
    assert_eq!(warnings_1[0].kind, AgentWrapperEventKind::Status);
    assert!(warnings_1[0].data.is_none());

    let out2 = adapter.map_event(ClaudeBackendEvent::Stream(parse_single_line(&line)));
    assert!(
        out2.iter()
            .all(|event| event.message.as_deref() != Some(SESSION_HANDLE_OVERSIZE_WARNING)),
        "expected oversize warning to be emitted at most once"
    );

    let completion = adapter
        .map_completion(super::super::harness::ClaudeBackendCompletion {
            status: success_exit_status(),
            final_text: None,
            backend_error_message: None,
        })
        .expect("completion maps");
    assert!(completion.data.is_none());
}