agecli-skill-protocol 0.2.1

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

#[test]
fn message_type_roundtrip() {
    assert_eq!(
        SkillMessageType::from_u32(1),
        Some(SkillMessageType::Execute)
    );
    assert_eq!(SkillMessageType::from_u32(128), Some(SkillMessageType::Ack));
    assert_eq!(SkillMessageType::from_u32(999), None);
}

#[test]
fn execute_payload_serde() {
    let payload = ExecutePayload {
        execution_id: "exec-1".into(),
        command_name: "test:exec".into(),
        command: Some("echo hello".into()),
        args: None,
        working_directory: None,
        environment: None,
        timeout_ms: 30000,
    };
    let json = serde_json::to_string(&payload).unwrap();
    let parsed: ExecutePayload = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.execution_id, "exec-1");
    assert_eq!(parsed.command_name, "test:exec");
    assert_eq!(parsed.command.as_deref(), Some("echo hello"));
}

#[test]
fn completed_payload_serde() {
    let payload = CompletedPayload {
        id: "exec-1".into(),
        exit_code: 0,
        status: "success".into(),
        error: None,
        finished_at_unix: 1700000000,
    };
    let json = serde_json::to_string(&payload).unwrap();
    let parsed: CompletedPayload = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.exit_code, 0);
    assert_eq!(parsed.status, "success");
}

#[test]
fn error_payload_serde() {
    let payload = ErrorPayload {
        id: Some("exec-err".into()),
        code: "SKILL_EXEC_FAILED".into(),
        message: "process killed".into(),
    };
    let json = serde_json::to_string(&payload).unwrap();
    let parsed: ErrorPayload = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.code, "SKILL_EXEC_FAILED");
    assert_eq!(parsed.id.as_deref(), Some("exec-err"));
}

#[test]
fn session_started_payload_serde() {
    let payload = SessionStartedPayload {
        id: "sess-1".into(),
        status: "active".into(),
    };
    let json = serde_json::to_string(&payload).unwrap();
    let parsed: SessionStartedPayload = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.id, "sess-1");
    assert_eq!(parsed.status, "active");
}

#[test]
fn stdout_payload_serde_with_bytes() {
    let payload = DataChunkPayload {
        id: "exec-1".into(),
        seq: 1,
        data: b"hello world".to_vec(),
    };
    let json = serde_json::to_string(&payload).unwrap();
    let parsed: DataChunkPayload = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.data, b"hello world");
    assert_eq!(parsed.seq, 1);
}

#[test]
fn all_message_types_have_values() {
    // Server → Skill
    assert_eq!(
        SkillMessageType::from_u32(1),
        Some(SkillMessageType::Execute)
    );
    assert_eq!(
        SkillMessageType::from_u32(2),
        Some(SkillMessageType::Cancel)
    );
    assert_eq!(
        SkillMessageType::from_u32(3),
        Some(SkillMessageType::StdinData)
    );
    assert_eq!(
        SkillMessageType::from_u32(4),
        Some(SkillMessageType::Resize)
    );
    assert_eq!(
        SkillMessageType::from_u32(5),
        Some(SkillMessageType::Signal)
    );
    assert_eq!(
        SkillMessageType::from_u32(6),
        Some(SkillMessageType::StartSession)
    );
    assert_eq!(
        SkillMessageType::from_u32(7),
        Some(SkillMessageType::Shutdown)
    );
    // Skill → Server
    assert_eq!(SkillMessageType::from_u32(128), Some(SkillMessageType::Ack));
    assert_eq!(
        SkillMessageType::from_u32(129),
        Some(SkillMessageType::StdoutChunk)
    );
    assert_eq!(
        SkillMessageType::from_u32(130),
        Some(SkillMessageType::StderrChunk)
    );
    assert_eq!(
        SkillMessageType::from_u32(131),
        Some(SkillMessageType::Progress)
    );
    assert_eq!(
        SkillMessageType::from_u32(132),
        Some(SkillMessageType::Completed)
    );
    assert_eq!(
        SkillMessageType::from_u32(133),
        Some(SkillMessageType::Error)
    );
    assert_eq!(
        SkillMessageType::from_u32(134),
        Some(SkillMessageType::SessionStarted)
    );
    // Proxy
    assert_eq!(
        SkillMessageType::from_u32(200),
        Some(SkillMessageType::ProxySubmit)
    );
    assert_eq!(
        SkillMessageType::from_u32(201),
        Some(SkillMessageType::ProxyCancel)
    );
    assert_eq!(
        SkillMessageType::from_u32(202),
        Some(SkillMessageType::ProxyStdoutChunk)
    );
    assert_eq!(
        SkillMessageType::from_u32(203),
        Some(SkillMessageType::ProxyStderrChunk)
    );
    assert_eq!(
        SkillMessageType::from_u32(204),
        Some(SkillMessageType::ProxyCompleted)
    );
    assert_eq!(
        SkillMessageType::from_u32(205),
        Some(SkillMessageType::ProxyRejected)
    );
    // Unknown
    assert_eq!(SkillMessageType::from_u32(999), None);
}

#[test]
fn proxy_message_types_map_correctly() {
    assert_eq!(
        SkillMessageType::from_u32(200),
        Some(SkillMessageType::ProxySubmit)
    );
    assert_eq!(
        SkillMessageType::from_u32(201),
        Some(SkillMessageType::ProxyCancel)
    );
    assert_eq!(
        SkillMessageType::from_u32(202),
        Some(SkillMessageType::ProxyStdoutChunk)
    );
    assert_eq!(
        SkillMessageType::from_u32(203),
        Some(SkillMessageType::ProxyStderrChunk)
    );
    assert_eq!(
        SkillMessageType::from_u32(204),
        Some(SkillMessageType::ProxyCompleted)
    );
    assert_eq!(
        SkillMessageType::from_u32(205),
        Some(SkillMessageType::ProxyRejected)
    );
}

#[test]
fn proxy_message_type_boundaries_are_none() {
    assert_eq!(SkillMessageType::from_u32(199), None);
    assert_eq!(SkillMessageType::from_u32(206), None);
}

#[test]
fn proxy_submit_payload_serde_pascal_case() {
    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,
        },
    };
    let json = serde_json::to_string(&payload).unwrap();
    assert!(json.contains("\"ProxyId\""), "json: {json}");
    assert!(json.contains("\"ExternalId\""), "json: {json}");
    assert!(json.contains("\"CommandName\""), "json: {json}");
    assert!(json.contains("\"Args\""), "json: {json}");
    assert!(json.contains("\"Target\""), "json: {json}");

    let parsed: ProxySubmitPayload = serde_json::from_str(&json).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");
    assert_eq!(parsed.target.agent_id, None);
}

#[test]
fn proxy_target_local_omits_agent_id() {
    let target = ProxyTarget {
        kind: "local".into(),
        agent_id: None,
    };
    let json = serde_json::to_string(&target).unwrap();
    assert!(json.contains("\"Kind\""), "json: {json}");
    assert!(!json.contains("AgentId"), "json: {json}");

    let parsed: ProxyTarget = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.kind, "local");
    assert_eq!(parsed.agent_id, None);
}

#[test]
fn proxy_target_agent_includes_agent_id() {
    let target = ProxyTarget {
        kind: "agent".into(),
        agent_id: Some("agent-42".into()),
    };
    let json = serde_json::to_string(&target).unwrap();
    assert!(json.contains("\"Kind\""), "json: {json}");
    assert!(json.contains("\"AgentId\""), "json: {json}");

    let parsed: ProxyTarget = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.kind, "agent");
    assert_eq!(parsed.agent_id.as_deref(), Some("agent-42"));
}

#[test]
fn proxy_rejected_payload_serde() {
    let payload = ProxyRejectedPayload {
        proxy_id: "proxy-1".into(),
        reason_code: "DENIED".into(),
        message: "not allowed".into(),
    };
    let json = serde_json::to_string(&payload).unwrap();
    assert!(json.contains("\"ProxyId\""), "json: {json}");
    assert!(json.contains("\"ReasonCode\""), "json: {json}");
    assert!(json.contains("\"Message\""), "json: {json}");

    let parsed: ProxyRejectedPayload = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.proxy_id, "proxy-1");
    assert_eq!(parsed.reason_code, "DENIED");
    assert_eq!(parsed.message, "not allowed");
}

#[test]
fn proxy_chunk_payload_serde_pascal_case_and_base64() {
    let payload = ProxyChunkPayload {
        proxy_id: "proxy-1".into(),
        seq: 7,
        data: b"hello world".to_vec(),
    };
    let json = serde_json::to_string(&payload).unwrap();
    assert!(json.contains("\"ProxyId\""), "json: {json}");
    assert!(json.contains("\"Seq\""), "json: {json}");
    assert!(json.contains("\"Data\""), "json: {json}");
    // base64 of "hello world"
    assert!(json.contains("\"aGVsbG8gd29ybGQ=\""), "json: {json}");

    let parsed: ProxyChunkPayload = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.proxy_id, "proxy-1");
    assert_eq!(parsed.seq, 7);
    assert_eq!(parsed.data, b"hello world");
}

#[test]
fn proxy_completed_payload_serde_pascal_case() {
    let payload = ProxyCompletedPayload {
        proxy_id: "proxy-1".into(),
        exit_code: 0,
        status: "success".into(),
        error: None,
        finished_at_unix: 1700000000,
    };
    let json = serde_json::to_string(&payload).unwrap();
    assert!(json.contains("\"ProxyId\""), "json: {json}");
    assert!(json.contains("\"ExitCode\""), "json: {json}");
    assert!(json.contains("\"Status\""), "json: {json}");
    assert!(json.contains("\"FinishedAtUnix\""), "json: {json}");
    // error: None must be omitted
    assert!(!json.contains("Error"), "json: {json}");

    let parsed: ProxyCompletedPayload = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.proxy_id, "proxy-1");
    assert_eq!(parsed.exit_code, 0);
    assert_eq!(parsed.status, "success");
    assert_eq!(parsed.error, None);
    assert_eq!(parsed.finished_at_unix, 1700000000);
}

#[test]
fn proxy_completed_payload_includes_error_when_some() {
    let payload = ProxyCompletedPayload {
        proxy_id: "proxy-1".into(),
        exit_code: 1,
        status: "failed".into(),
        error: Some("boom".into()),
        finished_at_unix: 1700000001,
    };
    let json = serde_json::to_string(&payload).unwrap();
    assert!(json.contains("\"Error\""), "json: {json}");

    let parsed: ProxyCompletedPayload = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed.error.as_deref(), Some("boom"));
}