#![expect(
clippy::expect_used,
clippy::panic,
reason = "tests assert on fallible parsing and panic with the failing fixture; propagating would replace a clear failure with noise"
)]
use protocol_droid_schema::wire::{
draft, v2024_11_05, v2025_03_26, v2025_06_18, v2025_11_25, v2026_07_28,
};
use serde_json::{Value, json};
fn block(kind: &str) -> Value {
match kind {
"text" => json!({"type": "text", "text": "hello"}),
"image" => json!({"type": "image", "data": "AAA=", "mimeType": "image/png"}),
"audio" => json!({"type": "audio", "data": "AAA=", "mimeType": "audio/wav"}),
"link" => json!({
"type": "resource_link",
"uri": "file:///a",
"name": "a",
}),
"resource" => json!({
"type": "resource",
"resource": {"uri": "file:///a", "text": "body"},
}),
other => panic!("no fixture for {other}"),
}
}
fn round_trip<T>(value: &Value) -> String
where
T: serde::de::DeserializeOwned + serde::Serialize,
{
let parsed: T = serde_json::from_value(value.clone()).expect("a known-good block");
serde_json::to_string(&parsed).expect("the generated types serialise")
}
fn assert_arm_is_stable<T>(kind: &str)
where
T: serde::de::DeserializeOwned + serde::Serialize,
{
let before = block(kind);
let after: Value = serde_json::from_str(&round_trip::<T>(&before)).expect("re-parses");
assert_eq!(
after, before,
"a {kind} block came back as a different arm: it landed somewhere that \
serialises to a different shape, which is how an untagged union reports \
a wrong member order"
);
}
#[test]
fn the_2024_11_05_union_matches_its_content_kinds() {
assert_arm_is_stable::<v2024_11_05::CallToolResultContentChoice>("text");
assert_arm_is_stable::<v2024_11_05::CallToolResultContentChoice>("image");
assert_arm_is_stable::<v2024_11_05::CallToolResultContentChoice>("resource");
}
#[test]
fn the_2025_03_26_union_matches_its_content_kinds() {
assert_arm_is_stable::<v2025_03_26::CallToolResultContentChoice>("text");
assert_arm_is_stable::<v2025_03_26::CallToolResultContentChoice>("image");
assert_arm_is_stable::<v2025_03_26::CallToolResultContentChoice>("audio");
assert_arm_is_stable::<v2025_03_26::CallToolResultContentChoice>("resource");
}
#[test]
fn the_2025_06_18_union_matches_its_content_kinds() {
assert_arm_is_stable::<v2025_06_18::ContentBlock>("text");
assert_arm_is_stable::<v2025_06_18::ContentBlock>("image");
assert_arm_is_stable::<v2025_06_18::ContentBlock>("audio");
assert_arm_is_stable::<v2025_06_18::ContentBlock>("link");
assert_arm_is_stable::<v2025_06_18::ContentBlock>("resource");
}
#[test]
fn the_2025_11_25_union_matches_its_content_kinds() {
assert_arm_is_stable::<v2025_11_25::ContentBlock>("text");
assert_arm_is_stable::<v2025_11_25::ContentBlock>("image");
assert_arm_is_stable::<v2025_11_25::ContentBlock>("audio");
assert_arm_is_stable::<v2025_11_25::ContentBlock>("link");
assert_arm_is_stable::<v2025_11_25::ContentBlock>("resource");
}
#[test]
fn the_2026_07_28_union_matches_its_content_kinds() {
assert_arm_is_stable::<v2026_07_28::ContentBlock>("text");
assert_arm_is_stable::<v2026_07_28::ContentBlock>("image");
assert_arm_is_stable::<v2026_07_28::ContentBlock>("audio");
assert_arm_is_stable::<v2026_07_28::ContentBlock>("link");
assert_arm_is_stable::<v2026_07_28::ContentBlock>("resource");
}
#[test]
fn the_draft_union_matches_its_content_kinds() {
assert_arm_is_stable::<draft::ContentBlock>("text");
assert_arm_is_stable::<draft::ContentBlock>("image");
assert_arm_is_stable::<draft::ContentBlock>("audio");
assert_arm_is_stable::<draft::ContentBlock>("link");
assert_arm_is_stable::<draft::ContentBlock>("resource");
}
#[test]
fn an_audio_block_is_not_accepted_as_an_image() {
let audio = block("audio");
let as_image: Result<v2025_06_18::ContentBlock, _> = serde_json::from_value(audio.clone());
let image = block("image");
let audio_as_image: Result<v2025_06_18::ContentBlock, _> =
serde_json::from_value(image.clone());
let audio_back: Value =
serde_json::from_str(&round_trip::<v2025_06_18::ContentBlock>(&audio)).expect("parses");
let image_back: Value =
serde_json::from_str(&round_trip::<v2025_06_18::ContentBlock>(&image)).expect("parses");
assert_eq!(audio_back, audio);
assert_eq!(image_back, image);
assert_ne!(audio_back, image_back);
assert!(as_image.is_ok() || audio_as_image.is_ok());
}
#[test]
#[ignore = "codegen emits `type` as String, so an audio block lands in the image arm"]
fn arm_is_claimed_by_const() {
let audio: v2025_03_26::CallToolResultContentChoice =
serde_json::from_value(block("audio")).expect("parses");
assert!(
matches!(audio, v2025_03_26::CallToolResultContentChoice::Member2(_)),
"an audio block landed in the image arm. The union is untagged and \
`r#type` is a plain `String`, so `ImageContent` accepts \
{{\"type\":\"audio\"}} and wins on member order. The audio is being \
served back as an image with no error at any layer."
);
}
#[test]
fn arms_that_differ_in_shape_do_discriminate() {
let link: v2025_06_18::ContentBlock = serde_json::from_value(block("link")).expect("parses");
assert!(matches!(link, v2025_06_18::ContentBlock::Member3(_)));
let resource: v2025_06_18::ContentBlock =
serde_json::from_value(block("resource")).expect("parses");
assert!(matches!(resource, v2025_06_18::ContentBlock::Member4(_)));
}