@@ -351,3 +351,128 @@ fn parses_candidate_finish_message() {
351351 Some ( "Response blocked by safety filters" )
352352 ) ;
353353}
354+
355+ // ---------------------------------------------------------------------------
356+ // Regression tests for issue #126 / upstream PR #162: Gemini's generateContent
357+ // rejects standard JSON-Schema metadata. MCP servers (notion, supabase, …)
358+ // emit schemas with $defs/$ref/$schema, which would otherwise crash the call.
359+ // `gemini_compatible_schema` must inline $ref targets and strip the metadata.
360+ // ---------------------------------------------------------------------------
361+
362+ #[ test]
363+ fn gemini_schema_inlines_refs_and_strips_metadata ( ) {
364+ let schema = serde_json:: json!( {
365+ "$schema" : "https://json-schema.org/draft-07" ,
366+ "$id" : "https://example.com/foo.json" ,
367+ "title" : "Foo" ,
368+ "type" : "object" ,
369+ "$defs" : {
370+ "Inner" : {
371+ "type" : "object" ,
372+ "properties" : {
373+ "kind" : { "const" : "leaf" }
374+ }
375+ }
376+ } ,
377+ "properties" : {
378+ "inner" : { "$ref" : "#/$defs/Inner" }
379+ }
380+ } ) ;
381+
382+ let out = gemini_compatible_schema ( & schema) ;
383+ let obj = out. as_object ( ) . expect ( "object root" ) ;
384+
385+ // Metadata keywords are stripped.
386+ assert ! ( !obj. contains_key( "$schema" ) , "$schema should be stripped" ) ;
387+ assert ! ( !obj. contains_key( "$id" ) , "$id should be stripped" ) ;
388+ assert ! ( !obj. contains_key( "title" ) , "title should be stripped" ) ;
389+ assert ! (
390+ !obj. contains_key( "$defs" ) ,
391+ "$defs should be stripped from root"
392+ ) ;
393+
394+ // $ref is inlined — `inner` should now look like the Inner def.
395+ let inner = obj
396+ . get ( "properties" )
397+ . and_then ( |p| p. get ( "inner" ) )
398+ . and_then ( |v| v. as_object ( ) )
399+ . expect ( "inner object" ) ;
400+ assert_eq ! ( inner. get( "type" ) . and_then( |v| v. as_str( ) ) , Some ( "object" ) ) ;
401+ // const → enum conversion still happens after inlining.
402+ let kind = inner
403+ . get ( "properties" )
404+ . and_then ( |p| p. get ( "kind" ) )
405+ . and_then ( |v| v. as_object ( ) )
406+ . expect ( "kind object" ) ;
407+ let enum_arr = kind. get ( "enum" ) . and_then ( |v| v. as_array ( ) ) . expect ( "enum" ) ;
408+ assert_eq ! ( enum_arr. len( ) , 1 ) ;
409+ assert_eq ! ( enum_arr[ 0 ] . as_str( ) , Some ( "leaf" ) ) ;
410+ }
411+
412+ #[ test]
413+ fn gemini_schema_resolves_definitions_alias_too ( ) {
414+ // Older drafts use `definitions` instead of `$defs`. Both must work.
415+ let schema = serde_json:: json!( {
416+ "type" : "object" ,
417+ "definitions" : {
418+ "Bar" : { "type" : "string" }
419+ } ,
420+ "properties" : {
421+ "bar" : { "$ref" : "#/definitions/Bar" }
422+ }
423+ } ) ;
424+
425+ let out = gemini_compatible_schema ( & schema) ;
426+ let obj = out. as_object ( ) . expect ( "object root" ) ;
427+ assert ! ( !obj. contains_key( "definitions" ) ) ;
428+ let bar = obj
429+ . get ( "properties" )
430+ . and_then ( |p| p. get ( "bar" ) )
431+ . and_then ( |v| v. as_object ( ) )
432+ . expect ( "bar object" ) ;
433+ assert_eq ! ( bar. get( "type" ) . and_then( |v| v. as_str( ) ) , Some ( "string" ) ) ;
434+ }
435+
436+ #[ test]
437+ fn gemini_schema_falls_back_to_empty_object_on_unresolved_ref ( ) {
438+ // Unresolvable $ref must produce a permissive empty object, not a crash
439+ // and not a forwarded $ref Gemini would reject.
440+ let schema = serde_json:: json!( {
441+ "type" : "object" ,
442+ "properties" : {
443+ "ghost" : { "$ref" : "#/$defs/DoesNotExist" }
444+ }
445+ } ) ;
446+ let out = gemini_compatible_schema ( & schema) ;
447+ let ghost = out
448+ . get ( "properties" )
449+ . and_then ( |p| p. get ( "ghost" ) )
450+ . expect ( "ghost" ) ;
451+ let ghost_obj = ghost. as_object ( ) . expect ( "ghost object" ) ;
452+ assert ! (
453+ ghost_obj. is_empty( ) ,
454+ "unresolved $ref should become an empty object, got {ghost:?}"
455+ ) ;
456+ }
457+
458+ #[ test]
459+ fn gemini_schema_does_not_recurse_forever_on_cycles ( ) {
460+ // A self-referential schema must terminate (returns an empty object once
461+ // depth limit is exceeded) instead of overflowing the stack.
462+ let schema = serde_json:: json!( {
463+ "type" : "object" ,
464+ "$defs" : {
465+ "Loop" : {
466+ "type" : "object" ,
467+ "properties" : {
468+ "next" : { "$ref" : "#/$defs/Loop" }
469+ }
470+ }
471+ } ,
472+ "properties" : {
473+ "head" : { "$ref" : "#/$defs/Loop" }
474+ }
475+ } ) ;
476+ // Should not panic / overflow.
477+ let _ = gemini_compatible_schema ( & schema) ;
478+ }
0 commit comments