Files
EvoScientist-Multi/tests/test_gateway_proxy.py
T
m4 561e161123
Build / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled
fix: fail-closed internal identity and preserve billing error semantics
- Gateway internal identity: when a service token is configured, reject
  wrong/missing tokens even from loopback (closes SSRF/local bypass).
- Terminal metering: classified AgentControlError propagates without
  retry; exhausted retries raise BILLING_UNAVAILABLE instead of a
  generic RuntimeError, keeping error attribution accurate.
2026-09-03 18:45:31 +08:00

154 lines
4.6 KiB
Python

import json
import httpx
import pytest
from langchain_core.messages import HumanMessage
from EvoScientist.llm.contracts import EvoRuntimeError
from EvoScientist.llm.gateway_proxy import GatewayProxyChatModel
class _FakeStream:
def __init__(self, lines):
self._lines = lines
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
def raise_for_status(self):
pass
async def aiter_lines(self):
for line in self._lines:
yield line
class _FakeClient:
def __init__(self, lines):
self._lines = lines
self.sent = None
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
def stream(self, method, url, json=None, headers=None):
assert method == "POST"
assert url.endswith("/api/internal/recoverable-runs/model/stream")
self.sent = json
self.headers = headers
return _FakeStream(self._lines)
def test_runtime_error_repr_preserves_only_stable_code():
error = EvoRuntimeError(
"UPSTREAM_RATE_LIMITED",
"safe display message",
details=({"provider_request": "must-not-persist"},),
)
assert repr(error) == "EvoRuntimeError(code='UPSTREAM_RATE_LIMITED')"
assert "safe display message" not in repr(error)
assert "must-not-persist" not in repr(error)
@pytest.mark.anyio
async def test_astream_yields_chunks_from_sse(monkeypatch):
monkeypatch.setenv("AI4SCI_EVO_RUNTIME_GRANT_SECRET", "runtime-service-secret")
model = GatewayProxyChatModel(
gateway_url="http://gw",
run_id="run-1",
envelope_signature="sig",
)
msg = {"type": "AIMessageChunk", "data": {"content": "hello"}}
lines = [
f"data: {json.dumps({'delta': {'message': msg}})}\n",
'data: {"delta": {"message": {"type": "AIMessageChunk", "data": {"content": " world"}}}}\n',
"data: [DONE]\n",
]
fake = _FakeClient(lines)
monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: fake)
chunks = [c async for c in model._astream([HumanMessage(content="hi")])]
assert fake.sent["stream"] is True
assert fake.headers == {"X-Ai4Sci-Service-Token": "runtime-service-secret"}
assert len(chunks) == 2
assert chunks[0].message.content == "hello"
@pytest.mark.anyio
async def test_astream_roundtrips_streaming_tool_call_chunks(monkeypatch):
model = GatewayProxyChatModel(
gateway_url="http://gw",
run_id="run-1",
envelope_signature="sig",
)
msg = {
"type": "AIMessageChunk",
"data": {
"content": "",
"tool_call_chunks": [
{
"name": "read_file",
"args": '{"path":',
"id": "call-1",
"index": 0,
"type": "tool_call_chunk",
}
],
},
}
lines = [f"data: {json.dumps({'delta': {'message': msg}})}\n", "data: [DONE]\n"]
fake = _FakeClient(lines)
monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: fake)
chunks = [c async for c in model._astream([HumanMessage(content="hi")])]
assert len(chunks) == 1
assert chunks[0].message.tool_call_chunks[0]["name"] == "read_file"
assert chunks[0].message.tool_call_chunks[0]["id"] == "call-1"
@pytest.mark.anyio
async def test_astream_raises_on_missing_done(monkeypatch):
model = GatewayProxyChatModel(
gateway_url="http://gw",
run_id="run-1",
envelope_signature="sig",
)
lines = [
'data: {"delta": {"message": {"type": "AIMessageChunk", "data": {"content": "hi"}}}}\n'
]
fake = _FakeClient(lines)
monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: fake)
with pytest.raises(RuntimeError, match="AI4SCI_MODEL_STREAM_INCOMPLETE"):
_ = [c async for c in model._astream([HumanMessage(content="hi")])]
@pytest.mark.anyio
async def test_astream_projects_gateway_error_frame(monkeypatch):
model = GatewayProxyChatModel(
gateway_url="http://gw",
run_id="run-1",
envelope_signature="sig",
)
lines = [
'data: {"type":"error","code":"UPSTREAM_RATE_LIMITED",'
'"status":429,"message":"模型服务请求频率超限,请稍后重试或切换模型。"}\n'
]
fake = _FakeClient(lines)
monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: fake)
with pytest.raises(EvoRuntimeError) as exc_info:
_ = [c async for c in model._astream([HumanMessage(content="hi")])]
assert exc_info.value.code == "UPSTREAM_RATE_LIMITED"
assert exc_info.value.details == ({"http_status": 429},)