@@ -309,3 +309,146 @@ fn effects_serialize_in_snake_case() {
309309 assert_eq ! ( serde_json:: to_value( effect) . unwrap( ) , json!( wire) ) ;
310310 }
311311}
312+
313+ // ---------------------------------------------------------------------------
314+ // Call outcome
315+ // ---------------------------------------------------------------------------
316+
317+ #[ test]
318+ fn the_call_outcome_kind_is_pinned ( ) {
319+ assert_eq ! ( super :: MCP_CALL_RESULT_KIND , "mcp_call" ) ;
320+ }
321+
322+ #[ test]
323+ fn an_answered_outcome_pins_its_wire_form_and_omits_the_error ( ) {
324+ let outcome = super :: McpCallOutcome :: answered ( "docs" , "search" ) ;
325+ let wire = serde_json:: to_value ( & outcome) . unwrap ( ) ;
326+ assert_eq ! (
327+ wire,
328+ json!( { "kind" : "mcp_call" , "server" : "docs" , "tool" : "search" , "ok" : true } )
329+ ) ;
330+ assert_eq ! (
331+ serde_json:: from_value:: <super :: McpCallOutcome >( wire) . unwrap( ) ,
332+ outcome
333+ ) ;
334+ }
335+
336+ #[ test]
337+ fn a_failed_outcome_pins_its_wire_form_and_round_trips ( ) {
338+ let outcome = super :: McpCallOutcome :: failed (
339+ "docs" ,
340+ "search" ,
341+ super :: McpCallError {
342+ code : crate :: errors:: UNAUTHORIZED . into ( ) ,
343+ unauthorized : true ,
344+ advertises_oauth : true ,
345+ } ,
346+ ) ;
347+ let wire = serde_json:: to_value ( & outcome) . unwrap ( ) ;
348+ assert_eq ! (
349+ wire,
350+ json!( {
351+ "kind" : "mcp_call" ,
352+ "server" : "docs" ,
353+ "tool" : "search" ,
354+ "ok" : false ,
355+ "error" : {
356+ "code" : "ai.tinyhumans.tinymcp.Error.Unauthorized" ,
357+ "unauthorized" : true ,
358+ "advertises_oauth" : true ,
359+ } ,
360+ } )
361+ ) ;
362+ assert_eq ! (
363+ serde_json:: from_value:: <super :: McpCallOutcome >( wire) . unwrap( ) ,
364+ outcome
365+ ) ;
366+ }
367+
368+ #[ test]
369+ fn a_plain_error_carries_no_authorization_signal ( ) {
370+ let error = super :: McpCallError :: new ( crate :: errors:: TRANSPORT ) ;
371+ assert_eq ! ( error. code, crate :: errors:: TRANSPORT ) ;
372+ assert ! ( !error. unauthorized) ;
373+ assert ! ( !error. advertises_oauth) ;
374+ }
375+
376+ #[ test]
377+ fn an_outcome_is_read_back_only_from_metadata_of_its_kind ( ) {
378+ let outcome = super :: McpCallOutcome :: answered ( "docs" , "search" ) ;
379+ let metadata = serde_json:: to_value ( & outcome) . unwrap ( ) ;
380+ assert_eq ! (
381+ super :: McpCallOutcome :: from_metadata( & metadata) ,
382+ Some ( outcome)
383+ ) ;
384+
385+ assert_eq ! (
386+ super :: McpCallOutcome :: from_metadata( & json!( { "kind" : "web_search" } ) ) ,
387+ None
388+ ) ;
389+ assert_eq ! ( super :: McpCallOutcome :: from_metadata( & json!( [ 1 , 2 ] ) ) , None ) ;
390+ assert_eq ! (
391+ super :: McpCallOutcome :: from_metadata( & json!( { "kind" : "mcp_call" , "ok" : "yes" } ) ) ,
392+ None
393+ ) ;
394+ }
395+
396+ #[ test]
397+ fn decoding_an_outcome_rejects_what_the_constructors_never_produce ( ) {
398+ let decode = |value : serde_json:: Value | serde_json:: from_value :: < super :: McpCallOutcome > ( value) ;
399+ let error = json ! ( { "code" : crate :: errors:: TRANSPORT , "unauthorized" : false , "advertises_oauth" : false } ) ;
400+ let base =
401+ |ok : bool | json ! ( { "kind" : "mcp_call" , "server" : "docs" , "tool" : "search" , "ok" : ok } ) ;
402+
403+ let mut answered_with_error = base ( true ) ;
404+ answered_with_error[ "error" ] = error. clone ( ) ;
405+ assert ! ( decode( answered_with_error) . is_err( ) ) ;
406+
407+ assert ! ( decode( base( false ) ) . is_err( ) ) ;
408+ let mut failed_null_error = base ( false ) ;
409+ failed_null_error[ "error" ] = serde_json:: Value :: Null ;
410+ assert ! ( decode( failed_null_error) . is_err( ) ) ;
411+
412+ let mut missing_kind = base ( true ) ;
413+ missing_kind. as_object_mut ( ) . unwrap ( ) . remove ( "kind" ) ;
414+ assert ! ( decode( missing_kind) . is_err( ) ) ;
415+ let mut wrong_kind = base ( true ) ;
416+ wrong_kind[ "kind" ] = json ! ( "web_search" ) ;
417+ assert ! ( decode( wrong_kind) . is_err( ) ) ;
418+
419+ assert_eq ! (
420+ decode( base( true ) ) . unwrap( ) ,
421+ super :: McpCallOutcome :: answered( "docs" , "search" )
422+ ) ;
423+ let mut failed = base ( false ) ;
424+ failed[ "error" ] = error;
425+ assert_eq ! (
426+ decode( failed) . unwrap( ) ,
427+ super :: McpCallOutcome :: failed(
428+ "docs" ,
429+ "search" ,
430+ super :: McpCallError :: new( crate :: errors:: TRANSPORT )
431+ )
432+ ) ;
433+ }
434+
435+ #[ test]
436+ fn decoding_an_error_rejects_an_oauth_advert_without_a_401 ( ) {
437+ let decode = |unauthorized : bool , advertises_oauth : bool | {
438+ serde_json:: from_value :: < super :: McpCallError > ( json ! ( {
439+ "code" : "x" ,
440+ "unauthorized" : unauthorized,
441+ "advertises_oauth" : advertises_oauth,
442+ } ) )
443+ } ;
444+ assert ! ( decode( false , true ) . is_err( ) ) ;
445+ assert ! ( decode( false , false ) . is_ok( ) ) ;
446+ assert ! ( decode( true , false ) . is_ok( ) ) ;
447+ assert ! ( decode( true , true ) . is_ok( ) ) ;
448+
449+ let nested = json ! ( {
450+ "kind" : "mcp_call" , "server" : "docs" , "tool" : "t" , "ok" : false ,
451+ "error" : { "code" : "x" , "unauthorized" : false , "advertises_oauth" : true } ,
452+ } ) ;
453+ assert_eq ! ( super :: McpCallOutcome :: from_metadata( & nested) , None ) ;
454+ }
0 commit comments