tea-protocol 0.1.0

Canonical protocol types for the tea-rs project
Documentation
use std::str::FromStr;

use tea_protocol::{
    CommandEnvelope, EventEnvelope, ProtocolErrorEnvelope, ProtocolMetadata, ProtocolTimestamp,
    ProtocolVersion, RecordEnvelope, SessionId, SessionSequence,
};

#[test]
fn invalid_protocol_versions_are_rejected() {
    for value in ["", "1", "1.0.0", "01.0", "1.00", "+1.0", " 1.0"] {
        assert!(
            ProtocolVersion::from_str(value).is_err(),
            "accepted {value:?}"
        );
    }
}

#[test]
fn non_canonical_or_non_v7_ids_are_rejected() {
    for value in [
        "",
        "550e8400-e29b-41d4-a716-446655440000",
        "0195A0B1-5E3A-7D72-A902-C4E85D828BF1",
        "0195a0b15e3a7d72a902c4e85d828bf1",
    ] {
        assert!(SessionId::from_str(value).is_err(), "accepted {value:?}");
    }
}

#[test]
fn invalid_sequences_are_rejected() {
    for value in ["", "00", "01", "+1", "-1", " 1", "1 ", "1.0", "1e3"] {
        assert!(
            SessionSequence::from_str(value).is_err(),
            "accepted {value:?}"
        );
    }
    assert!(SessionSequence::new(u64::MAX).checked_next().is_none());
    assert!(serde_json::from_str::<SessionSequence>("42").is_err());
}

#[test]
fn duplicate_fields_are_rejected_recursively_at_envelope_boundaries() {
    let duplicate_command_id = r#"{
        "protocolVersion":"1.0",
        "type":"abort",
        "commandId":"0195a0b1-5e5e-741f-b474-0aa7aa000036",
        "commandId":"0195a0b1-5e5b-739d-bf5c-0aa7aa000033",
        "sessionId":"0195a0b1-5e3a-7d72-a902-c4e85d828bf1",
        "timestamp":"2026-07-23T09:30:15.200Z",
        "payload":{}
    }"#;
    assert!(serde_json::from_str::<CommandEnvelope>(duplicate_command_id).is_err());

    let duplicate_payload = r#"{
        "protocolVersion":"1.0",
        "type":"run_finished",
        "eventId":"0195a0b1-5e49-7ec5-8d81-0aa7aa000015",
        "sessionId":"0195a0b1-5e3a-7d72-a902-c4e85d828bf1",
        "runId":"0195a0b1-5e40-7136-8ae0-0aa7aa000006",
        "sequence":"6",
        "timestamp":"2026-07-23T09:30:15.000Z",
        "payload":{"status":"completed","status":"failed"}
    }"#;
    assert!(serde_json::from_str::<EventEnvelope>(duplicate_payload).is_err());

    let duplicate_record_type = r#"{
        "protocolVersion":"1.0",
        "type":"run_cancelled",
        "type":"run_interrupted",
        "recordId":"0195a0b1-5e57-78ff-80e1-0aa7aa000029",
        "sessionId":"0195a0b1-5e3a-7d72-a902-c4e85d828bf1",
        "sequence":"7",
        "timestamp":"2026-07-23T09:30:14.130Z",
        "payload":{"runId":"0195a0b1-5e40-7136-8ae0-0aa7aa000006"}
    }"#;
    assert!(serde_json::from_str::<RecordEnvelope>(duplicate_record_type).is_err());

    let duplicate_error_code = r#"{
        "protocolVersion":"1.0",
        "type":"protocol_error",
        "error":{"code":"internal","code":"invalid_input","message":"bad","retry":"never"}
    }"#;
    assert!(serde_json::from_str::<ProtocolErrorEnvelope>(duplicate_error_code).is_err());
}

#[test]
fn duplicate_metadata_namespaces_are_rejected_directly() {
    let duplicate = r#"{"com.example":{"value":1},"com.example":{"value":2}}"#;
    assert!(serde_json::from_str::<ProtocolMetadata>(duplicate).is_err());
}

#[test]
fn incompatible_protocol_majors_are_rejected_by_all_envelopes() {
    let cases = [
        r#"{"protocolVersion":"2.0","type":"abort","commandId":"0195a0b1-5e5e-741f-b474-0aa7aa000036","sessionId":"0195a0b1-5e3a-7d72-a902-c4e85d828bf1","timestamp":"2026-07-23T09:30:15.200Z","payload":{}}"#,
        r#"{"protocolVersion":"2.0","type":"run_started","eventId":"0195a0b1-5e3f-742a-9891-0aa7aa000005","sessionId":"0195a0b1-5e3a-7d72-a902-c4e85d828bf1","runId":"0195a0b1-5e40-7136-8ae0-0aa7aa000006","sequence":"1","timestamp":"2026-07-23T09:30:12.125Z","payload":{}}"#,
        r#"{"protocolVersion":"2.0","type":"run_cancelled","recordId":"0195a0b1-5e57-78ff-80e1-0aa7aa000029","sessionId":"0195a0b1-5e3a-7d72-a902-c4e85d828bf1","sequence":"7","timestamp":"2026-07-23T09:30:14.130Z","payload":{"runId":"0195a0b1-5e40-7136-8ae0-0aa7aa000006"}}"#,
    ];
    assert!(serde_json::from_str::<CommandEnvelope>(cases[0]).is_err());
    assert!(serde_json::from_str::<EventEnvelope>(cases[1]).is_err());
    assert!(serde_json::from_str::<RecordEnvelope>(cases[2]).is_err());
}

#[test]
fn invalid_or_lossy_timestamps_are_rejected() {
    for value in [
        "",
        "2026-07-23 09:30:12.123Z",
        "2026-07-23T09:30:12.123",
        "2026-07-23T09:30:12.123456Z",
    ] {
        assert!(
            ProtocolTimestamp::from_str(value).is_err(),
            "accepted {value:?}"
        );
    }
}