use std::collections::BTreeSet;
use super::*;
use rypsok_core::model::ErrorKind;
fn policy(languages: &[&str]) -> SearchPolicy {
SearchPolicy {
query_max_chars: 400,
max_results_default: 5,
max_results_ceiling: 20,
languages: languages
.iter()
.map(|l| Language::new(*l).unwrap())
.collect(),
categories: [Category::General, Category::Science]
.into_iter()
.collect::<BTreeSet<_>>(),
}
}
#[test]
fn req_mcp_008_definitions_fit_the_budget_and_are_flat() {
let tools = tool_list(&policy(&["en", "ja"]), true);
assert!(
definitions_size(&tools) <= 2000,
"definitions take {} characters",
definitions_size(&tools)
);
for tool in &tools {
let schema = Value::Object(tool.input_schema.as_ref().clone());
assert_eq!(schema["type"], "object");
assert_eq!(schema["additionalProperties"], false);
let properties = schema["properties"]
.as_object()
.cloned()
.unwrap_or_default();
assert!(
properties.len() <= 5,
"{} has {} parameters",
tool.name,
properties.len()
);
for (name, property) in properties {
let kind = property["type"].as_str().unwrap_or_default();
assert!(
matches!(kind, "string" | "integer" | "boolean"),
"{name} is a {kind}"
);
}
}
}
#[test]
fn req_mcp_009_annotations_and_order() {
let tools = tool_list(&policy(&["en"]), true);
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
assert_eq!(names, vec!["web_search", "engine_status"]);
for tool in &tools {
let a = tool.annotations.as_ref().unwrap();
assert_eq!(a.read_only_hint, Some(true));
assert_eq!(a.destructive_hint, Some(false));
assert_eq!(a.idempotent_hint, Some(true));
assert_eq!(a.open_world_hint, Some(tool.name == "web_search"));
}
}
#[test]
fn req_status_003_status_tool_absent_when_disabled() {
let tools = tool_list(&policy(&["en"]), false);
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "web_search");
}
#[test]
fn req_search_002_schema_enumerates_served_categories_and_languages() {
let schema = web_search_input_schema(&policy(&["en", "ja"]));
assert_eq!(
schema["properties"]["category"]["enum"],
json!(["general", "science"])
);
assert_eq!(
schema["properties"]["language"]["enum"],
json!(["en", "ja"])
);
assert_eq!(schema["properties"]["max_results"]["default"], json!(5));
assert_eq!(schema["properties"]["query"]["maxLength"], json!(400));
let single = web_search_input_schema(&policy(&["en"]));
assert!(single["properties"].get("language").is_none());
}
#[test]
fn req_mcp_004_arguments_are_validated_per_parameter() {
let args = |v: Value| -> Option<JsonObject> { v.as_object().cloned() };
let err = parse_search_arguments(args(json!({})).as_ref()).unwrap_err();
assert_eq!(
(err.kind, err.detail.parameter.as_deref()),
(ErrorKind::InvalidRequest, Some("query"))
);
let err =
parse_search_arguments(args(json!({ "query": "x", "page": 2 })).as_ref()).unwrap_err();
assert!(!err.message.contains("page"));
assert_eq!(err.detail.allowed.as_ref().map(Vec::len), Some(5));
let err = parse_search_arguments(args(json!({ "query": 5 })).as_ref()).unwrap_err();
assert_eq!(err.message, "query must be a string");
let err = parse_search_arguments(args(json!({ "query": "x", "max_results": "many" })).as_ref())
.unwrap_err();
assert_eq!(err.detail.parameter.as_deref(), Some("max_results"));
let err =
parse_search_arguments(args(json!({ "query": "x", "category": "shopping" })).as_ref())
.unwrap_err();
assert_eq!(err.detail.parameter.as_deref(), Some("category"));
assert!(!err.message.contains("shopping"));
let ok = parse_search_arguments(
args(json!({ "query": "x", "max_results": 3, "time_range": "week", "language": "ja" }))
.as_ref(),
)
.unwrap();
assert_eq!(ok.max_results, Some(3));
assert_eq!(ok.time_range, Some(TimeRange::Week));
assert_eq!(ok.language.unwrap().as_str(), "ja");
}
#[test]
fn req_mcp_011_output_schemas_match_the_fixtures() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("fixtures")
.join("schemas");
for (name, schema) in [
("search_response.json", search_output_schema()),
("fetch_response.json", fetch_output_schema()),
("status_response.json", status_output_schema()),
] {
let path = root.join(name);
let rendered = serde_json::to_string_pretty(&schema).unwrap() + "\n";
if std::env::var_os("RYPSOK_UPDATE_FIXTURES").is_some() {
std::fs::write(&path, &rendered).unwrap();
}
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"{}: {e} (set RYPSOK_UPDATE_FIXTURES=1 to write it)",
path.display()
)
});
assert_eq!(
expected.replace("\r\n", "\n"),
rendered,
"{} changed; review it and set RYPSOK_UPDATE_FIXTURES=1 to accept",
path.display()
);
assert!(
schema["properties"].get("outcome").is_some(),
"{name} declares outcome"
);
}
}