use super::contains_revision;
use crate::checks::draft::testkit::{client, findings_for, server, trace};
const RETRY: &str = "versioning.retry-uses-supported-version";
const EXTENSIONS: &str = "versioning.extension-identifier-format";
const INIT_ERROR: &str = "versioning.initialize-error-names-versions";
fn request_at(seq: u64, id: u64, version: &str) -> String {
client(
seq,
&format!(
r#"{{"jsonrpc":"2.0","id":{id},"method":"tools/list","params":{{"_meta":{{"io.modelcontextprotocol/protocolVersion":"{version}","io.modelcontextprotocol/clientCapabilities":{{}}}}}}}}"#
),
)
}
fn unsupported(seq: u64, id: u64, data: &str) -> String {
server(
seq,
&format!(
r#"{{"jsonrpc":"2.0","id":{id},"error":{{"code":-32022,"message":"Unsupported protocol version"{data}}}}}"#
),
)
}
fn client_extensions(seq: u64, extensions: &str) -> String {
client(
seq,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{{"_meta":{{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{{"extensions":{extensions}}}}}}}}}"#
),
)
}
fn server_extensions(extensions: &str) -> String {
trace(&[
client(
0,
r#"{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}"#,
),
server(
1,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"result":{{"resultType":"complete","supportedVersions":["2026-07-28"],"capabilities":{{"extensions":{extensions}}}}}}}"#
),
),
])
}
fn refused_handshake(error: &str) -> String {
trace(&[
client(
0,
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}"#,
),
server(1, &format!(r#"{{"jsonrpc":"2.0","id":1,"error":{error}}}"#)),
])
}
#[test]
fn without_a_supported_list_the_client_is_not_judged() {
for data in [
"",
r#","data":{"requested":"1900-01-01"}"#,
r#","data":{"supported":[]}"#,
] {
let session = trace(&[
request_at(0, 1, "1900-01-01"),
unsupported(1, 1, data),
request_at(2, 2, "1899-01-01"),
]);
assert!(
findings_for(RETRY, &session).is_empty(),
"data {data:?} should leave the client unjudged"
);
}
}
#[test]
fn retrying_outside_the_offered_list_is_the_violation() {
let session = trace(&[
request_at(0, 1, "1900-01-01"),
unsupported(
1,
1,
r#","data":{"supported":["2026-07-28"],"requested":"1900-01-01"}"#,
),
request_at(2, 2, "1899-01-01"),
]);
let findings = findings_for(RETRY, &session);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("1899-01-01"), "{findings:?}");
assert!(findings[0].contains("2026-07-28"), "{findings:?}");
}
#[test]
fn retrying_inside_the_offered_list_conforms() {
let session = trace(&[
request_at(0, 1, "1900-01-01"),
unsupported(1, 1, r#","data":{"supported":["2026-07-28"]}"#),
request_at(2, 2, "2026-07-28"),
]);
assert!(findings_for(RETRY, &session).is_empty());
}
#[test]
fn the_request_that_drew_the_error_is_not_itself_a_violation() {
let session = trace(&[
request_at(0, 1, "1900-01-01"),
unsupported(1, 1, r#","data":{"supported":["2026-07-28"]}"#),
]);
assert!(findings_for(RETRY, &session).is_empty());
}
#[test]
fn a_later_error_replaces_the_offered_list() {
let session = trace(&[
request_at(0, 1, "1900-01-01"),
unsupported(1, 1, r#","data":{"supported":["2025-11-25"]}"#),
request_at(2, 2, "2025-11-25"),
unsupported(3, 2, r#","data":{"supported":["2026-07-28"]}"#),
request_at(4, 3, "2025-11-25"),
]);
let findings = findings_for(RETRY, &session);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("seq 3"), "{findings:?}");
}
#[test]
fn a_request_declaring_no_version_is_not_judged_here() {
let session = trace(&[
request_at(0, 1, "1900-01-01"),
unsupported(1, 1, r#","data":{"supported":["2026-07-28"]}"#),
client(2, r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#),
]);
assert!(findings_for(RETRY, &session).is_empty());
}
#[test]
fn a_notification_is_not_a_retry() {
let session = trace(&[
request_at(0, 1, "1900-01-01"),
unsupported(1, 1, r#","data":{"supported":["2026-07-28"]}"#),
client(
2,
r#"{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"1899-01-01"}}}"#,
),
]);
assert!(findings_for(RETRY, &session).is_empty());
}
#[test]
fn a_prefixed_extension_identifier_conforms() {
let session = client_extensions(
0,
r#"{"io.modelcontextprotocol/ui":{"mimeTypes":["text/html"]}}"#,
);
assert!(findings_for(EXTENSIONS, &session).is_empty());
}
#[test]
fn an_extension_identifier_without_a_prefix_is_reported() {
let session = client_extensions(0, r#"{"ui":{}}"#);
let findings = findings_for(EXTENSIONS, &session);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("no prefix"), "{findings:?}");
assert!(findings[0].contains("client capabilities"), "{findings:?}");
}
#[test]
fn an_extension_identifier_breaking_the_meta_grammar_is_reported() {
let session = client_extensions(0, r#"{"1bad./x":{}}"#);
let findings = findings_for(EXTENSIONS, &session);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("prefix label"), "{findings:?}");
}
#[test]
fn the_servers_own_extensions_are_judged_too() {
let findings = findings_for(EXTENSIONS, &server_extensions(r#"{"tasks":{}}"#));
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("server capabilities"), "{findings:?}");
let clean = server_extensions(r#"{"io.modelcontextprotocol/tasks":{}}"#);
assert!(findings_for(EXTENSIONS, &clean).is_empty());
}
#[test]
fn a_capability_surface_without_extensions_is_not_judged() {
for extensions in ["{}", "null", "[]", r#""io.modelcontextprotocol/ui""#] {
let session = client_extensions(0, extensions);
assert!(
findings_for(EXTENSIONS, &session).is_empty(),
"extensions {extensions} should yield no identifiers"
);
}
}
#[test]
fn a_handshake_answered_with_a_result_is_a_dual_era_server() {
let session = trace(&[
client(
0,
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}"#,
),
server(
1,
r#"{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{},"serverInfo":{"name":"s","version":"0"}}}"#,
),
]);
assert!(findings_for(INIT_ERROR, &session).is_empty());
}
#[test]
fn a_refusal_naming_no_version_is_reported() {
let findings = findings_for(
INIT_ERROR,
&refused_handshake(r#"{"code":-32601,"message":"Method not found"}"#),
);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("fall-forward"), "{findings:?}");
}
#[test]
fn a_refusal_names_its_versions_in_data_or_in_prose() {
for error in [
r#"{"code":-32022,"message":"Unsupported","data":{"supported":["2026-07-28"]}}"#,
r#"{"code":-32601,"message":"This server speaks 2026-07-28 and later only"}"#,
r#"{"code":-32601,"message":"no","data":{"note":["see 2026-07-28"]}}"#,
] {
assert!(
findings_for(INIT_ERROR, &refused_handshake(error)).is_empty(),
"{error} names a version"
);
}
}
#[test]
fn a_date_shaped_string_that_is_not_a_revision_does_not_count() {
let findings = findings_for(
INIT_ERROR,
&refused_handshake(r#"{"code":-32601,"message":"retry after 2026-13-45"}"#),
);
assert_eq!(findings.len(), 1, "{findings:?}");
}
#[test]
fn contains_revision_scans_anywhere_in_the_string() {
assert!(contains_revision("2026-07-28"));
assert!(contains_revision("we speak 2026-07-28."));
assert!(contains_revision("…2026-07-28"), "multi-byte prefix");
assert!(!contains_revision("2026-07-2"));
assert!(!contains_revision("2026/07/28"));
assert!(!contains_revision("2026-00-28"));
assert!(!contains_revision(""));
}