#![allow(clippy::unwrap_used)]
use super::*;
use crate::reader::{Limits, parse_trace};
const SET: &str = r#"{
"revisions": ["2025-11-25", "2026-07-28"],
"requirements": [
{"id": "BASE-001", "level": "MUST", "actor": "both",
"source": {"section": "b#x", "quote": "MUST jsonrpc 2.0"},
"checks": ["base.jsonrpc-version"]},
{"id": "LIFE-009", "level": "MUST", "actor": "server",
"applies": {"removed": "2026-07-28"},
"source": {"section": "l#y", "quote": "MUST jsonrpc 2.0"},
"checks": ["base.jsonrpc-version"]},
{"id": "DISC-001", "level": "MUST", "actor": "server",
"applies": {"introduced": "2026-07-28"},
"source": {"section": "d#z", "quote": "MUST jsonrpc 2.0"},
"checks": ["base.jsonrpc-version"]}
]
}"#;
fn set() -> RegistrySet {
RegistrySet::from_json(SET).unwrap()
}
fn revs() -> [ProtocolRevision; 2] {
["2025-11-25".parse().unwrap(), "2026-07-28".parse().unwrap()]
}
#[test]
fn no_revisions_is_an_error() {
assert_eq!(
validate_revisions(&set(), &[], &[]),
Err(MultiError::NoRevisions)
);
}
#[test]
fn unknown_revision_names_itself() {
let unknown: ProtocolRevision = "2024-01-01".parse().unwrap();
assert_eq!(
validate_revisions(&set(), &[unknown], &[]),
Err(MultiError::UnknownRevision(unknown))
);
assert!(unknown.to_string().contains("2024-01-01"));
}
#[test]
fn rows_align_outcomes_with_revisions_and_mark_absence() {
let report = validate_revisions(&set(), &revs(), &[]).unwrap();
assert_eq!(report.revisions, ["2025-11-25", "2026-07-28"]);
assert_eq!(report.summaries.len(), 2);
let find = |id: &str| {
report
.requirements
.iter()
.find(|row| row.id == id)
.cloned()
.unwrap()
};
let base = find("BASE-001");
assert!(base.outcomes[0].is_some() && base.outcomes[1].is_some());
assert!(!base.differs());
let life = find("LIFE-009");
assert!(life.outcomes[0].is_some());
assert_eq!(life.outcomes[1], None);
assert!(life.differs());
let disc = find("DISC-001");
assert_eq!(disc.outcomes[0], None);
assert!(disc.outcomes[1].is_some());
assert!(disc.differs());
}
#[test]
fn union_order_follows_the_set_and_drops_clauses_in_no_judged_revision() {
let older: [ProtocolRevision; 1] = ["2025-11-25".parse().unwrap()];
let report = validate_revisions(&set(), &older, &[]).unwrap();
let ids: Vec<&str> = report.requirements.iter().map(|r| r.id.as_str()).collect();
assert_eq!(ids, ["BASE-001", "LIFE-009"]);
assert!(report.requirements.iter().all(|row| !row.differs()));
}
#[test]
fn differs_detects_a_non_adjacent_divergence() {
let uniform = MultiRow {
id: "X-001".to_owned(),
level: "MUST".to_owned(),
outcomes: vec![
Some(Outcome::Pass),
Some(Outcome::Pass),
Some(Outcome::Pass),
],
};
assert!(!uniform.differs());
let diverges = MultiRow {
outcomes: vec![
Some(Outcome::Pass),
Some(Outcome::Pass),
Some(Outcome::Fail),
],
..uniform
};
assert!(diverges.differs());
}
#[test]
fn overall_verdict_is_the_worst_across_revisions() {
let mut report = validate_revisions(&set(), &revs(), &[]).unwrap();
assert_eq!(report.verdict(), Verdict::Pass);
report.summaries[1].verdict = Verdict::PassWithWarnings;
assert_eq!(report.verdict(), Verdict::PassWithWarnings);
report.summaries[1].verdict = Verdict::Fail;
assert_eq!(report.verdict(), Verdict::Fail);
report.summaries[0].verdict = Verdict::Unsupported;
assert_eq!(report.verdict(), Verdict::Unsupported);
}
#[test]
fn human_render_shows_each_revision_cell_and_marks_divergence() {
let report = validate_revisions(&set(), &revs(), &[]).unwrap();
let text = report.render_human();
assert!(text.contains("revisions 2025-11-25, 2026-07-28"), "{text}");
assert!(text.contains("LIFE-009"), "{text}");
assert!(text.contains("2026-07-28=absent"), "{text}");
assert!(text.contains("*differs"), "{text}");
assert!(text.contains("overall verdict: pass"), "{text}");
}
#[cfg(feature = "draft-2026-07-28")]
#[test]
fn the_shipped_registries_share_no_clause() {
use mcp_conformance_core::requirement::RegistrySet;
let set = RegistrySet::builtin().unwrap();
let revisions: Vec<ProtocolRevision> = set
.revisions()
.iter()
.map(|revision| revision.to_string().parse().unwrap())
.collect();
assert_eq!(revisions.len(), 2, "this test is about the pair");
let report = validate_revisions(&set, &revisions, &[]).unwrap();
let shared: Vec<&str> = report
.requirements
.iter()
.filter(|row| row.outcomes.iter().all(Option::is_some))
.map(|row| row.id.as_str())
.collect();
assert!(shared.is_empty(), "in force at both revisions: {shared:?}");
assert!(
report.requirements.iter().all(MultiRow::differs),
"every row differs, which is what makes the marker uninformative"
);
}
#[test]
fn each_revision_line_accounts_for_every_clause_it_judged() {
let report = validate_revisions(&set(), &revs(), &[]).unwrap();
let text = report.render_human();
for (index, summary) in report.summaries.iter().enumerate() {
let line = format!(
" {}: {} — verdict {}\n",
summary.revision, summary.totals, summary.verdict
);
assert!(text.contains(&line), "{text}");
let judged = report
.requirements
.iter()
.filter(|row| row.outcomes.get(index).is_some_and(Option::is_some))
.count();
let counted: u32 = summary
.totals
.labelled()
.iter()
.map(|&(_, count)| count)
.sum();
assert_eq!(counted as usize, judged, "{}: {line}", summary.revision);
}
}
#[test]
fn judges_a_real_trace_and_is_deterministic() {
let trace = r#"{"seq":0,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}}"#;
let events = parse_trace(trace, &Limits::default()).unwrap();
let a = validate_revisions(&set(), &revs(), &events).unwrap();
let b = validate_revisions(&set(), &revs(), &events).unwrap();
assert_eq!(
serde_json::to_string(&a).unwrap(),
serde_json::to_string(&b).unwrap()
);
}
#[test]
#[cfg(feature = "draft-2026-07-28")]
fn a_run_that_judged_none_of_the_sessions_revisions_says_so() {
let stateless = r#"{"seq":0,"direction":"client-to-server","transport":"streamable-http","kind":"message","payload":{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}}
{"seq":1,"direction":"server-to-client","transport":"streamable-http","kind":"message","payload":{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}}"#;
let events = parse_trace(stateless, &Limits::default()).unwrap();
let set = RegistrySet::builtin().unwrap();
let wrong: Vec<ProtocolRevision> = vec!["2025-11-25".parse().unwrap()];
let report = validate_revisions(&set, &wrong, &events).unwrap();
assert_eq!(
report.revision_mismatch.as_deref(),
Some(["2026-07-28".to_owned()].as_slice())
);
let rendered = report.render_human();
assert!(
rendered.contains("declares protocol revision 2026-07-28"),
"{rendered}"
);
assert!(rendered.contains("which was not judged"), "{rendered}");
let both: Vec<ProtocolRevision> =
vec!["2025-11-25".parse().unwrap(), "2026-07-28".parse().unwrap()];
let report = validate_revisions(&set, &both, &events).unwrap();
assert!(report.revision_mismatch.is_none());
assert!(!report.render_human().contains("NOTE"));
}