open-swe/tests/sandbox/test_langsmith_sandbox_timeout.py
Adam Moussa ae1f883b4c
refactor: move tests into tests/<domain>/ layout
Applies the plan's C5 step: git mv every test per the domain-reorg
move-map (movemap-m50.txt) into tests/{agent,analyzer,auth,dashboard,
github,middleware,models,reviewer,sandbox,slack,tools,webhooks}/, plus
the 13 fork-only placements from the scoping report §2c (Atlassian
webhook tests -> tests/webhooks/, test_atlassian_connect.py and
test_auth_error_leak.py -> tests/auth/, jira/confluence util tests ->
tests/tools/, test_repo_binding_isolation.py -> tests/sandbox/,
bot-identity/autofix tests -> tests/github/).

Path-only move: the only content edits are parents[1] -> parents[2]
fixes in test_e2b_integration.py and test_daytona_integration.py,
required because their __file__-relative ROOT path gained one more
directory level in the move.

Monkeypatch retargets for these files were already completed in C4;
none remained outstanding here.
2026-07-17 14:42:45 -04:00

151 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"