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
- 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.
128 lines
3.9 KiB
Python
128 lines
3.9 KiB
Python
from EvoScientist.llm.errors import AgentControlError
|
|
from EvoScientist.middleware.model_fallback import _is_non_fallbackable
|
|
import uuid
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from EvoScientist.middleware import recoverable_metering
|
|
from EvoScientist.middleware.recoverable_metering import _metering_config, _source_type
|
|
|
|
|
|
def test_agent_control_error_is_non_fallbackable():
|
|
error = AgentControlError(
|
|
"INSUFFICIENT_BALANCE",
|
|
"balance unavailable",
|
|
status_code=403,
|
|
)
|
|
|
|
assert "platform control error" in (_is_non_fallbackable(error) or "")
|
|
assert error.model_dump()["code"] == "INSUFFICIENT_BALANCE"
|
|
|
|
|
|
def test_recoverable_metering_reads_explicit_evomemory_scope():
|
|
metering = _metering_config(
|
|
{
|
|
"configurable": {
|
|
"ai4sci_metering": {
|
|
"gateway_url": "http://gateway",
|
|
"run_id": "run-parent",
|
|
"envelope_signature": "signed-parent",
|
|
"source_type": "evomemory_linker",
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
assert metering is not None
|
|
assert metering["source_type"] == "evomemory_linker"
|
|
assert _source_type({"metering_scope": "evomemory_subagent_worker"}, []) == (
|
|
"evomemory_subagent_worker"
|
|
)
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_recoverable_metering_sends_internal_service_identity(monkeypatch):
|
|
captured = {}
|
|
|
|
class Response:
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
class Client:
|
|
async def post(self, url, *, json, headers):
|
|
captured.update(url=url, json=json, headers=headers)
|
|
return Response()
|
|
|
|
monkeypatch.setenv("AI4SCI_EVO_RUNTIME_GRANT_SECRET", "runtime-service-secret")
|
|
monkeypatch.delenv("EVOSCIENTIST_BACKEND_SERVICE_TOKEN", raising=False)
|
|
monkeypatch.setattr(recoverable_metering, "_clients", {"http://gateway": Client()})
|
|
|
|
await recoverable_metering._post(
|
|
{
|
|
"gateway_url": "http://gateway",
|
|
"run_id": "run-1",
|
|
"envelope_signature": "signature",
|
|
},
|
|
"start",
|
|
{},
|
|
)
|
|
|
|
assert captured["headers"] == {
|
|
"X-Ai4Sci-Service-Token": "runtime-service-secret"
|
|
}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_recoverable_metering_reports_gateway_identity_failure(monkeypatch):
|
|
class Client:
|
|
async def post(self, url, *, json, headers):
|
|
return httpx.Response(401, request=httpx.Request("POST", url))
|
|
|
|
monkeypatch.setattr(recoverable_metering, "_clients", {"http://gateway": Client()})
|
|
|
|
with pytest.raises(AgentControlError) as exc_info:
|
|
await recoverable_metering._post(
|
|
{
|
|
"gateway_url": "http://gateway",
|
|
"run_id": "run-1",
|
|
"envelope_signature": "signature",
|
|
},
|
|
"start",
|
|
{},
|
|
)
|
|
|
|
assert exc_info.value.code == "BILLING_UNAVAILABLE"
|
|
|
|
|
|
async def _fake_sleep(_delay: float) -> None:
|
|
return None
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_terminal_metering_preserves_billing_unavailable_and_skips_retry_on_401(monkeypatch):
|
|
calls = {"count": 0}
|
|
|
|
class Client:
|
|
async def post(self, url, *, json, headers):
|
|
calls["count"] += 1
|
|
return httpx.Response(401, request=httpx.Request("POST", url))
|
|
|
|
monkeypatch.setattr(recoverable_metering, "_clients", {"http://gateway": Client()})
|
|
monkeypatch.setattr(recoverable_metering.asyncio, "sleep", _fake_sleep)
|
|
|
|
callback = recoverable_metering.RecoverableMeteringCallback(
|
|
{
|
|
"gateway_url": "http://gateway",
|
|
"run_id": "run-1",
|
|
"envelope_signature": "signature",
|
|
}
|
|
)
|
|
callback._attempts.add(uuid.UUID(int=1))
|
|
|
|
with pytest.raises(AgentControlError) as exc_info:
|
|
await callback._terminal(uuid.UUID(int=1), "succeeded", None)
|
|
|
|
assert exc_info.value.code == "BILLING_UNAVAILABLE"
|
|
assert calls["count"] == 1
|