mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-13 03:32:21 +02:00
260 lines
7.1 KiB
Python
260 lines
7.1 KiB
Python
import asyncio
|
|
import re
|
|
import httpx as real_httpx
|
|
import pytest
|
|
|
|
import src.tool_execution as te
|
|
|
|
|
|
def test_tui_host_bridge_patch_url_replaces_run_endpoint():
|
|
assert te._tui_host_bridge_patch_url({
|
|
"surface": "odysseus-tui",
|
|
"host_shell_bridge": {
|
|
"url": "http://host.docker.internal:17654/run",
|
|
"token": "secret",
|
|
},
|
|
}) == ("http://host.docker.internal:17654/patch", "secret")
|
|
|
|
|
|
def test_tui_host_bridge_patch_requires_authenticated_bridge():
|
|
assert te._tui_host_bridge_patch_url({
|
|
"surface": "odysseus-tui",
|
|
"host_shell_bridge": {"url": "http://localhost:1234/run"},
|
|
}) is None
|
|
|
|
|
|
def test_apply_patch_via_tui_host_bridge_posts_patch_and_preserves_result(monkeypatch):
|
|
seen = {}
|
|
|
|
class Response:
|
|
status_code = 200
|
|
|
|
def json(self):
|
|
return {"output": "patched", "exit_code": 0, "files": ["app.py"]}
|
|
|
|
class Client:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return None
|
|
|
|
async def post(self, url, **kwargs):
|
|
seen.update(url=url, kwargs=kwargs)
|
|
return Response()
|
|
|
|
class Httpx:
|
|
Timeout = real_httpx.Timeout
|
|
AsyncClient = lambda self, **_kwargs: Client()
|
|
|
|
monkeypatch.setitem(__import__("sys").modules, "httpx", Httpx())
|
|
result = asyncio.run(te._apply_patch_via_tui_host_bridge(
|
|
"*** Begin Patch\n*** End Patch",
|
|
{
|
|
"surface": "odysseus-tui",
|
|
"host_shell_bridge": {"url": "http://127.0.0.1:17654/run", "token": "secret"},
|
|
},
|
|
))
|
|
assert result["exit_code"] == 0
|
|
assert seen["url"] == "http://127.0.0.1:17654/patch"
|
|
assert seen["kwargs"]["headers"]["x-odysseus-tui-bridge-token"] == "secret"
|
|
assert re.fullmatch(
|
|
r"[A-Za-z0-9_-]+", seen["kwargs"]["json"]["request_id"]
|
|
)
|
|
|
|
|
|
def test_apply_patch_via_tui_host_bridge_rejects_malformed_exit_code(monkeypatch):
|
|
from src import tool_execution as te
|
|
|
|
class Response:
|
|
status_code = 200
|
|
|
|
def json(self):
|
|
return {"output": "patched", "exit_code": "zero"}
|
|
|
|
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 Response()
|
|
|
|
import httpx
|
|
|
|
monkeypatch.setattr(httpx, "AsyncClient", Client)
|
|
result = asyncio.run(te._apply_patch_via_tui_host_bridge(
|
|
"*** Begin Patch\n*** End Patch",
|
|
{
|
|
"surface": "odysseus-tui",
|
|
"host_shell_bridge": {
|
|
"url": "http://127.0.0.1:17654/run",
|
|
"token": "secret",
|
|
},
|
|
},
|
|
))
|
|
|
|
assert result == {
|
|
"error": "apply_patch: host bridge returned an invalid exit_code",
|
|
"exit_code": 1,
|
|
}
|
|
|
|
|
|
def test_cancelled_apply_patch_notifies_tui_host_bridge(monkeypatch):
|
|
posted = asyncio.Event()
|
|
seen = {}
|
|
|
|
class Client:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return None
|
|
|
|
async def post(self, _url, **kwargs):
|
|
seen["request_id"] = kwargs["json"]["request_id"]
|
|
posted.set()
|
|
await asyncio.Future()
|
|
|
|
class Httpx:
|
|
Timeout = real_httpx.Timeout
|
|
AsyncClient = lambda self, **_kwargs: Client()
|
|
|
|
cancelled = []
|
|
|
|
async def fake_cancel(_bridge, request_id):
|
|
cancelled.append(request_id)
|
|
|
|
monkeypatch.setitem(__import__("sys").modules, "httpx", Httpx())
|
|
monkeypatch.setattr(te, "_cancel_bridge_request", fake_cancel)
|
|
|
|
async def scenario():
|
|
task = asyncio.create_task(te._apply_patch_via_tui_host_bridge(
|
|
"*** Begin Patch\n*** End Patch",
|
|
{
|
|
"surface": "odysseus-tui",
|
|
"host_shell_bridge": {
|
|
"url": "http://127.0.0.1:17654/run", "token": "secret",
|
|
},
|
|
},
|
|
))
|
|
await posted.wait()
|
|
task.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await task
|
|
await asyncio.sleep(0)
|
|
|
|
asyncio.run(scenario())
|
|
assert cancelled == [seen["request_id"]]
|
|
|
|
|
|
def test_apply_patch_via_tui_host_bridge_rejects_invalid_success_payload(monkeypatch):
|
|
class Response:
|
|
status_code = 200
|
|
|
|
def json(self):
|
|
raise ValueError("not json")
|
|
|
|
class Client:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return None
|
|
|
|
async def post(self, *_args, **_kwargs):
|
|
return Response()
|
|
|
|
class Httpx:
|
|
Timeout = real_httpx.Timeout
|
|
AsyncClient = lambda self, **_kwargs: Client()
|
|
|
|
monkeypatch.setitem(__import__("sys").modules, "httpx", Httpx())
|
|
result = asyncio.run(te._apply_patch_via_tui_host_bridge(
|
|
"*** Begin Patch\n*** End Patch",
|
|
{
|
|
"surface": "odysseus-tui",
|
|
"host_shell_bridge": {"url": "http://127.0.0.1:17654/run", "token": "secret"},
|
|
},
|
|
))
|
|
|
|
assert result == {
|
|
"error": "apply_patch: host bridge returned an invalid payload",
|
|
"exit_code": 1,
|
|
}
|
|
|
|
|
|
def test_apply_patch_via_tui_host_bridge_infers_failure_from_error_payload(monkeypatch):
|
|
class Response:
|
|
status_code = 200
|
|
|
|
def json(self):
|
|
return {"error": "apply_patch: context mismatch"}
|
|
|
|
class Client:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return None
|
|
|
|
async def post(self, *_args, **_kwargs):
|
|
return Response()
|
|
|
|
class Httpx:
|
|
Timeout = real_httpx.Timeout
|
|
AsyncClient = lambda self, **_kwargs: Client()
|
|
|
|
monkeypatch.setitem(__import__("sys").modules, "httpx", Httpx())
|
|
result = asyncio.run(te._apply_patch_via_tui_host_bridge(
|
|
"*** Begin Patch\n*** End Patch",
|
|
{
|
|
"surface": "odysseus-tui",
|
|
"host_shell_bridge": {"url": "http://127.0.0.1:17654/run", "token": "secret"},
|
|
},
|
|
))
|
|
|
|
assert result["exit_code"] == 1
|
|
assert "context mismatch" in result["error"]
|
|
|
|
|
|
def test_apply_patch_bridge_error_overrides_false_success_code(monkeypatch):
|
|
class Response:
|
|
status_code = 500
|
|
|
|
def json(self):
|
|
return {"error": "host failed", "exit_code": 0}
|
|
|
|
class Client:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return None
|
|
|
|
async def post(self, *_args, **_kwargs):
|
|
return Response()
|
|
|
|
class Httpx:
|
|
Timeout = real_httpx.Timeout
|
|
AsyncClient = lambda self, **_kwargs: Client()
|
|
|
|
monkeypatch.setitem(__import__("sys").modules, "httpx", Httpx())
|
|
result = asyncio.run(te._apply_patch_via_tui_host_bridge(
|
|
"*** Begin Patch\n*** End Patch",
|
|
{
|
|
"surface": "odysseus-tui",
|
|
"host_shell_bridge": {
|
|
"url": "http://127.0.0.1:17654/run",
|
|
"token": "secret",
|
|
},
|
|
},
|
|
))
|
|
|
|
assert result == {"error": "host failed", "exit_code": 1}
|