use schemars::{schema_for, JsonSchema};
use serde_json::json;
use super::strip_int_formats;
#[test]
fn removes_rust_int_formats_but_keeps_standard_ones() {
let mut schema: schemars::Schema = serde_json::from_value(json!({
"type": "object",
"properties": {
"id": { "type": "integer", "format": "uint64", "minimum": 0 },
"ids": { "type": "array", "items": { "type": "integer", "format": "uint" } },
"when": { "type": "string", "format": "date-time" }
}
}))
.expect("valid schema");
strip_int_formats(&mut schema);
let value = serde_json::to_value(&schema).expect("serializable");
assert!(value["properties"]["id"].get("format").is_none());
assert!(value["properties"]["ids"]["items"].get("format").is_none());
assert_eq!(value["properties"]["id"]["type"], "integer");
assert_eq!(value["properties"]["id"]["minimum"], 0);
assert_eq!(value["properties"]["when"]["format"], "date-time");
}
#[derive(JsonSchema)]
#[schemars(transform = strip_int_formats)]
#[allow(dead_code)]
struct Sample {
id: u64,
hop: usize,
}
#[test]
fn derived_schema_has_no_int_format() {
let schema = schema_for!(Sample);
let text = serde_json::to_string(&schema).expect("serializable");
assert!(
!text.contains("\"format\""),
"derived schema still carries an int format: {text}"
);
}
#[cfg(feature = "mcp")]
fn scalarized(schema: serde_json::Value) -> serde_json::Value {
let serde_json::Value::Object(map) = schema else {
panic!("test: le schema d'entree est un objet JSON");
};
let mut wire = super::WireInputSchema::adopt(map);
super::scalarize_slot_types(&mut wire);
serde_json::Value::Object(wire.0)
}
#[test]
#[cfg(feature = "mcp")]
fn collapses_a_nullable_union_into_its_typed_branch() {
let out = scalarized(json!({
"type": "object",
"properties": {
"media": {
"description": "la description du slot gagne",
"default": null,
"anyOf": [
{
"type": "object",
"description": "celle de la branche perd",
"properties": {"data": {"type": "string"}},
"required": ["data"]
},
{"type": "null"}
]
}
}
}));
let media = &out["properties"]["media"];
assert_eq!(media["type"], json!("object"));
assert_eq!(media["description"], json!("la description du slot gagne"));
assert_eq!(media["required"], json!(["data"]));
assert!(media.get("anyOf").is_none(), "l'union a disparu: {media}");
assert!(
media.get("default").is_none(),
"(d) un `default: null` sur un slot devenu non-nullable est retire: {media}"
);
}
#[test]
#[cfg(feature = "mcp")]
fn keeps_a_nullable_union_whose_branch_carries_no_direct_type() {
let schema = json!({
"type": "object",
"properties": {
"source": {
"anyOf": [{"$ref": "#/$defs/SourceReference"}, {"type": "null"}]
}
}
});
let out = scalarized(schema.clone());
assert_eq!(
out["properties"]["source"], schema["properties"]["source"],
"une branche non typee reste intacte"
);
}
#[test]
#[cfg(feature = "mcp")]
fn collapses_a_nullable_type_list() {
let out = scalarized(json!({
"type": "object",
"properties": {
"priority": {"type": ["integer", "null"], "minimum": 0, "maximum": 255, "default": null}
}
}));
let priority = &out["properties"]["priority"];
assert_eq!(priority["type"], json!("integer"));
assert_eq!(priority["minimum"], json!(0));
assert_eq!(priority["maximum"], json!(255));
assert!(priority.get("default").is_none(), "{priority}");
}
#[test]
#[cfg(feature = "mcp")]
fn collapses_a_const_union_into_an_enum_and_folds_the_branch_descriptions() {
let out = scalarized(json!({
"type": "object",
"properties": {
"op": {
"description": "Comparison operator.",
"oneOf": [
{"const": "eq", "description": "`=`", "type": "string"},
{"const": "lt", "description": "`<`", "type": "string"}
]
}
}
}));
let op = &out["properties"]["op"];
assert_eq!(op["type"], json!("string"));
assert_eq!(op["enum"], json!(["eq", "lt"]));
assert!(op.get("oneOf").is_none(), "l'union a disparu: {op}");
let description = op["description"].as_str().expect("une description");
assert!(
description.starts_with("Comparison operator."),
"{description}"
);
assert!(description.contains("\"eq\": `=`"), "{description}");
assert!(description.contains("\"lt\": `<`"), "{description}");
}
#[test]
#[cfg(feature = "mcp")]
fn collapses_a_nullable_const_union_through_its_branch() {
let out = scalarized(json!({
"type": "object",
"properties": {
"format": {
"anyOf": [
{"oneOf": [{"const": "plain", "type": "string"}, {"const": "jsonl", "type": "string"}]},
{"type": "null"}
]
}
}
}));
let format = &out["properties"]["format"];
assert_eq!(format["type"], json!("string"));
assert_eq!(format["enum"], json!(["plain", "jsonl"]));
}
#[test]
#[cfg(feature = "mcp")]
fn walks_items_tuple_items_and_additional_properties() {
let out = scalarized(json!({
"type": "object",
"properties": {
"facts": {"type": "array", "items": {"type": "object", "properties": {
"text": {"type": ["string", "null"]}
}}},
"pair": {"type": "array", "items": [
{"type": ["integer", "null"]},
{"type": ["boolean", "null"]}
]},
"models": {"type": "object", "additionalProperties": {
"anyOf": [{"type": "object", "properties": {}}, {"type": "null"}]
}}
}
}));
assert_eq!(
out["properties"]["facts"]["items"]["properties"]["text"]["type"],
json!("string")
);
assert_eq!(
out["properties"]["pair"]["items"][0]["type"],
json!("integer")
);
assert_eq!(
out["properties"]["pair"]["items"][1]["type"],
json!("boolean")
);
assert_eq!(
out["properties"]["models"]["additionalProperties"]["type"],
json!("object")
);
}
#[test]
#[cfg(feature = "mcp")]
fn leaves_a_genuinely_polymorphic_type_list_alone() {
let out = scalarized(json!({
"type": "object",
"properties": {
"value": {"type": ["number", "string", "boolean"]}
}
}));
assert_eq!(
out["properties"]["value"]["type"],
json!(["number", "string", "boolean"])
);
}
#[cfg(feature = "mcp")]
#[derive(schemars::JsonSchema)]
#[allow(dead_code)]
struct NestedOut {
label: String,
}
#[cfg(feature = "mcp")]
#[derive(schemars::JsonSchema)]
#[allow(dead_code)]
struct OptionalOut {
nested: Option<NestedOut>,
memory_id: Option<u64>,
}
#[test]
#[cfg(feature = "mcp")]
fn output_hardening_keeps_a_nullable_union_intact() {
let published = super::wire_safe_output_schema::<OptionalOut>();
let schema = serde_json::to_value(&*published).expect("le schema se serialise");
let nested = &schema["properties"]["nested"];
assert!(
admits_null(nested),
"la sortie doit continuer d'admettre `null` sur un champ optionnel, got {nested}"
);
let memory_id = &schema["properties"]["memory_id"];
assert!(
admits_null(memory_id),
"idem pour un id optionnel, got {memory_id}"
);
}
#[cfg(feature = "mcp")]
fn admits_null(slot: &serde_json::Value) -> bool {
match slot.get("type") {
Some(serde_json::Value::String(kind)) => kind == "null",
Some(serde_json::Value::Array(names)) => names.iter().any(|name| name == "null"),
_ => ["anyOf", "oneOf"].iter().any(|keyword| {
matches!(slot.get(*keyword), Some(serde_json::Value::Array(branches))
if branches.iter().any(admits_null))
}),
}
}