mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 12:43:16 +00:00
152 lines
5.2 KiB
Python
152 lines
5.2 KiB
Python
|
|
"""Client-side execution deadline for the LangSmith sandbox backend."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import time
|
||
|
|
from types import SimpleNamespace
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from langsmith.sandbox import CommandTimeoutError, SandboxConnectionError
|
||
|
|
|
||
|
|
from agent.integrations.langsmith import TimeoutLangSmithSandbox
|
||
|
|
|
||
|
|
|
||
|
|
class _FakeHandle:
|
||
|
|
def __init__(self, *, sleep: float = 0.0, result: Any = None, raises: Exception | None = None):
|
||
|
|
self._sleep = sleep
|
||
|
|
self._result = result
|
||
|
|
self._raises = raises
|
||
|
|
self.killed = False
|
||
|
|
|
||
|
|
@property
|
||
|
|
def result(self) -> Any:
|
||
|
|
if self._sleep:
|
||
|
|
time.sleep(self._sleep)
|
||
|
|
if self._raises is not None:
|
||
|
|
raise self._raises
|
||
|
|
return self._result
|
||
|
|
|
||
|
|
def kill(self) -> None:
|
||
|
|
self.killed = True
|
||
|
|
|
||
|
|
|
||
|
|
class _FakeSandbox:
|
||
|
|
def __init__(self, handle: _FakeHandle, *, run_raises: Exception | None = None):
|
||
|
|
self._handle = handle
|
||
|
|
self._run_raises = run_raises
|
||
|
|
self.run_calls: list[dict[str, Any]] = []
|
||
|
|
|
||
|
|
def run(self, command: str, *, timeout: int, wait: bool) -> _FakeHandle:
|
||
|
|
self.run_calls.append({"command": command, "timeout": timeout, "wait": wait})
|
||
|
|
if self._run_raises is not None:
|
||
|
|
raise self._run_raises
|
||
|
|
return self._handle
|
||
|
|
|
||
|
|
|
||
|
|
def _backend(
|
||
|
|
handle: _FakeHandle, *, run_raises: Exception | None = None
|
||
|
|
) -> TimeoutLangSmithSandbox:
|
||
|
|
sb = TimeoutLangSmithSandbox.__new__(TimeoutLangSmithSandbox)
|
||
|
|
sb._sandbox = _FakeSandbox(handle, run_raises=run_raises)
|
||
|
|
sb._default_timeout = 30 * 60
|
||
|
|
return sb
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture(autouse=True)
|
||
|
|
def _no_grace(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
monkeypatch.setenv("SANDBOX_EXECUTE_CLIENT_GRACE_SECONDS", "0")
|
||
|
|
|
||
|
|
|
||
|
|
async def test_aexecute_kills_on_client_timeout() -> None:
|
||
|
|
handle = _FakeHandle(sleep=5.0)
|
||
|
|
sb = _backend(handle)
|
||
|
|
start = time.monotonic()
|
||
|
|
resp = await sb.aexecute("sleep 999", timeout=1)
|
||
|
|
assert time.monotonic() - start < 3.0
|
||
|
|
assert resp.exit_code == 124
|
||
|
|
assert "killed" in resp.output
|
||
|
|
assert handle.killed
|
||
|
|
assert sb._sandbox.run_calls[0]["wait"] is False
|
||
|
|
|
||
|
|
|
||
|
|
async def test_aexecute_success_combines_streams() -> None:
|
||
|
|
handle = _FakeHandle(result=SimpleNamespace(stdout="out", stderr="err", exit_code=0))
|
||
|
|
sb = _backend(handle)
|
||
|
|
resp = await sb.aexecute("echo hi", timeout=5)
|
||
|
|
assert resp.exit_code == 0
|
||
|
|
assert resp.output == "out\nerr"
|
||
|
|
assert not handle.killed
|
||
|
|
|
||
|
|
|
||
|
|
async def test_aexecute_server_timeout_not_killed() -> None:
|
||
|
|
handle = _FakeHandle(raises=CommandTimeoutError("server enforced"))
|
||
|
|
sb = _backend(handle)
|
||
|
|
resp = await sb.aexecute("make hang", timeout=2)
|
||
|
|
assert resp.exit_code == 124
|
||
|
|
assert "on the sandbox" in resp.output
|
||
|
|
assert not handle.killed
|
||
|
|
|
||
|
|
|
||
|
|
def test_execute_kills_on_client_timeout() -> None:
|
||
|
|
handle = _FakeHandle(sleep=5.0)
|
||
|
|
sb = _backend(handle)
|
||
|
|
start = time.monotonic()
|
||
|
|
resp = sb.execute("sleep 999", timeout=1)
|
||
|
|
assert time.monotonic() - start < 3.0
|
||
|
|
assert resp.exit_code == 124
|
||
|
|
assert handle.killed
|
||
|
|
|
||
|
|
|
||
|
|
def _patch_base_execute(monkeypatch: pytest.MonkeyPatch, sink: dict[str, Any]) -> None:
|
||
|
|
def fake_base_execute(self: Any, command: str, *, timeout: int | None = None) -> Any:
|
||
|
|
sink["command"] = command
|
||
|
|
sink["timeout"] = timeout
|
||
|
|
return SimpleNamespace(output="via-http", exit_code=0, truncated=False)
|
||
|
|
|
||
|
|
monkeypatch.setattr("agent.integrations.langsmith.LangSmithSandbox.execute", fake_base_execute)
|
||
|
|
|
||
|
|
|
||
|
|
async def test_aexecute_ws_connect_failure_falls_back_to_base(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
# run(wait=False) connects eagerly, so a connect failure raises from run().
|
||
|
|
sb = _backend(_FakeHandle(), run_raises=SandboxConnectionError("no ws"))
|
||
|
|
called: dict[str, Any] = {}
|
||
|
|
_patch_base_execute(monkeypatch, called)
|
||
|
|
resp = await sb.aexecute("git status", timeout=5)
|
||
|
|
assert called == {"command": "git status", "timeout": 5}
|
||
|
|
assert resp.output == "via-http"
|
||
|
|
|
||
|
|
|
||
|
|
async def test_aexecute_ws_connect_timeout_falls_back_to_base(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
sb = _backend(_FakeHandle(), run_raises=TimeoutError("connect timed out"))
|
||
|
|
called: dict[str, Any] = {}
|
||
|
|
_patch_base_execute(monkeypatch, called)
|
||
|
|
resp = await sb.aexecute("git status", timeout=5)
|
||
|
|
assert called["command"] == "git status"
|
||
|
|
assert resp.output == "via-http"
|
||
|
|
|
||
|
|
|
||
|
|
def test_execute_ws_connect_failure_falls_back_to_base(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
sb = _backend(_FakeHandle(), run_raises=SandboxConnectionError("no ws"))
|
||
|
|
called: dict[str, Any] = {}
|
||
|
|
_patch_base_execute(monkeypatch, called)
|
||
|
|
resp = sb.execute("git status", timeout=5)
|
||
|
|
assert called == {"command": "git status", "timeout": 5}
|
||
|
|
assert resp.output == "via-http"
|
||
|
|
|
||
|
|
|
||
|
|
async def test_aexecute_midstream_ws_drop_falls_back_to_base(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
# Connect succeeds (run returns a handle) but the stream drops while draining.
|
||
|
|
sb = _backend(_FakeHandle(raises=SandboxConnectionError("dropped")))
|
||
|
|
called: dict[str, Any] = {}
|
||
|
|
_patch_base_execute(monkeypatch, called)
|
||
|
|
resp = await sb.aexecute("git status", timeout=5)
|
||
|
|
assert called["command"] == "git status"
|
||
|
|
assert resp.output == "via-http"
|