#![expect(
clippy::expect_used,
clippy::panic,
clippy::unwrap_used,
reason = "property tests assert on fallible calls; propagating would replace a shrunk counterexample with a generic error"
)]
use proptest::prelude::*;
use protocol_droid::domain::ir::lower::{
DegradeRule, Loss, LowerError, check_expressible, lower_tool_result, rule_for,
};
use protocol_droid::domain::ir::{AudioContent, Content, FeatureSet, TextContent, ToolResult};
use protocol_droid::domain::{
CapabilitySet, DegradePolicy, Feature, FeatureProfile, Negotiation, ProtocolVersion,
TransportKind, resolve,
};
fn any_session() -> impl Strategy<Value = ProtocolVersion> {
prop_oneof![
Just(ProtocolVersion::V2024_11_05),
Just(ProtocolVersion::V2025_03_26),
Just(ProtocolVersion::V2025_06_18),
Just(ProtocolVersion::V2025_11_25),
Just(ProtocolVersion::V2026_07_28),
]
}
fn session(version: ProtocolVersion) -> FeatureProfile {
let caps = CapabilitySet::all_for(version);
resolve(Negotiation {
version,
client_caps: caps,
server_caps: caps,
transport: TransportKind::Stdio,
})
}
fn features_of(profile: &FeatureProfile) -> FeatureSet {
FeatureSet::union_of(profile.iter())
}
fn text() -> impl Strategy<Value = String> {
"[a-zA-Z0-9 _\\-\\.]{0,24}"
}
fn json_key() -> impl Strategy<Value = String> {
"[a-z]{1,8}"
}
fn any_text_result() -> impl Strategy<Value = ToolResult> {
(text(), prop::option::of(json_key()), prop::bool::ANY).prop_map(|(body, key, with_json)| {
let plain = ToolResult::text(body);
match key {
Some(key) if with_json => plain
.clone()
.with_structured(format!("{{\"{key}\":1}}"))
.unwrap_or(plain),
_ => plain,
}
})
}
fn audio_result() -> impl Strategy<Value = ToolResult> {
text().prop_map(|body| {
ToolResult::new(vec![
Content::Text(TextContent::new(body)),
Content::Audio(AudioContent::new("AAA=", "audio/wav")),
])
})
}
proptest! {
#[test]
fn lowering_is_exact_under_a_strict_policy(
version in any_session(),
result in any_text_result(),
) {
let profile = session(version);
let available = features_of(&profile);
let required = result.required_features();
let fits = required.is_subset_of(available);
let gated = check_expressible(&required, &profile, version);
let lowered =
lower_tool_result(result.clone(), &profile, version, DegradePolicy::strict());
prop_assert_eq!(gated.is_ok(), fits, "gate disagreed at {}", version);
prop_assert_eq!(lowered.is_ok(), fits, "lowering disagreed at {}", version);
match lowered {
Ok((value, report)) => {
prop_assert_eq!(value, result, "{}", version);
prop_assert!(report.is_empty(), "{} reported a change", version);
prop_assert!(report.is_lossless(), "{}", version);
}
Err(LowerError::FeatureUnavailable { negotiated, .. }) => {
prop_assert_eq!(negotiated, version);
}
Err(other) => prop_assert!(false, "unexpected under strict: {other}"),
}
}
#[test]
fn a_value_that_fits_survives_lowering_unchanged(
from in any_session(),
to in any_session(),
result in any_text_result(),
) {
let required = result.required_features();
prop_assume!(required.is_subset_of(features_of(&session(from))));
prop_assume!(required.is_subset_of(features_of(&session(to))));
let (lowered, report) =
lower_tool_result(result.clone(), &session(to), to, DegradePolicy::strict())
.expect("the value fits the target session");
prop_assert_eq!(lowered, result);
prop_assert!(report.is_empty());
}
#[test]
fn the_structured_content_fallback_is_lossless(
version in any_session(),
body in text(),
key in json_key(),
) {
let profile = session(version);
let available = features_of(&profile);
let source = ToolResult::text(body)
.with_structured(format!("{{\"{key}\":1}}"))
.expect("hand-built object");
let json = source
.structured_content
.as_ref()
.map(|json| json.as_str().to_owned())
.expect("just set");
if source.required_features().is_subset_of(available) {
let (result, report) =
lower_tool_result(source.clone(), &profile, version, DegradePolicy::strict())
.expect("fits");
prop_assert_eq!(result, source);
prop_assert!(report.is_empty());
return Ok(());
}
let policy = DegradePolicy::strict().with(Feature::ToolStructuredOutput);
let (result, report) = lower_tool_result(source, &profile, version, policy)
.expect("the policy authorises the fallback");
prop_assert!(result.structured_content.is_none());
let Some(Content::Text(appended)) = result.content.last() else {
return Err(TestCaseError::fail("no text block appended"));
};
prop_assert_eq!(&appended.text, &json);
prop_assert!(result.is_lossless_anywhere());
prop_assert_eq!(report.len(), 1);
let Some((step, _)) = report.steps().split_first() else {
return Err(TestCaseError::fail("expected one step"));
};
prop_assert_eq!(step.rule, DegradeRule::StructuredContentToText, "{}", version);
prop_assert_eq!(report.worst_loss(), Loss::None);
}
#[test]
fn no_rule_fires_without_a_policy_naming_its_feature(
version in any_session(),
result in audio_result(),
) {
let profile = session(version);
let available = features_of(&profile);
let strict = lower_tool_result(result.clone(), &profile, version, DegradePolicy::strict());
let permissive =
lower_tool_result(result, &profile, version, DegradePolicy::permissive());
match (strict, permissive) {
(Ok((value, report)), Ok((other, _))) => {
prop_assert_eq!(value, other);
prop_assert!(report.is_empty());
}
(Err(LowerError::FeatureUnavailable { .. }), Ok((value, report))) => {
prop_assert!(!report.is_empty());
prop_assert!(!report.is_lossless_or_flagged());
prop_assert!(
value.required_features().is_subset_of(available),
"permissive lowering left a feature the target lacks",
);
}
(strict, permissive) => prop_assert!(
false,
"unexpected pairing: strict={strict:?} permissive={permissive:?}",
),
}
}
#[test]
fn every_selectable_rule_addresses_the_missing_feature(index in 0_usize..64) {
let all = Feature::ALL;
if all.is_empty() {
return Err(TestCaseError::reject("no features"));
}
let Some(feature) = all.get(index % all.len()).copied() else {
return Err(TestCaseError::reject("index out of range"));
};
let Some(rule) = rule_for(feature, DegradePolicy::permissive()) else {
return Ok(());
};
prop_assert!(
rule.is_fallback_for(feature),
"{rule:?} was selected for {feature} but does not address it",
);
prop_assert!(
DegradeRule::ALL.contains(&rule),
"{rule:?} was selected but is not in ALL",
);
}
#[test]
fn the_report_never_understates_what_was_lost(
version in any_session(),
result in audio_result(),
) {
let profile = session(version);
let Ok((_value, report)) =
lower_tool_result(result, &profile, version, DegradePolicy::permissive())
else {
return Ok(());
};
for step in report.steps() {
prop_assert_eq!(step.loss(), step.rule.loss(), "{:?}", step);
}
if report.is_lossless() {
prop_assert!(report.steps().iter().all(|s| s.loss().is_lossless()));
}
if report.is_empty() {
prop_assert!(report.is_lossless());
} else {
prop_assert_eq!(report.len(), report.steps().len(), "{}", version);
}
}
}
#[test]
fn text_is_expressible_at_every_revision() {
for version in ProtocolVersion::KNOWN {
let profile = session(version);
let result = ToolResult::text("hello");
let (lowered, report) =
lower_tool_result(result.clone(), &profile, version, DegradePolicy::strict())
.unwrap_or_else(|error| panic!("{version}: {error}"));
assert_eq!(lowered, result, "{version}");
assert!(report.is_empty(), "{version}");
}
}
#[test]
fn the_cross_revision_matrix_is_stable() {
let rows: Vec<(ProtocolVersion, bool, bool, bool)> = ProtocolVersion::KNOWN
.into_iter()
.map(|version| {
let available = features_of(&session(version));
(
version,
available.contains(Feature::ToolStructuredOutput),
available.contains(Feature::ToolAudioContent),
available.contains(Feature::ToolResourceLinks),
)
})
.collect();
let expected = [
(ProtocolVersion::V2024_11_05, false, false, false),
(ProtocolVersion::V2025_03_26, false, true, false),
(ProtocolVersion::V2025_06_18, true, true, true),
(ProtocolVersion::V2025_11_25, true, true, true),
(ProtocolVersion::V2026_07_28, true, true, true),
];
assert_eq!(rows.len(), expected.len());
for ((version, structured, audio, links), (want, want_struct, want_audio, want_links)) in
rows.iter().zip(expected)
{
assert_eq!(*version, want);
assert_eq!(*structured, want_struct, "{version} structuredContent");
assert_eq!(*audio, want_audio, "{version} audio");
assert_eq!(*links, want_links, "{version} resource links");
}
}
#[test]
fn report_predicates_agree_with_their_steps() {
for version in ProtocolVersion::KNOWN {
let profile = session(version);
for result in [
ToolResult::text("x"),
ToolResult::new(vec![Content::Audio(AudioContent::new("AAA=", "audio/wav"))]),
ToolResult::text("x")
.with_structured(r#"{"a":1}"#)
.expect("hand-built"),
] {
let Ok((_value, report)) =
lower_tool_result(result, &profile, version, DegradePolicy::permissive())
else {
continue;
};
if report.is_empty() {
assert_eq!(report.len(), 0, "{version}");
assert!(report.is_lossless(), "{version}");
} else {
assert_eq!(report.len(), report.steps().len(), "{version}");
}
}
}
}
#[test]
fn the_missing_feature_is_deterministic() {
for version in ProtocolVersion::KNOWN {
let profile = session(version);
let available = features_of(&profile);
let missing = FeatureSet::union_of(
Feature::ALL
.iter()
.copied()
.filter(|f| !available.contains(*f)),
);
if missing.is_empty() {
continue;
}
let first = check_expressible(&missing, &profile, version).unwrap_err();
for _ in 0..8 {
let again = check_expressible(&missing, &profile, version).unwrap_err();
assert_eq!(first, again, "{version}");
}
}
}