test(llm): 锁定 llm_provider_failed 失败日志 request_id 条件透传(CR-H4 收尾)

This commit is contained in:
Yaojia Wang
2026-07-08 11:29:27 +02:00
parent d6b53d08b2
commit e08b01ac58

View File

@@ -7,7 +7,10 @@
from __future__ import annotations from __future__ import annotations
import uuid
import pytest import pytest
import structlog
from fakes_resilience import ( from fakes_resilience import (
AuthError, AuthError,
FakeLedger, FakeLedger,
@@ -18,9 +21,22 @@ from fakes_resilience import (
transient, transient,
) )
from ww_llm_gateway.gateway import CircuitBreaker, Gateway from ww_llm_gateway.gateway import CircuitBreaker, Gateway
from ww_llm_gateway.types import LlmRequest from ww_llm_gateway.types import LlmRequest, Scope
from ww_shared import AppError, ErrorCode from ww_shared import AppError, ErrorCode
_SCOPE = Scope(user_id=uuid.UUID(int=1))
def _failing_gw() -> Gateway:
"""单 provider 链、每次 transient 失败:耗尽重试后记 `llm_provider_failed` 再抛。"""
primary = ScriptedAdapter("deepseek", failures=[transient() for _ in range(10)])
return Gateway(
{"deepseek": primary},
FakeLedger(),
chain_resolver=chain_resolver(chain(("deepseek", "deepseek-chat"))),
max_retries=1,
)
async def test_primary_success_no_fallback(req: LlmRequest) -> None: async def test_primary_success_no_fallback(req: LlmRequest) -> None:
primary = ScriptedAdapter("deepseek", text="主模型") primary = ScriptedAdapter("deepseek", text="主模型")
@@ -238,3 +254,41 @@ async def test_open_circuit_skips_provider(req: LlmRequest) -> None:
assert resp.served_by.provider == "openai" assert resp.served_by.provider == "openai"
assert resp.served_by.fell_back is True assert resp.served_by.fell_back is True
assert primary.complete_calls == 0 assert primary.complete_calls == 0
# CR-H4 收尾:`llm_provider_failed`(失败/回退日志)也须条件透传 request_id——
# 设了则带上、未设则不发键(否则 None 覆盖 merge_contextvars 供的 id回退 sync/SSE 追踪)。
async def test_run_failure_log_carries_request_id_conditionally() -> None:
with structlog.testing.capture_logs() as logs:
with pytest.raises(AppError):
await _failing_gw().run(
LlmRequest(tier="writer", input="x", scope=_SCOPE, request_id="rid-run")
)
failed = next(e for e in logs if e["event"] == "llm_provider_failed")
assert failed["request_id"] == "rid-run"
with structlog.testing.capture_logs() as logs:
with pytest.raises(AppError):
await _failing_gw().run(LlmRequest(tier="writer", input="x", scope=_SCOPE))
failed = next(e for e in logs if e["event"] == "llm_provider_failed")
assert "request_id" not in failed
async def test_stream_failure_log_carries_request_id_conditionally() -> None:
with structlog.testing.capture_logs() as logs:
with pytest.raises(AppError):
async for _ in _failing_gw().stream(
LlmRequest(tier="writer", input="x", scope=_SCOPE, request_id="rid-stream")
):
pass
failed = next(e for e in logs if e["event"] == "llm_provider_failed")
assert failed["request_id"] == "rid-stream"
with structlog.testing.capture_logs() as logs:
with pytest.raises(AppError):
async for _ in _failing_gw().stream(LlmRequest(tier="writer", input="x", scope=_SCOPE)):
pass
failed = next(e for e in logs if e["event"] == "llm_provider_failed")
assert "request_id" not in failed