Files
wehub-resource-sync 5296d0e97c
CI / Ban suppressions and legacy annotations (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / ruff-check (push) Has been cancelled
CI / ruff-format (push) Has been cancelled
CI / ty (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:44 +08:00

265 lines
8.1 KiB
Python

"""Provider streams raise canonical failures after closing committed blocks."""
from collections import deque
from collections.abc import AsyncIterator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from free_claude_code.config.nim import NimSettings
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.async_iterators import AsyncCloseable
from free_claude_code.core.failures import ExecutionFailure, FailureKind
from free_claude_code.providers.base import ProviderConfig
from free_claude_code.providers.http import close_provider_stream
from free_claude_code.providers.nvidia_nim import NvidiaNimProvider
from tests.providers.request_factory import make_messages_request
from tests.providers.support import passthrough_rate_limiter
class _FailingStream:
def __init__(
self,
chunks: list[object],
error: Exception | None,
*,
close_error: Exception | None = None,
) -> None:
self._chunks = chunks
self._error = error
self._close_error = close_error
self.close_calls = 0
def __aiter__(self) -> AsyncIterator[object]:
return self._iterate()
async def _iterate(self) -> AsyncIterator[object]:
for chunk in self._chunks:
yield chunk
if self._error is not None:
raise self._error
async def aclose(self) -> None:
self.close_calls += 1
if self._close_error is not None:
raise self._close_error
def _chunk(*, content: str | None = None, finish_reason: str | None = None) -> object:
delta = MagicMock(content=content, tool_calls=None, reasoning_content=None)
choice = MagicMock(delta=delta, finish_reason=finish_reason)
return MagicMock(choices=[choice], usage=None)
def _provider() -> NvidiaNimProvider:
return NvidiaNimProvider(
ProviderConfig(
api_key="test_key",
base_url="https://test.api.nvidia.com/v1",
rate_limit=10,
rate_window=60,
),
nim_settings=NimSettings(),
rate_limiter=passthrough_rate_limiter(),
)
@pytest.mark.asyncio
async def test_committed_provider_failure_closes_block_then_raises_canonical_value() -> (
None
):
provider = _provider()
request = make_messages_request(
"test-model",
messages=[],
max_tokens=32,
)
# Crossing the recovery buffer's byte threshold makes the content block
# downstream-visible before the failure, so its close prelude must escape.
stream = _FailingStream(
[_chunk(content="x" * 65_536)],
RuntimeError("connection lost after commit"),
)
emitted: deque[str] = deque()
with (
patch.object(
provider._client.chat.completions,
"create",
new_callable=AsyncMock,
return_value=stream,
),
pytest.raises(ExecutionFailure) as exc_info,
):
async for event in provider.stream_response(
request,
request_id="req_committed_failure",
):
emitted.append(event)
events = parse_sse_text("".join(emitted))
assert [event.event for event in events][-1] == "content_block_stop"
assert not any(event.event in {"error", "message_stop"} for event in events)
assert exc_info.value.kind is FailureKind.UPSTREAM
assert exc_info.value.status_code == 502
assert exc_info.value.retryable is False
assert "connection lost after commit" in exc_info.value.message
assert "Request ID: req_committed_failure" in exc_info.value.message
@pytest.mark.asyncio
async def test_openai_stream_close_failure_cannot_mask_execution_failure() -> None:
provider = _provider()
request = make_messages_request(
"test-model",
messages=[],
max_tokens=32,
)
stream = _FailingStream(
[],
RuntimeError("original provider failure"),
close_error=RuntimeError("cleanup api_key=SECRET"),
)
with (
patch.object(
provider._client.chat.completions,
"create",
new_callable=AsyncMock,
return_value=stream,
),
patch("free_claude_code.providers.http.trace_event") as trace_event,
pytest.raises(ExecutionFailure) as exc_info,
):
[
event
async for event in provider.stream_response(
request,
request_id="req_close_failure",
)
]
assert stream.close_calls == 1
assert exc_info.value.status_code == 502
assert "original provider failure" in exc_info.value.message
assert "cleanup" not in exc_info.value.message
assert "SECRET" not in exc_info.value.message
trace_event.assert_called_once_with(
stage="provider",
event="provider.stream.close_failed",
source="provider",
provider="NIM",
request_id="req_close_failure",
close_exc_type="RuntimeError",
preserved_exc_type="ExecutionFailure",
)
assert "SECRET" not in repr(trace_event.call_args)
@pytest.mark.asyncio
async def test_stream_close_failure_without_active_error_is_observability_only() -> (
None
):
stream = _FailingStream(
[],
RuntimeError("unused"),
close_error=RuntimeError("normal close failed"),
)
with patch("free_claude_code.providers.http.trace_event") as trace_event:
await close_provider_stream(
stream,
active_error=None,
provider_name="TEST",
request_id="req_normal_close",
)
assert stream.close_calls == 1
trace_event.assert_called_once_with(
stage="provider",
event="provider.stream.close_failed",
source="provider",
provider="TEST",
request_id="req_normal_close",
close_exc_type="RuntimeError",
preserved_exc_type=None,
)
@pytest.mark.asyncio
async def test_completed_stream_close_failure_preserves_success_lifecycle() -> None:
provider = _provider()
request = make_messages_request(
"test-model",
messages=[],
max_tokens=32,
)
stream = _FailingStream(
[_chunk(content="complete", finish_reason="stop")],
None,
close_error=RuntimeError("cleanup api_key=SECRET"),
)
with (
patch.object(
provider._client.chat.completions,
"create",
new_callable=AsyncMock,
return_value=stream,
),
patch("free_claude_code.providers.http.trace_event") as trace_event,
):
emitted = [
event
async for event in provider.stream_response(
request,
request_id="req_successful_close_failure",
)
]
events = parse_sse_text("".join(emitted))
assert events[-1].event == "message_stop"
assert sum(event.event == "message_stop" for event in events) == 1
assert not any(event.event == "error" for event in events)
assert stream.close_calls == 1
trace_event.assert_called_once_with(
stage="provider",
event="provider.stream.close_failed",
source="provider",
provider="NIM",
request_id="req_successful_close_failure",
close_exc_type="RuntimeError",
preserved_exc_type=None,
)
assert "SECRET" not in repr(trace_event.call_args)
@pytest.mark.asyncio
async def test_closing_public_openai_stream_closes_raw_stream_once() -> None:
provider = _provider()
request = make_messages_request(
"test-model",
messages=[],
max_tokens=32,
)
raw_stream = _FailingStream(
[
_chunk(content="x" * 65_536),
_chunk(content="done", finish_reason="stop"),
],
None,
)
with patch.object(
provider._client.chat.completions,
"create",
new_callable=AsyncMock,
return_value=raw_stream,
):
stream = provider.stream_response(request, request_id="req_early_close")
await anext(stream)
assert isinstance(stream, AsyncCloseable)
await stream.aclose()
assert raw_stream.close_calls == 1