agecli-skill-protocol 0.2.1

Wire protocol contract for agecli skill ↔ host UDS communication (binary framing + payload types)
Documentation
use super::*;
use crate::reader::SkillMessageReader;

#[tokio::test]
async fn write_and_read_roundtrip() {
    let (client, server) = tokio::io::duplex(4096);
    let mut writer = SkillMessageWriter::new(client);
    let mut reader = SkillMessageReader::new(server);

    let payload = AckPayload {
        id: "test-1".into(),
        accepted: true,
    };
    writer.write_ack(&payload).await.unwrap();
    drop(writer); // close write side

    let msg = reader.read_message().await.unwrap().unwrap();
    assert_eq!(msg.msg_type, SkillMessageType::Ack);
    let parsed: AckPayload = msg.deserialize_payload().unwrap();
    assert_eq!(parsed.id, "test-1");
    assert!(parsed.accepted);
}

#[tokio::test]
async fn write_and_read_execute() {
    let (client, server) = tokio::io::duplex(4096);
    let mut writer = SkillMessageWriter::new(client);
    let mut reader = SkillMessageReader::new(server);

    let payload = ExecutePayload {
        execution_id: "exec-1".into(),
        command_name: "test:exec".into(),
        command: Some("ls -la".into()),
        args: None,
        working_directory: None,
        environment: None,
        timeout_ms: 30000,
    };
    writer.write_execute(&payload).await.unwrap();
    drop(writer);

    let msg = reader.read_message().await.unwrap().unwrap();
    assert_eq!(msg.msg_type, SkillMessageType::Execute);
    let parsed: ExecutePayload = msg.deserialize_payload().unwrap();
    assert_eq!(parsed.execution_id, "exec-1");
    assert_eq!(parsed.command.as_deref(), Some("ls -la"));
}

#[tokio::test]
async fn write_and_read_proxy_submit() {
    let (client, server) = tokio::io::duplex(4096);
    let mut writer = SkillMessageWriter::new(client);
    let mut reader = SkillMessageReader::new(server);

    let payload = ProxySubmitPayload {
        proxy_id: "proxy-1".into(),
        external_id: "ext-1".into(),
        command_name: "test:exec".into(),
        args: vec!["--flag".into(), "value".into()],
        target: ProxyTarget {
            kind: "local".into(),
            agent_id: None,
        },
    };
    writer.write_proxy_submit(&payload).await.unwrap();
    drop(writer);

    let msg = reader.read_message().await.unwrap().unwrap();
    assert_eq!(msg.msg_type, SkillMessageType::ProxySubmit);
    assert_eq!(msg.msg_type as u32, 200);
    let parsed: ProxySubmitPayload = msg.deserialize_payload().unwrap();
    assert_eq!(parsed.proxy_id, "proxy-1");
    assert_eq!(parsed.external_id, "ext-1");
    assert_eq!(parsed.command_name, "test:exec");
    assert_eq!(parsed.args, vec!["--flag", "value"]);
    assert_eq!(parsed.target.kind, "local");
}

#[tokio::test]
async fn write_and_read_proxy_cancel() {
    let (client, server) = tokio::io::duplex(4096);
    let mut writer = SkillMessageWriter::new(client);
    let mut reader = SkillMessageReader::new(server);

    writer.write_proxy_cancel("proxy-1").await.unwrap();
    drop(writer);

    let msg = reader.read_message().await.unwrap().unwrap();
    assert_eq!(msg.msg_type, SkillMessageType::ProxyCancel);
    assert_eq!(msg.msg_type as u32, 201);
    assert_eq!(msg.payload, b"proxy-1");
}

#[tokio::test]
async fn write_and_read_proxy_stdout_chunk() {
    let (client, server) = tokio::io::duplex(4096);
    let mut writer = SkillMessageWriter::new(client);
    let mut reader = SkillMessageReader::new(server);

    let payload = ProxyChunkPayload {
        proxy_id: "proxy-1".into(),
        seq: 1,
        data: b"out".to_vec(),
    };
    writer.write_proxy_stdout_chunk(&payload).await.unwrap();
    drop(writer);

    let msg = reader.read_message().await.unwrap().unwrap();
    assert_eq!(msg.msg_type, SkillMessageType::ProxyStdoutChunk);
    assert_eq!(msg.msg_type as u32, 202);
    let parsed: ProxyChunkPayload = msg.deserialize_payload().unwrap();
    assert_eq!(parsed.proxy_id, "proxy-1");
    assert_eq!(parsed.seq, 1);
    assert_eq!(parsed.data, b"out");
}

#[tokio::test]
async fn write_and_read_proxy_stderr_chunk() {
    let (client, server) = tokio::io::duplex(4096);
    let mut writer = SkillMessageWriter::new(client);
    let mut reader = SkillMessageReader::new(server);

    let payload = ProxyChunkPayload {
        proxy_id: "proxy-1".into(),
        seq: 2,
        data: b"err".to_vec(),
    };
    writer.write_proxy_stderr_chunk(&payload).await.unwrap();
    drop(writer);

    let msg = reader.read_message().await.unwrap().unwrap();
    assert_eq!(msg.msg_type, SkillMessageType::ProxyStderrChunk);
    assert_eq!(msg.msg_type as u32, 203);
    let parsed: ProxyChunkPayload = msg.deserialize_payload().unwrap();
    assert_eq!(parsed.proxy_id, "proxy-1");
    assert_eq!(parsed.seq, 2);
    assert_eq!(parsed.data, b"err");
}

#[tokio::test]
async fn write_and_read_proxy_completed() {
    let (client, server) = tokio::io::duplex(4096);
    let mut writer = SkillMessageWriter::new(client);
    let mut reader = SkillMessageReader::new(server);

    let payload = ProxyCompletedPayload {
        proxy_id: "proxy-1".into(),
        exit_code: 0,
        status: "success".into(),
        error: None,
        finished_at_unix: 1700000000,
    };
    writer.write_proxy_completed(&payload).await.unwrap();
    drop(writer);

    let msg = reader.read_message().await.unwrap().unwrap();
    assert_eq!(msg.msg_type, SkillMessageType::ProxyCompleted);
    assert_eq!(msg.msg_type as u32, 204);
    let parsed: ProxyCompletedPayload = msg.deserialize_payload().unwrap();
    assert_eq!(parsed.proxy_id, "proxy-1");
    assert_eq!(parsed.exit_code, 0);
    assert_eq!(parsed.status, "success");
    assert_eq!(parsed.finished_at_unix, 1700000000);
}

#[tokio::test]
async fn write_and_read_proxy_rejected() {
    let (client, server) = tokio::io::duplex(4096);
    let mut writer = SkillMessageWriter::new(client);
    let mut reader = SkillMessageReader::new(server);

    let payload = ProxyRejectedPayload {
        proxy_id: "proxy-1".into(),
        reason_code: "DENIED".into(),
        message: "not allowed".into(),
    };
    writer.write_proxy_rejected(&payload).await.unwrap();
    drop(writer);

    let msg = reader.read_message().await.unwrap().unwrap();
    assert_eq!(msg.msg_type, SkillMessageType::ProxyRejected);
    assert_eq!(msg.msg_type as u32, 205);
    let parsed: ProxyRejectedPayload = msg.deserialize_payload().unwrap();
    assert_eq!(parsed.proxy_id, "proxy-1");
    assert_eq!(parsed.reason_code, "DENIED");
    assert_eq!(parsed.message, "not allowed");
}