@@ -602,17 +602,9 @@ fn extract_context_metadata(payload: &[u8]) -> Option<ContextMetadata> {
602602 } ;
603603
604604 // Find key 30 (context_metadata)
605- let context_metadata_value = map. iter ( ) . find_map ( |( k, v) | {
606- let key = match k {
607- Value :: Integer ( i) => i. as_u64 ( ) ?,
608- _ => return None ,
609- } ;
610- if key == 30 {
611- Some ( v)
612- } else {
613- None
614- }
615- } ) ?;
605+ let context_metadata_value =
606+ map. iter ( )
607+ . find_map ( |( k, v) | if key_to_tag ( k) ? == 30 { Some ( v) } else { None } ) ?;
616608
617609 let metadata_map = match context_metadata_value {
618610 Value :: Map ( m) => m,
@@ -622,9 +614,9 @@ fn extract_context_metadata(payload: &[u8]) -> Option<ContextMetadata> {
622614 let mut metadata = ContextMetadata :: default ( ) ;
623615
624616 for ( k, v) in metadata_map. iter ( ) {
625- let key = match k {
626- Value :: Integer ( i ) => i . as_u64 ( ) . unwrap_or ( 0 ) ,
627- _ => continue ,
617+ let key = match key_to_tag ( k ) {
618+ Some ( t ) => t ,
619+ None => continue ,
628620 } ;
629621
630622 match key {
@@ -685,9 +677,9 @@ fn extract_provenance(prov_map: &[(Value, Value)]) -> Provenance {
685677 let mut prov = Provenance :: default ( ) ;
686678
687679 for ( k, v) in prov_map. iter ( ) {
688- let key = match k {
689- Value :: Integer ( i ) => i . as_u64 ( ) . unwrap_or ( 0 ) ,
690- _ => continue ,
680+ let key = match key_to_tag ( k ) {
681+ Some ( t ) => t ,
682+ None => continue ,
691683 } ;
692684
693685 match key {
@@ -741,6 +733,20 @@ fn extract_provenance(prov_map: &[(Value, Value)]) -> Provenance {
741733 prov
742734}
743735
736+ /// Interpret a msgpack map key as a numeric tag.
737+ /// Accepts both integer keys and string-encoded integers (e.g., "30").
738+ /// Matches the projection layer's key_to_tag behavior (CLIENT_SPEC.md §3.1).
739+ fn key_to_tag ( key : & Value ) -> Option < u64 > {
740+ match key {
741+ Value :: Integer ( int) => int. as_u64 ( ) . or_else ( || {
742+ int. as_i64 ( )
743+ . and_then ( |v| if v >= 0 { Some ( v as u64 ) } else { None } )
744+ } ) ,
745+ Value :: String ( s) => s. as_str ( ) ?. parse :: < u64 > ( ) . ok ( ) ,
746+ _ => None ,
747+ }
748+ }
749+
744750fn extract_string ( v : & Value ) -> Option < String > {
745751 if let Value :: String ( s) = v {
746752 s. as_str ( ) . map ( |s| s. to_string ( ) )
@@ -784,3 +790,101 @@ fn extract_string_map(v: &Value) -> Option<HashMap<String, String>> {
784790 None
785791 }
786792}
793+
794+ #[ cfg( test) ]
795+ mod tests {
796+ use super :: * ;
797+ use rmpv:: Value ;
798+
799+ fn str_val ( s : & str ) -> Value {
800+ Value :: String ( s. into ( ) )
801+ }
802+
803+ fn int_val ( n : u64 ) -> Value {
804+ Value :: Integer ( rmpv:: Integer :: from ( n) )
805+ }
806+
807+ #[ test]
808+ fn key_to_tag_accepts_integer_keys ( ) {
809+ assert_eq ! ( key_to_tag( & int_val( 30 ) ) , Some ( 30 ) ) ;
810+ assert_eq ! ( key_to_tag( & int_val( 0 ) ) , Some ( 0 ) ) ;
811+ assert_eq ! ( key_to_tag( & int_val( 80 ) ) , Some ( 80 ) ) ;
812+ }
813+
814+ #[ test]
815+ fn key_to_tag_accepts_string_encoded_integers ( ) {
816+ assert_eq ! ( key_to_tag( & str_val( "30" ) ) , Some ( 30 ) ) ;
817+ assert_eq ! ( key_to_tag( & str_val( "1" ) ) , Some ( 1 ) ) ;
818+ assert_eq ! ( key_to_tag( & str_val( "80" ) ) , Some ( 80 ) ) ;
819+ assert_eq ! ( key_to_tag( & str_val( "0" ) ) , Some ( 0 ) ) ;
820+ }
821+
822+ #[ test]
823+ fn key_to_tag_rejects_non_numeric_strings ( ) {
824+ assert_eq ! ( key_to_tag( & str_val( "hello" ) ) , None ) ;
825+ assert_eq ! ( key_to_tag( & str_val( "" ) ) , None ) ;
826+ assert_eq ! ( key_to_tag( & str_val( "-1" ) ) , None ) ;
827+ }
828+
829+ #[ test]
830+ fn key_to_tag_rejects_other_types ( ) {
831+ assert_eq ! ( key_to_tag( & Value :: Boolean ( true ) ) , None ) ;
832+ assert_eq ! ( key_to_tag( & Value :: Nil ) , None ) ;
833+ }
834+
835+ /// Build a msgpack payload where the outer map uses string keys and
836+ /// the context_metadata inner maps also use string keys — matching
837+ /// what Go's msgpack encoder produces.
838+ fn encode_context_metadata_with_string_keys (
839+ client_tag : & str ,
840+ service_name : & str ,
841+ correlation_id : & str ,
842+ ) -> Vec < u8 > {
843+ let provenance = Value :: Map ( vec ! [
844+ ( str_val( "40" ) , str_val( service_name) ) ,
845+ ( str_val( "12" ) , str_val( correlation_id) ) ,
846+ ] ) ;
847+ let context_metadata = Value :: Map ( vec ! [
848+ ( str_val( "1" ) , str_val( client_tag) ) ,
849+ ( str_val( "10" ) , provenance) ,
850+ ] ) ;
851+ let payload = Value :: Map ( vec ! [ ( str_val( "30" ) , context_metadata) ] ) ;
852+ let mut buf = Vec :: new ( ) ;
853+ rmpv:: encode:: write_value ( & mut buf, & payload) . unwrap ( ) ;
854+ buf
855+ }
856+
857+ fn encode_context_metadata_with_integer_keys ( client_tag : & str , service_name : & str ) -> Vec < u8 > {
858+ let provenance = Value :: Map ( vec ! [ ( int_val( 40 ) , str_val( service_name) ) ] ) ;
859+ let context_metadata = Value :: Map ( vec ! [
860+ ( int_val( 1 ) , str_val( client_tag) ) ,
861+ ( int_val( 10 ) , provenance) ,
862+ ] ) ;
863+ let payload = Value :: Map ( vec ! [ ( int_val( 30 ) , context_metadata) ] ) ;
864+ let mut buf = Vec :: new ( ) ;
865+ rmpv:: encode:: write_value ( & mut buf, & payload) . unwrap ( ) ;
866+ buf
867+ }
868+
869+ #[ test]
870+ fn extract_context_metadata_with_string_keys ( ) {
871+ let payload =
872+ encode_context_metadata_with_string_keys ( "kilroy/run-123" , "kilroy" , "run-123" ) ;
873+ let meta = extract_context_metadata ( & payload) . expect ( "should extract metadata" ) ;
874+ assert_eq ! ( meta. client_tag. as_deref( ) , Some ( "kilroy/run-123" ) ) ;
875+
876+ let prov = meta. provenance . expect ( "should have provenance" ) ;
877+ assert_eq ! ( prov. service_name. as_deref( ) , Some ( "kilroy" ) ) ;
878+ assert_eq ! ( prov. correlation_id. as_deref( ) , Some ( "run-123" ) ) ;
879+ }
880+
881+ #[ test]
882+ fn extract_context_metadata_with_integer_keys ( ) {
883+ let payload = encode_context_metadata_with_integer_keys ( "test-tag" , "my-service" ) ;
884+ let meta = extract_context_metadata ( & payload) . expect ( "should extract metadata" ) ;
885+ assert_eq ! ( meta. client_tag. as_deref( ) , Some ( "test-tag" ) ) ;
886+
887+ let prov = meta. provenance . expect ( "should have provenance" ) ;
888+ assert_eq ! ( prov. service_name. as_deref( ) , Some ( "my-service" ) ) ;
889+ }
890+ }
0 commit comments