@@ -154,6 +154,33 @@ def test_preexisting_baggage_preserved(self, memory_and_logger):
154154 assert parsed ["team" ] == "eng"
155155 assert parsed [BRAINTRUST_PARENT_KEY ] == "project_name:propagation-test"
156156
157+ def test_title_cased_baggage_emits_single_lowercase_header (self , memory_and_logger ):
158+ # Per W3C (§3.3.1) the header name SHOULD be sent lowercase. A carrier
159+ # that arrives with a title-cased `Baggage` (e.g. from a framework that
160+ # normalizes header casing) must be rewritten to a single lowercase
161+ # `baggage` key, not left with two conflicting case-variants.
162+ _mem , logger = memory_and_logger
163+ with logger .start_span (name = "svc_a" ) as span :
164+ carrier = span .inject ({"Baggage" : "user=alice" })
165+
166+ baggage_keys = [k for k in carrier if k .lower () == BAGGAGE_HEADER ]
167+ assert baggage_keys == [BAGGAGE_HEADER ]
168+ parsed = parse_baggage (carrier [BAGGAGE_HEADER ])
169+ assert parsed ["user" ] == "alice"
170+ assert parsed [BRAINTRUST_PARENT_KEY ] == "project_name:propagation-test"
171+
172+ def test_title_cased_traceparent_emits_single_lowercase_header (self , memory_and_logger ):
173+ # A pre-existing title-cased `Traceparent` must be replaced by a single
174+ # lowercase `traceparent` (W3C §3.2.1), with no stale variant remaining.
175+ _mem , logger = memory_and_logger
176+ with logger .start_span (name = "svc_a" ) as span :
177+ carrier = span .inject ({"Traceparent" : "stale" })
178+
179+ traceparent_keys = [k for k in carrier if k .lower () == TRACEPARENT_HEADER ]
180+ assert traceparent_keys == [TRACEPARENT_HEADER ]
181+ tp = parse_traceparent (carrier [TRACEPARENT_HEADER ])
182+ assert tp == (span .root_span_id , span .span_id )
183+
157184 def test_never_emits_x_bt_parent (self , memory_and_logger ):
158185 _mem , logger = memory_and_logger
159186 with logger .start_span (name = "svc_a" ) as span :
@@ -371,6 +398,32 @@ def test_legacy_parent_slug_ignored_in_hex_mode():
371398 assert not child .span_parents
372399
373400
401+ def test_legacy_parent_slug_ignored_in_hex_mode_toplevel_start_span ():
402+ # Same as above, but through the module-level `start_span`, which resolves
403+ # the parent slug independently of `Logger.start_span` and must apply the
404+ # same format-mismatch guard.
405+ import uuid
406+
407+ from braintrust .logger import start_span
408+ from braintrust .span_identifier_v3 import SpanComponentsV3
409+
410+ legacy_slug = SpanComponentsV3 (
411+ object_type = SpanObjectTypeV3 .PROJECT_LOGS ,
412+ object_id = "legacy-proj" ,
413+ row_id = str (uuid .uuid4 ()),
414+ span_id = str (uuid .uuid4 ()),
415+ root_span_id = str (uuid .uuid4 ()),
416+ ).to_str ()
417+
418+ with _internal_with_memory_background_logger ():
419+ init_test_logger ("legacy-proj" )
420+ with start_span (name = "child" , parent = legacy_slug ) as child :
421+ # Fresh hex root: hex-shaped ids, no parent linkage.
422+ assert len (child .span_id ) == 16
423+ assert len (child .root_span_id ) == 32
424+ assert not child .span_parents
425+
426+
374427# --------------------------------------------------------------------------- #
375428# tracestate pass-through (W3C: forward upstream vendor state)
376429# --------------------------------------------------------------------------- #
0 commit comments