Files
deer-flow/backend/tests/test_trace_context.py
yym36991andGitHub 0f088033fe fix(gateway): prefer X-Trace-Id over metadata.deerflow_trace_id when header is set (#4283)
When TraceMiddleware binds a valid inbound X-Trace-Id, worker resolution
prefers that id over config.metadata.deerflow_trace_id so logs, response
headers, Langfuse, and runtime context stay aligned.
Also add trace-context marker reset coverage for the inbound-header flag.
2026-07-19 09:13:51 +08:00

122 lines
4.7 KiB
Python

"""Unit tests for ``deerflow.trace_context`` validation helpers.
The middleware-level end-to-end coverage lives in ``test_trace_middleware.py``;
this file pins the character-set invariants of ``normalize_trace_id`` directly
so that a future relaxation of the check trips a targeted failure.
"""
from __future__ import annotations
import pytest
from deerflow.trace_context import (
_MAX_TRACE_ID_LENGTH,
is_trace_id_from_request_header,
mark_trace_id_from_request_header,
normalize_trace_id,
request_trace_context,
reset_trace_id_from_request_header,
resolve_deerflow_trace_id,
)
class TestNormalizeTraceIdAcceptsPrintableAscii:
def test_accepts_uuid_hex(self) -> None:
assert normalize_trace_id("0123456789abcdef0123456789abcdef") == "0123456789abcdef0123456789abcdef"
def test_accepts_alphanumerics_and_punctuation(self) -> None:
assert normalize_trace_id("abc-123_XYZ.foo:bar/baz") == "abc-123_XYZ.foo:bar/baz"
def test_strips_surrounding_whitespace(self) -> None:
assert normalize_trace_id(" trace-1 ") == "trace-1"
def test_accepts_boundary_low(self) -> None:
assert normalize_trace_id("\x20abc") == "abc"
def test_accepts_boundary_high(self) -> None:
assert normalize_trace_id("abc\x7e") == "abc\x7e"
def test_accepts_maximum_length(self) -> None:
value = "a" * _MAX_TRACE_ID_LENGTH
assert normalize_trace_id(value) == value
class TestNormalizeTraceIdRejectsUnsafeInput:
def test_rejects_non_string(self) -> None:
assert normalize_trace_id(None) is None
assert normalize_trace_id(12345) is None
assert normalize_trace_id(b"abc") is None
def test_rejects_empty_and_whitespace_only(self) -> None:
assert normalize_trace_id("") is None
assert normalize_trace_id(" \t ") is None
def test_rejects_over_max_length(self) -> None:
assert normalize_trace_id("a" * (_MAX_TRACE_ID_LENGTH + 1)) is None
@pytest.mark.parametrize(
"value",
[
"trace\x00id", # NUL
"trace\x1fid", # last C0 control
"trace\tid", # embedded tab
"trace\nid", # LF — the classic log-injection / CRLF pivot
"trace\rid", # CR
],
)
def test_rejects_c0_controls(self, value: str) -> None:
assert normalize_trace_id(value) is None
def test_rejects_del(self) -> None:
assert normalize_trace_id("trace\x7fid") is None
@pytest.mark.parametrize("value", ["trace\x80id", "trace\x9fid"])
def test_rejects_c1_controls_in_latin1_range(self, value: str) -> None:
"""C1 controls latin-1-encode successfully but are stripped or
rejected by hardened intermediaries (nginx / envoy / cloudfront),
silently breaking the response. Reject at validation time."""
assert normalize_trace_id(value) is None
def test_rejects_latin1_supplement_characters(self) -> None:
assert normalize_trace_id("caf\xe9") is None # é = 0xE9
def test_rejects_cjk_characters(self) -> None:
"""Codepoints > 0xFF raise UnicodeEncodeError inside
``MutableHeaders.__setitem__`` before ``send`` is dispatched, forcing
a 500 on any endpoint. This is the exact case from the review."""
assert normalize_trace_id("请求-1") is None
assert normalize_trace_id("トレース") is None
def test_rejects_emoji(self) -> None:
assert normalize_trace_id("trace-\U0001f680") is None # 🚀
def test_rejects_surrogate_pair_pieces(self) -> None:
assert normalize_trace_id("trace-\ud83d") is None
class TestResolveDeerflowTraceId:
def test_header_marker_defaults_false_and_resets(self) -> None:
assert is_trace_id_from_request_header() is False
token = mark_trace_id_from_request_header(from_header=True)
try:
assert is_trace_id_from_request_header() is True
finally:
reset_trace_id_from_request_header(token)
assert is_trace_id_from_request_header() is False
def test_metadata_wins_without_inbound_header(self) -> None:
with request_trace_context("ambient-trace"):
assert resolve_deerflow_trace_id("metadata-trace") == "metadata-trace"
def test_inbound_header_overrides_metadata(self) -> None:
with request_trace_context("header-trace"):
token = mark_trace_id_from_request_header(from_header=True)
try:
assert resolve_deerflow_trace_id("metadata-trace") == "header-trace"
finally:
reset_trace_id_from_request_header(token)
def test_falls_back_to_ambient_context(self) -> None:
with request_trace_context("ambient-only"):
assert resolve_deerflow_trace_id(None) == "ambient-only"