mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-13 03:32:21 +02:00
209 lines
6.2 KiB
Python
209 lines
6.2 KiB
Python
import asyncio
|
|
import re
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from src.agent_tools.subprocess_tools import (
|
|
_host_shell_requires_detach,
|
|
_host_shell_should_auto_poll,
|
|
)
|
|
from src.agent_loop import _host_bridge_failure_response, _is_host_bridge_failure_result
|
|
from src.tool_execution import format_tool_result
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, payload: dict):
|
|
self.status_code = 200
|
|
self._payload = payload
|
|
|
|
def json(self):
|
|
return self._payload
|
|
|
|
|
|
class _FakeAsyncClient:
|
|
requests: list[dict] = []
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self._responses = iter([
|
|
{"status": "running", "detached": True, "job_id": "job-1", "output": "started"},
|
|
{"status": "completed", "job_id": "job-1", "output": "finished", "exit_code": 0},
|
|
])
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return None
|
|
|
|
async def post(self, url, *, json, headers):
|
|
self.requests.append({"url": url, "json": json, "headers": headers})
|
|
return _FakeResponse(next(self._responses))
|
|
|
|
|
|
def test_explicit_background_marker_requires_detach():
|
|
assert _host_shell_requires_detach("#!bg\npip install package")
|
|
|
|
|
|
def test_long_sleep_requires_detach_when_model_omits_flag():
|
|
assert _host_shell_requires_detach("sleep 22; printf done")
|
|
assert not _host_shell_requires_detach("sleep 3; printf done")
|
|
|
|
|
|
def test_short_commands_remain_synchronous():
|
|
assert not _host_shell_requires_detach("ip route")
|
|
|
|
|
|
def test_only_implicit_long_jobs_are_auto_polled():
|
|
assert _host_shell_should_auto_poll("sleep 22; printf done")
|
|
assert not _host_shell_should_auto_poll("#!bg\nsleep 22")
|
|
|
|
|
|
def test_detached_result_requires_polling_the_returned_job_id():
|
|
rendered = format_tool_result(
|
|
"host_shell: sleep",
|
|
{"output": "started", "exit_code": 0, "detached": True, "status": "running", "job_id": "job-1"},
|
|
)
|
|
assert "POLL REQUIRED" in rendered
|
|
assert '"job_id":"job-1"' in rendered
|
|
assert "substitute command" in rendered
|
|
|
|
|
|
def test_bridge_transport_failure_is_terminal_for_the_turn():
|
|
assert _is_host_bridge_failure_result(
|
|
{"error": "host_shell: bridge call failed: connection refused", "exit_code": 1}
|
|
)
|
|
assert not _is_host_bridge_failure_result(
|
|
{"output": "bridge check completed", "exit_code": 0}
|
|
)
|
|
|
|
|
|
def test_bridge_transport_failure_response_is_concise_and_actionable():
|
|
response = _host_bridge_failure_response()
|
|
assert "unavailable" in response
|
|
assert "Restart or reconnect" in response
|
|
assert len(response) < 180
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_host_shell_auto_polls_implicit_long_job(monkeypatch):
|
|
from src.agent_tools import subprocess_tools
|
|
|
|
_FakeAsyncClient.requests = []
|
|
monkeypatch.setattr(subprocess_tools.httpx, "AsyncClient", _FakeAsyncClient)
|
|
result = await subprocess_tools.HostShellTool().execute(
|
|
'{"command":"sleep 22; printf done"}',
|
|
{
|
|
"client_runtime_context": {
|
|
"host_shell_bridge": {
|
|
"url": "http://127.0.0.1:17654/run",
|
|
"token": "bridge-token",
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
assert result["output"] == "finished"
|
|
assert result["status"] == "completed"
|
|
assert [item["json"] for item in _FakeAsyncClient.requests] == [
|
|
{"command": "sleep 22; printf done", "timeout": 30, "detach": True},
|
|
{"job_id": "job-1"},
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
("payload", "expected_error"),
|
|
[
|
|
({"error": "request rejected", "exit_code": 1}, "request rejected"),
|
|
({"output": "looks fine", "exit_code": "zero"}, "invalid exit_code"),
|
|
({"output": "looks fine", "exit_code": False}, "invalid exit_code"),
|
|
],
|
|
)
|
|
async def test_host_shell_turns_malformed_bridge_results_into_tool_failures(
|
|
monkeypatch, payload, expected_error,
|
|
):
|
|
from src.agent_tools import subprocess_tools
|
|
|
|
class Client:
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return None
|
|
|
|
async def post(self, *_args, **_kwargs):
|
|
return _FakeResponse(payload)
|
|
|
|
monkeypatch.setattr(subprocess_tools.httpx, "AsyncClient", Client)
|
|
result = await subprocess_tools.HostShellTool().execute(
|
|
'{"command":"printf ready"}',
|
|
{
|
|
"client_runtime_context": {
|
|
"host_shell_bridge": {
|
|
"url": "http://127.0.0.1:17654/run",
|
|
"token": "bridge-token",
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
assert expected_error in result["error"]
|
|
assert result["exit_code"] == 1
|
|
assert result["host_bridge"] == "tui"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelled_canonical_host_shell_notifies_tui_bridge(monkeypatch):
|
|
from src.agent_tools import subprocess_tools
|
|
|
|
entered = asyncio.Event()
|
|
captured = {}
|
|
|
|
class BlockingClient:
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return None
|
|
|
|
async def post(self, _url, *, json, headers):
|
|
captured.update(json)
|
|
entered.set()
|
|
await asyncio.Future()
|
|
|
|
cancel = AsyncMock()
|
|
monkeypatch.setattr(subprocess_tools.httpx, "AsyncClient", BlockingClient)
|
|
monkeypatch.setattr(
|
|
subprocess_tools, "_cancel_host_shell_bridge_request", cancel,
|
|
)
|
|
task = asyncio.create_task(subprocess_tools.HostShellTool().execute(
|
|
'{"command":"printf ready"}',
|
|
{
|
|
"client_runtime_context": {
|
|
"host_shell_bridge": {
|
|
"url": "http://127.0.0.1:17654/run",
|
|
"token": "bridge-token",
|
|
}
|
|
}
|
|
},
|
|
))
|
|
await entered.wait()
|
|
task.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await task
|
|
await asyncio.sleep(0)
|
|
|
|
assert re.fullmatch(r"[A-Za-z0-9_-]+", captured["request_id"])
|
|
cancel.assert_awaited_once_with(
|
|
"http://127.0.0.1:17654/run",
|
|
"bridge-token",
|
|
captured["request_id"],
|
|
)
|