//! Serde wire-shape pin tests. //! //! `ToolConfigEntry` is serialized into session-bind metadata and backend //! JSONB config storage. These tests pin the exact JSON shape so a //! field rename/retype in `kigi-tools.proto` cannot silently break those //! wire contracts (the producer and consumer live in separate services). use kigi_tools_api::ToolConfigEntry; fn full_entry() -> ToolConfigEntry { ToolConfigEntry { id: "Kigi:grep".to_owned(), params_json: Some(r#"{"max_results":50}"#.to_owned()), name_override: Some("search".to_owned()), params_name_overrides: std::collections::HashMap::from([( "pattern".to_owned(), "query".to_owned(), )]), behavior_version: Some("legacy-0.4.10".to_owned()), description_override: Some("Search the codebase".to_owned()), } } #[test] fn tool_config_entry_serializes_to_pinned_json_shape() { let value = serde_json::to_value(full_entry()).expect("serialize"); assert_eq!( value, serde_json::json!({ "id": "Kigi:grep", "params_json": "{\"max_results\":50}", "name_override": "search", "params_name_overrides": {"pattern": "query"}, "behavior_version": "legacy-0.4.10", "description_override": "Search the codebase", }) ); } #[test] fn tool_config_entry_round_trips() { let entry = full_entry(); let json = serde_json::to_string(&entry).expect("serialize"); let back: ToolConfigEntry = serde_json::from_str(&json).expect("deserialize"); assert_eq!(back, entry); } #[test] fn minimal_entry_deserializes_from_id_only() { let back: ToolConfigEntry = serde_json::from_value(serde_json::json!({"id": "Kigi:read_file"})) .expect("deserialize minimal"); assert_eq!(back.id, "Kigi:read_file"); assert_eq!(back.params_json, None); assert_eq!(back.name_override, None); assert!(back.params_name_overrides.is_empty()); assert_eq!(back.behavior_version, None); assert_eq!(back.description_override, None); } #[test] fn missing_id_fails_to_deserialize() { let result: Result = serde_json::from_value(serde_json::json!({"name_override": "search"})); assert!(result.is_err(), "payload without `id` must be rejected"); } #[test] fn explicit_null_optional_fields_deserialize_as_none() { // `#[serde(default)]` only covers *absent* keys; explicit `null` is // accepted by the `Option` fields themselves. let back: ToolConfigEntry = serde_json::from_value(serde_json::json!({ "id": "Kigi:read_file", "params_json": null, "name_override": null, "behavior_version": null, "description_override": null, })) .expect("deserialize explicit nulls"); assert_eq!(back.params_json, None); assert_eq!(back.name_override, None); assert_eq!(back.behavior_version, None); assert_eq!(back.description_override, None); } #[test] fn explicit_null_map_is_rejected() { // The map field is not `Option`-typed, so `null` is not coerced to an // empty map: producers must omit the key or emit `{}`. let result: Result = serde_json::from_value(serde_json::json!({ "id": "Kigi:read_file", "params_name_overrides": null, })); assert!( result.is_err(), "null params_name_overrides must be rejected (omit the key or send {{}})" ); }