@@ -20,6 +20,7 @@ use std::sync::Arc;
2020use tokio:: sync:: mpsc;
2121use tokio_stream:: StreamExt ;
2222use tokio_stream:: wrappers:: ReceiverStream ;
23+ use tonic:: metadata:: KeyAndValueRef ;
2324
2425/// Execution plan for inspect workflow visualization
2526#[ derive( Debug , Clone , Serialize , Deserialize ) ]
@@ -777,6 +778,7 @@ impl TestRunner {
777778
778779 let mut variables: HashMap < String , Value > = HashMap :: new ( ) ;
779780 let mut last_message: Option < Value > = None ;
781+ let mut last_error_message: Option < String > = None ;
780782 let mut captured_headers: HashMap < String , String > = HashMap :: new ( ) ;
781783 let mut captured_trailers: HashMap < String , String > = HashMap :: new ( ) ;
782784 let mut failure_reasons: Vec < String > = Vec :: new ( ) ;
@@ -1076,8 +1078,34 @@ impl TestRunner {
10761078 }
10771079 }
10781080
1079- // Standalone Asserts - consumes a new message
1080- match response_stream. as_mut ( ) . unwrap ( ) . next ( ) . await {
1081+ // Standalone ASSERTS usually consumes a new message.
1082+ // If stream is unavailable but we already captured an ERROR,
1083+ // evaluate assertions against that error context.
1084+ let Some ( stream) = response_stream. as_mut ( ) else {
1085+ if !self . no_assert
1086+ && let SectionContent :: Assertions ( lines) = & section. content
1087+ {
1088+ if let Some ( error_message) = & last_error_message {
1089+ let error_value = Value :: String ( error_message. clone ( ) ) ;
1090+ self . run_assertions (
1091+ lines,
1092+ & error_value,
1093+ & captured_headers,
1094+ & captured_trailers,
1095+ & mut failure_reasons,
1096+ format ! ( "after ERROR at line {}" , section. start_line) ,
1097+ ) ;
1098+ } else {
1099+ failure_reasons. push ( format ! (
1100+ "ASSERTS section at line {} has no active response/error context" ,
1101+ section. start_line
1102+ ) ) ;
1103+ }
1104+ }
1105+ continue ;
1106+ } ;
1107+
1108+ match stream. next ( ) . await {
10811109 Some ( Ok ( crate :: grpc:: client:: StreamItem :: Message ( msg) ) ) => {
10821110 last_message = Some ( msg. clone ( ) ) ;
10831111
@@ -1119,6 +1147,9 @@ impl TestRunner {
11191147 }
11201148 }
11211149 Some ( Err ( status) ) => {
1150+ last_error_message = Some ( status. message ( ) . to_string ( ) ) ;
1151+ captured_trailers
1152+ . extend ( Self :: metadata_map_to_hashmap ( status. metadata ( ) ) ) ;
11221153 if !self . no_assert {
11231154 failure_reasons. push ( format ! (
11241155 "Expected message for ASSERTS section at line {}, but received Error: {}" ,
@@ -1202,6 +1233,10 @@ impl TestRunner {
12021233 let ( matches, got, mismatch_reason) = if let Some ( status) =
12031234 e. downcast_ref :: < tonic:: Status > ( )
12041235 {
1236+ last_error_message = Some ( status. message ( ) . to_string ( ) ) ;
1237+ captured_trailers. extend (
1238+ Self :: metadata_map_to_hashmap ( status. metadata ( ) ) ,
1239+ ) ;
12051240 let status_name = Self :: grpc_code_name_from_numeric (
12061241 status. code ( ) as i64 ,
12071242 )
@@ -1293,6 +1328,9 @@ impl TestRunner {
12931328 // Expect an error from the stream
12941329 match response_stream. as_mut ( ) . unwrap ( ) . next ( ) . await {
12951330 Some ( Err ( status) ) => {
1331+ last_error_message = Some ( status. message ( ) . to_string ( ) ) ;
1332+ captured_trailers
1333+ . extend ( Self :: metadata_map_to_hashmap ( status. metadata ( ) ) ) ;
12961334 let status_name =
12971335 Self :: grpc_code_name_from_numeric ( status. code ( ) as i64 )
12981336 . unwrap_or ( "Unknown" ) ;
@@ -1492,6 +1530,26 @@ impl TestRunner {
14921530 self . response_handler . validate_document ( document, response)
14931531 }
14941532
1533+ fn metadata_map_to_hashmap ( metadata : & tonic:: metadata:: MetadataMap ) -> HashMap < String , String > {
1534+ let mut out = HashMap :: new ( ) ;
1535+ for entry in metadata. iter ( ) {
1536+ match entry {
1537+ KeyAndValueRef :: Ascii ( key, value) => {
1538+ if let Ok ( v) = value. to_str ( ) {
1539+ out. insert ( key. to_string ( ) , v. to_string ( ) ) ;
1540+ }
1541+ }
1542+ KeyAndValueRef :: Binary ( key, value) => {
1543+ out. insert (
1544+ key. to_string ( ) ,
1545+ String :: from_utf8_lossy ( value. as_encoded_bytes ( ) ) . into_owned ( ) ,
1546+ ) ;
1547+ }
1548+ }
1549+ }
1550+ out
1551+ }
1552+
14951553 fn substitute_variables ( & self , value : & mut Value , variables : & HashMap < String , Value > ) {
14961554 match value {
14971555 Value :: String ( s) => {
@@ -2026,4 +2084,21 @@ mod tests {
20262084 assert_eq ! ( values. len( ) , 1 ) ;
20272085 assert_eq ! ( values[ 0 ] , json!( { "key" : "value" } ) ) ;
20282086 }
2087+
2088+ #[ test]
2089+ fn test_metadata_map_to_hashmap_extracts_ascii_values ( ) {
2090+ let mut metadata = tonic:: metadata:: MetadataMap :: new ( ) ;
2091+ metadata. insert ( "code" , "EXTERNAL_SERVICE_ERROR_CODE" . parse ( ) . unwrap ( ) ) ;
2092+ metadata. insert ( "message" , "External service error message" . parse ( ) . unwrap ( ) ) ;
2093+
2094+ let trailers = TestRunner :: metadata_map_to_hashmap ( & metadata) ;
2095+ assert_eq ! (
2096+ trailers. get( "code" ) ,
2097+ Some ( & "EXTERNAL_SERVICE_ERROR_CODE" . to_string( ) )
2098+ ) ;
2099+ assert_eq ! (
2100+ trailers. get( "message" ) ,
2101+ Some ( & "External service error message" . to_string( ) )
2102+ ) ;
2103+ }
20292104}
0 commit comments