tea-protocol 0.1.0

Canonical protocol types for the tea-rs project
Documentation
use std::any::TypeId;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;

use serde_json::{Value, json};
use tea_protocol::{
    ApprovalId, CURRENT_PROTOCOL_VERSION, CommandEnvelope, CommandId, CorrelationId, EventEnvelope,
    EventId, MessageId, PROTOCOL_V1_0, ProtocolErrorEnvelope, ProtocolTimestamp, ProtocolVersion,
    RecordEnvelope, RecordId, RunId, SessionId, SessionSequence, ToolCallId, TurnId,
};

const SESSION_ID: &str = "0195a0b1-5e3a-7d72-a902-c4e85d828bf1";
const SECOND_ID: &str = "0195a0b1-5e3b-7ef0-8ec1-0aa7aa000001";

fn fixture(name: &str) -> Value {
    fixture_for("v1.0", name)
}

fn fixture_for(version: &str, name: &str) -> Value {
    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(version)
        .join(name);
    serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap()
}

#[test]
fn protocol_version_is_a_canonical_string() {
    assert_eq!(PROTOCOL_V1_0.to_string(), "1.0");
    assert_eq!(CURRENT_PROTOCOL_VERSION, PROTOCOL_V1_0);
    assert_eq!(ProtocolVersion::from_str("1.0").unwrap(), PROTOCOL_V1_0);
    assert_eq!(serde_json::to_string(&PROTOCOL_V1_0).unwrap(), r#""1.0""#);
    assert_eq!(
        serde_json::from_str::<ProtocolVersion>(r#""1.0""#).unwrap(),
        PROTOCOL_V1_0
    );
}

#[test]
fn strong_ids_are_distinct_and_use_canonical_uuid_v7_strings() {
    assert_ne!(TypeId::of::<SessionId>(), TypeId::of::<RunId>());
    assert_ne!(TypeId::of::<MessageId>(), TypeId::of::<ToolCallId>());

    macro_rules! check_id {
        ($id:ty, $value:expr) => {{
            let parsed = <$id>::from_str($value).unwrap();
            assert_eq!(parsed.to_string(), $value);
            assert_eq!(
                serde_json::to_string(&parsed).unwrap(),
                format!(r#""{}""#, $value)
            );
            assert_eq!(
                serde_json::from_str::<$id>(&format!(r#""{}""#, $value)).unwrap(),
                parsed
            );
        }};
    }

    check_id!(SessionId, SESSION_ID);
    check_id!(RunId, SECOND_ID);
    check_id!(TurnId, SESSION_ID);
    check_id!(MessageId, SECOND_ID);
    check_id!(ToolCallId, SESSION_ID);
    check_id!(ApprovalId, SECOND_ID);
    check_id!(EventId, SESSION_ID);
    check_id!(CommandId, SECOND_ID);
    check_id!(RecordId, SESSION_ID);
}

#[test]
fn session_sequence_serializes_as_a_decimal_string() {
    let sequence = SessionSequence::new(42);
    assert_eq!(sequence.get(), 42);
    assert_eq!(sequence.checked_next().unwrap().get(), 43);
    assert_eq!(sequence.to_string(), "42");
    assert_eq!(serde_json::to_string(&sequence).unwrap(), r#""42""#);
    assert_eq!(
        serde_json::from_str::<SessionSequence>(r#""42""#).unwrap(),
        sequence
    );
}

#[test]
fn additive_unknown_fields_are_ignored_on_known_types() {
    let mut command = fixture("command-prompt.json");
    command["futureEnvelopeField"] = json!(true);
    command["payload"]["futurePayloadField"] = json!({"value": 1});
    assert!(serde_json::from_value::<CommandEnvelope>(command).is_ok());

    let mut event = fixture("event-run-finished.json");
    event["futureEnvelopeField"] = json!(true);
    event["payload"]["futurePayloadField"] = json!([1, 2]);
    assert!(serde_json::from_value::<EventEnvelope>(event).is_ok());

    let mut record = fixture("record-message-committed.json");
    record["futureEnvelopeField"] = json!(true);
    record["payload"]["futurePayloadField"] = json!("future");
    assert!(serde_json::from_value::<RecordEnvelope>(record).is_ok());
}

#[test]
fn same_major_minor_fixtures_decode_known_envelopes() {
    let command: CommandEnvelope =
        serde_json::from_value(fixture_for("v1.1", "command-prompt.json")).unwrap();
    let event: EventEnvelope =
        serde_json::from_value(fixture_for("v1.1", "event-run-finished.json")).unwrap();
    let record: RecordEnvelope =
        serde_json::from_value(fixture_for("v1.1", "record-message-committed.json")).unwrap();
    let error: ProtocolErrorEnvelope =
        serde_json::from_value(fixture_for("v1.1", "error-unsupported-command.json")).unwrap();

    for version in [
        command.protocol_version(),
        event.protocol_version(),
        record.protocol_version(),
        error.protocol_version(),
    ] {
        assert_eq!(version, ProtocolVersion::new(1, 1));
    }
}

#[test]
fn namespaced_metadata_survives_known_envelope_round_trip() {
    let mut event = fixture("event-run-finished.json");
    event["metadata"] = json!({"com.example": {"trace": "safe"}});
    let typed: EventEnvelope = serde_json::from_value(event.clone()).unwrap();
    assert_eq!(serde_json::to_value(typed).unwrap(), event);
}

#[test]
fn correlation_id_is_a_distinct_canonical_id() {
    assert_ne!(TypeId::of::<CorrelationId>(), TypeId::of::<CommandId>());
    let parsed = CorrelationId::from_str(SECOND_ID).unwrap();
    assert_eq!(parsed.to_string(), SECOND_ID);
}

#[test]
fn timestamp_normalizes_to_utc_millisecond_rfc3339() {
    let canonical = ProtocolTimestamp::from_str("2026-07-23T09:30:12.123Z").unwrap();
    assert_eq!(canonical.to_string(), "2026-07-23T09:30:12.123Z");
    assert_eq!(
        serde_json::to_string(&canonical).unwrap(),
        r#""2026-07-23T09:30:12.123Z""#
    );

    let offset = ProtocolTimestamp::from_str("2026-07-23T10:30:12.123+01:00").unwrap();
    assert_eq!(offset, canonical);
    assert_eq!(offset.to_string(), "2026-07-23T09:30:12.123Z");
}