rho-coding-agent 1.19.2

A lightweight agent harness inspired by Pi
Documentation
#[cfg(unix)]
use super::HerdrState;
use super::{graphics_info_reports_paintable, HerdrGraphicsCapability, HerdrReporter};
use std::collections::HashMap;

#[test]
fn disabled_without_complete_herdr_environment() {
    let reporter = HerdrReporter::from_env_vars(|key| match key {
        "HERDR_ENV" => Some("1".into()),
        "HERDR_SOCKET_PATH" => Some("/tmp/herdr.sock".into()),
        _ => None,
    });

    assert!(!reporter.is_enabled());
}

#[cfg(unix)]
#[test]
fn enabled_from_complete_herdr_environment() {
    let values = HashMap::from([
        ("HERDR_ENV", "1"),
        ("HERDR_SOCKET_PATH", "/tmp/herdr.sock"),
        ("HERDR_PANE_ID", "w1:p1"),
    ]);
    let reporter = HerdrReporter::from_env_vars(|key| values.get(key).map(|value| (*value).into()));

    assert!(reporter.is_enabled());
}

#[cfg(windows)]
#[test]
fn disabled_when_platform_does_not_support_herdr_socket() {
    let values = HashMap::from([
        ("HERDR_ENV", "1"),
        ("HERDR_SOCKET_PATH", r"C:\\temp\\herdr.sock"),
        ("HERDR_PANE_ID", "w1:p1"),
    ]);
    let reporter = HerdrReporter::from_env_vars(|key| values.get(key).map(|value| (*value).into()));

    assert!(!reporter.is_enabled());
}

#[cfg(unix)]
#[test]
fn socket_reachability_connects_to_live_socket() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    let _listener = std::os::unix::net::UnixListener::bind(&socket_path).unwrap();
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    assert_eq!(reporter.socket_is_reachable(), Some(true));
}

#[cfg(unix)]
#[test]
fn socket_reachability_rejects_regular_file() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    std::fs::write(&socket_path, "not a socket").unwrap();
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    assert_eq!(reporter.socket_is_reachable(), Some(false));
}

#[cfg(unix)]
#[tokio::test]
async fn report_state_sends_herdr_json_rpc_request() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    let mut server = super::test_support::TestHerdrServer::bind(&socket_path).await;
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    reporter
        .report_state(
            HerdrState::Working,
            Some("running tools"),
            Some("session-1"),
        )
        .await;

    let request = server.next_request().await;
    assert_eq!(request["method"], "pane.report_agent");
    assert_eq!(request["params"]["pane_id"], "w1:p1");
    assert_eq!(request["params"]["source"], "herdr:rho");
    assert_eq!(request["params"]["agent"], "rho");
    assert_eq!(request["params"]["state"], "working");
    assert_eq!(request["params"]["message"], "running tools");
    assert_eq!(request["params"]["agent_session_id"], "session-1");
    assert!(request["params"].get("seq").is_none());
}

#[cfg(unix)]
#[tokio::test]
async fn report_session_sends_session_reference() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    let mut server = super::test_support::TestHerdrServer::bind(&socket_path).await;
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    reporter.report_session(Some("session-2")).await;

    let request = server.next_request().await;
    assert_eq!(request["method"], "pane.report_agent_session");
    assert_eq!(request["params"]["agent_session_id"], "session-2");
    assert!(request["params"].get("seq").is_none());
}

#[cfg(unix)]
#[tokio::test]
async fn release_sends_release_request() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    let mut server = super::test_support::TestHerdrServer::bind(&socket_path).await;
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    reporter.release().await;

    let request = server.next_request().await;
    assert_eq!(request["method"], "pane.release_agent");
    assert_eq!(request["params"]["pane_id"], "w1:p1");
    assert_eq!(request["params"]["agent"], "rho");
    assert!(request["params"].get("seq").is_none());
}

#[cfg(unix)]
#[tokio::test]
async fn open_sibling_pane_splits_and_submits_input_atomically() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    let mut server = super::test_support::TestHerdrServer::bind_with_responses(
        &socket_path,
        vec![
            br#"{"id":"1","result":{"pane":{"pane_id":"w1:p2"}}}
"#,
            br#"{"id":"2","result":{}}
"#,
        ],
    )
    .await;
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    reporter
        .open_sibling_pane("'/tmp/rho' 'attach' 'a1b2c3'")
        .await
        .unwrap();

    let split = server.next_request().await;
    assert_eq!(split["method"], "pane.split");
    assert_eq!(split["params"]["target_pane_id"], "w1:p1");
    assert_eq!(split["params"]["direction"], "right");
    assert_eq!(split["params"]["focus"], false);

    let input = server.next_request().await;
    assert_eq!(input["method"], "pane.send_input");
    assert_eq!(input["params"]["pane_id"], "w1:p2");
    assert_eq!(input["params"]["text"], "'/tmp/rho' 'attach' 'a1b2c3'");
    assert_eq!(input["params"]["keys"], serde_json::json!(["enter"]));
}

#[cfg(unix)]
#[tokio::test]
async fn open_sibling_pane_closes_split_when_input_fails() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    let mut server = super::test_support::TestHerdrServer::bind_with_responses(
        &socket_path,
        vec![
            br#"{"id":"1","result":{"pane":{"pane_id":"w1:p2"}}}
"#,
            br#"{"id":"2","error":{"code":"pane_missing","message":"pane disappeared"}}
"#,
            br#"{"id":"3","result":{}}
"#,
        ],
    )
    .await;
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    let error = reporter
        .open_sibling_pane("rho attach a1b2c3")
        .await
        .unwrap_err();

    assert_eq!(error.to_string(), "pane disappeared");
    assert_eq!(server.next_request().await["method"], "pane.split");
    assert_eq!(server.next_request().await["method"], "pane.send_input");
    let close = server.next_request().await;
    assert_eq!(close["method"], "pane.close");
    assert_eq!(close["params"]["pane_id"], "w1:p2");
}

#[cfg(unix)]
#[tokio::test]
async fn open_sibling_pane_keeps_input_and_cleanup_errors() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    let mut server = super::test_support::TestHerdrServer::bind_with_responses(
        &socket_path,
        vec![
            br#"{"id":"1","result":{"pane":{"pane_id":"w1:p2"}}}
"#,
            br#"{"id":"2","error":{"message":"input failed"}}
"#,
            br#"{"id":"3","error":{"message":"close failed"}}
"#,
        ],
    )
    .await;
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    let error = reporter
        .open_sibling_pane("rho attach a1b2c3")
        .await
        .unwrap_err();

    assert_eq!(
        error.to_string(),
        "input failed; pane cleanup failed: close failed"
    );
    assert_eq!(server.next_request().await["method"], "pane.split");
    assert_eq!(server.next_request().await["method"], "pane.send_input");
    assert_eq!(server.next_request().await["method"], "pane.close");
}

#[test]
fn graphics_info_without_error_is_paintable() {
    assert!(graphics_info_reports_paintable(
        br#"{"id":"1","result":{"type":"pane_graphics_info","cell_width_px":14}}"#
    ));
}

#[test]
fn graphics_info_cell_size_error_is_not_paintable() {
    assert!(!graphics_info_reports_paintable(
        br#"{"id":"1","error":{"code":"cell_size_unavailable","message":"host cell size is unavailable"}}"#
    ));
}

#[tokio::test]
async fn graphics_capability_is_not_herdr_when_disabled() {
    let reporter = HerdrReporter::default();
    assert_eq!(
        reporter.graphics_capability().await,
        HerdrGraphicsCapability::NotHerdr
    );
}

#[cfg(unix)]
#[tokio::test]
async fn graphics_capability_paintable_when_result_present_without_eof() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    let mut server = super::test_support::TestHerdrServer::bind_with_response(
        &socket_path,
        br#"{"id":"1","result":{"type":"pane_graphics_info","cell_width_px":14}}
"#,
    )
    .await;
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    let capability = reporter.graphics_capability().await;
    let request = server.next_request().await;

    assert_eq!(capability, HerdrGraphicsCapability::Paintable);
    assert_eq!(request["method"], "pane.graphics.info");
    assert_eq!(request["params"]["pane_id"], "w1:p1");
}

#[cfg(unix)]
#[tokio::test]
async fn graphics_capability_unpaintable_on_cell_size_error() {
    let socket_dir = tempfile::tempdir().unwrap();
    let socket_path = socket_dir.path().join("herdr.sock");
    let _server = super::test_support::TestHerdrServer::bind_with_response(
        &socket_path,
        br#"{"id":"1","error":{"code":"cell_size_unavailable","message":"host cell size is unavailable"}}
"#,
    )
    .await;
    let reporter = super::test_support::reporter_for_socket(&socket_path);

    assert_eq!(
        reporter.graphics_capability().await,
        HerdrGraphicsCapability::Unpaintable
    );
}