Files
forge/apps/api/tests/test_tool_reliability.py
T
nihalashetty ae67bff5a3 feat: deep-agent canvas, live observability, and multi-environment tooling
Self-hosted platform for building, testing, and shipping LangChain/LangGraph agents. Deep-agent sub-agents on the canvas, a live tracing/observability timeline, auto-provisioned built-in tools with import/export, per-environment tool variables, streamed evaluations, and per-user auth token forwarding.
2026-07-28 01:49:19 +05:30

84 lines
3.0 KiB
Python

"""REST tool reliability config: cache, rate_limit, retry."""
from __future__ import annotations
import uuid
import httpx
import pytest
from forge.tools.rest import execute_rest
def _cfg(**extra) -> dict:
return {
"name": f"t_{uuid.uuid4().hex[:8]}",
"kind": "rest_api",
"request": {"method": "GET", "url_template": "https://api.acme.dev/v2/ping", "fields": []},
**extra,
}
async def test_cache_serves_repeat_get_without_second_call():
calls = {"n": 0}
def handler(req):
calls["n"] += 1
return httpx.Response(200, json={"v": calls["n"]})
client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
cfg = _cfg(cache={"ttl_seconds": 60})
a = await execute_rest(cfg, {}, tenant_id="t", project_id="p", client=client)
b = await execute_rest(cfg, {}, tenant_id="t", project_id="p", client=client)
await client.aclose()
assert calls["n"] == 1 and a["raw"] == b["raw"] # second served from cache
async def test_rate_limit_blocks_second_call():
client = httpx.AsyncClient(transport=httpx.MockTransport(lambda r: httpx.Response(200, json={})))
cfg = _cfg(rate_limit={"per_minute": 1})
await execute_rest(cfg, {}, tenant_id="t_rl", project_id="p", client=client)
with pytest.raises(RuntimeError):
await execute_rest(cfg, {}, tenant_id="t_rl", project_id="p", client=client)
await client.aclose()
async def test_retry_recovers_from_transient_500():
state = {"n": 0}
def handler(req):
state["n"] += 1
return httpx.Response(500 if state["n"] == 1 else 200, json={"ok": state["n"]})
client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
cfg = _cfg(retry={"max_retries": 2, "initial_delay": 0.001, "jitter": False, "retry_on": ["http_error"]})
res = await execute_rest(cfg, {}, tenant_id="t", project_id="p", client=client)
await client.aclose()
assert state["n"] == 2 and res["status"] == 200
async def test_no_retry_when_not_configured():
client = httpx.AsyncClient(transport=httpx.MockTransport(lambda r: httpx.Response(500, json={})))
cfg = _cfg() # no retry policy
with pytest.raises(httpx.HTTPStatusError):
await execute_rest(cfg, {}, tenant_id="t", project_id="p", client=client)
await client.aclose()
async def test_missing_path_param_raises_clear_error():
client = httpx.AsyncClient(transport=httpx.MockTransport(lambda r: httpx.Response(200, json={})))
cfg = _cfg()
cfg["request"]["url_template"] = "https://api.acme.dev/v2/orders/{order_id}"
with pytest.raises(ValueError, match="order_id"):
await execute_rest(cfg, {}, tenant_id="t", project_id="p", client=client)
await client.aclose()
def test_cap_payload_truncates_large_only():
from forge.tools.projection import cap_payload
assert cap_payload({"a": 1}, 20000) == {"a": 1} # small passes through unchanged
big = "x" * 50000
out = cap_payload(big, 100)
assert isinstance(out, str) and len(out) < 50000 and "truncated" in out