mirror of
https://github.com/usestrix/strix.git
synced 2026-10-26 06:02:30 +00:00
Tool requests carrying an explicit `timeout` kwarg (terminal_execute, python_action, etc.) were capped at the sandbox hard timeout (default 120s) regardless of the tool-level value, producing a "Tool timed out after 120s" error on legitimate long-running scans. - Resolve the wait_for envelope per-request: if the tool kwarg sets a timeout larger than the default cap, grow the envelope to `tool_timeout + 30s` so the tool's own timer runs to completion plus a small buffer for sandbox overhead. Otherwise keep the sandbox default as the hard cap. - Make the timeout error actionable — mention STRIX_SANDBOX_EXECUTION_TIMEOUT and the per-call timeout kwarg so users can self-discover the knob. Closes #436
120 lines
4.8 KiB
Python
120 lines
4.8 KiB
Python
"""Unit + functional tests for request-timeout resolution in the sandbox tool server.
|
|
|
|
Regression coverage for #436 — ``terminal_execute`` timing out at the sandbox
|
|
hard cap even when the caller passed a longer explicit ``timeout`` kwarg.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import pytest
|
|
from fastapi.security import HTTPAuthorizationCredentials
|
|
|
|
from strix.tools import registry
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from types import ModuleType
|
|
|
|
|
|
TOOL_SERVER_PATH = Path(__file__).resolve().parents[2] / "strix" / "runtime" / "tool_server.py"
|
|
|
|
|
|
@pytest.fixture
|
|
def tool_server(monkeypatch: pytest.MonkeyPatch) -> ModuleType:
|
|
"""Load ``strix.runtime.tool_server`` under its import-time guards.
|
|
|
|
The module requires ``STRIX_SANDBOX_MODE=true`` and reads ``sys.argv`` at
|
|
import time. We load it under a unique module name via ``importlib.util``
|
|
and register it with ``monkeypatch.setitem`` so the entry is torn down
|
|
between tests — keeping sibling tests clean.
|
|
"""
|
|
|
|
monkeypatch.setenv("STRIX_SANDBOX_MODE", "true")
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
["tool_server", "--token", "test-token", "--port", "0"],
|
|
)
|
|
|
|
spec = importlib.util.spec_from_file_location(
|
|
"strix.runtime._tool_server_under_test", TOOL_SERVER_PATH
|
|
)
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
monkeypatch.setitem(sys.modules, spec.name, module)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class TestResolveRequestTimeout:
|
|
def test_no_timeout_kwarg_uses_default(self, tool_server: ModuleType) -> None:
|
|
assert tool_server._resolve_request_timeout(120, {}) == 120
|
|
assert tool_server._resolve_request_timeout(120, {"command": "ls"}) == 120
|
|
|
|
def test_small_tool_timeout_keeps_default_hard_cap(self, tool_server: ModuleType) -> None:
|
|
# Inner tool asks for 10s; sandbox default (120) still dominates.
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": 10}) == 120
|
|
|
|
def test_large_tool_timeout_grows_envelope_by_buffer(self, tool_server: ModuleType) -> None:
|
|
# The actual #436 case: agent passes a 600s timeout, sandbox should grow
|
|
# the wait_for envelope to 600s + buffer instead of killing at 120.
|
|
buffer = tool_server._TOOL_TIMEOUT_BUFFER_SECONDS
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": 600}) == 600 + buffer
|
|
|
|
def test_float_timeout_is_accepted(self, tool_server: ModuleType) -> None:
|
|
buffer = tool_server._TOOL_TIMEOUT_BUFFER_SECONDS
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": 300.0}) == 300 + buffer
|
|
|
|
def test_zero_or_negative_timeout_falls_back_to_default(self, tool_server: ModuleType) -> None:
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": 0}) == 120
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": -1}) == 120
|
|
|
|
def test_bool_is_not_treated_as_timeout(self, tool_server: ModuleType) -> None:
|
|
# ``bool`` is a subclass of ``int``; guard against ``True`` being read
|
|
# as ``1 second`` (or ``False`` disabling the envelope).
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": True}) == 120
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": False}) == 120
|
|
|
|
def test_non_numeric_timeout_is_ignored(self, tool_server: ModuleType) -> None:
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": None}) == 120
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": "forever"}) == 120
|
|
assert tool_server._resolve_request_timeout(120, {"timeout": ["a"]}) == 120
|
|
|
|
|
|
async def test_timeout_error_message_is_actionable(
|
|
tool_server: ModuleType, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""When a tool exceeds the effective envelope, the response error mentions
|
|
``STRIX_SANDBOX_EXECUTION_TIMEOUT`` so issue-reporters like #436 can
|
|
self-discover the configuration knob."""
|
|
|
|
# Shrink the envelope to keep the test fast.
|
|
monkeypatch.setattr(tool_server, "REQUEST_TIMEOUT", 1)
|
|
|
|
def _slow_tool(**_kwargs: Any) -> dict[str, Any]:
|
|
time.sleep(3)
|
|
return {"ok": True}
|
|
|
|
_slow_tool.__name__ = "__slow_test_tool__"
|
|
monkeypatch.setitem(registry._tools_by_name, "__slow_test_tool__", _slow_tool)
|
|
|
|
creds = HTTPAuthorizationCredentials(scheme="Bearer", credentials="test-token")
|
|
request = tool_server.ToolExecutionRequest(
|
|
agent_id="agent-x",
|
|
tool_name="__slow_test_tool__",
|
|
kwargs={},
|
|
)
|
|
|
|
response = await tool_server.execute_tool(request, creds)
|
|
|
|
assert response.result is None
|
|
assert response.error is not None
|
|
assert "timed out after 1s" in response.error
|
|
assert "STRIX_SANDBOX_EXECUTION_TIMEOUT" in response.error
|