mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 17:23:15 +00:00
* feat(dashboard): stream agent chat via @langchain/react v2 protocol Replace the bespoke SSE + React Query polling path with LangGraph’s v2 event stream through credentialed dashboard proxies. Run starts go through stream commands; mid-run follow-ups still queue via /messages. * fix import path * fix tests after rebase * format * PR feedback * improved model fallback * fix image handling * embrace sdk * cleanup * cr * more cleanup * fix cors * harden security --------- Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
229 lines
7.5 KiB
Python
229 lines
7.5 KiB
Python
"""Tests for ToolArtifactMiddleware diff stamping on edit_file/write_file."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from langchain_core.messages import ToolMessage
|
|
|
|
from agent.middleware.tool_artifact import ToolArtifactMiddleware
|
|
from agent.utils import sandbox_state
|
|
|
|
|
|
class FakeReadResult:
|
|
def __init__(
|
|
self, *, content: str | None = None, encoding: str = "utf-8", error: str | None = None
|
|
) -> None:
|
|
self.error = error
|
|
self.file_data = None if content is None else {"content": content, "encoding": encoding}
|
|
|
|
|
|
class FakeBackend:
|
|
def __init__(self, results: dict[str, Any]) -> None:
|
|
self.results = results
|
|
self.reads: list[str] = []
|
|
|
|
def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> Any:
|
|
self.reads.append(file_path)
|
|
result = self.results.get(file_path)
|
|
if isinstance(result, Exception):
|
|
raise result
|
|
if result is None:
|
|
return FakeReadResult(error="File not found")
|
|
return result
|
|
|
|
async def aread(self, file_path: str, offset: int = 0, limit: int = 2000) -> Any:
|
|
return self.read(file_path, offset, limit)
|
|
|
|
|
|
def _request(name: str, args: dict[str, Any], thread_id: str = "t1") -> Any:
|
|
return SimpleNamespace(
|
|
tool_call={"name": name, "args": args, "id": "call-1"},
|
|
runtime=SimpleNamespace(config={"configurable": {"thread_id": thread_id}}),
|
|
)
|
|
|
|
|
|
def _ok(name: str) -> ToolMessage:
|
|
return ToolMessage(content="ok", tool_call_id="call-1", status="success", name=name)
|
|
|
|
|
|
@pytest.fixture
|
|
def register_backend():
|
|
registered: list[str] = []
|
|
|
|
def _register(thread_id: str, backend: Any) -> Any:
|
|
sandbox_state.SANDBOX_BACKENDS[thread_id] = backend
|
|
registered.append(thread_id)
|
|
return backend
|
|
|
|
yield _register
|
|
for thread_id in registered:
|
|
sandbox_state.SANDBOX_BACKENDS.pop(thread_id, None)
|
|
|
|
|
|
async def test_edit_file_stamps_full_file_diff(register_backend) -> None:
|
|
backend = FakeBackend({"/repo/a.py": FakeReadResult(content="line1\nOLD\nline3\n")})
|
|
register_backend("t1", backend)
|
|
request = _request(
|
|
"edit_file", {"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"}
|
|
)
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
return _ok("edit_file")
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact == {
|
|
"diff": {
|
|
"filePath": "/repo/a.py",
|
|
"originalContent": "line1\nOLD\nline3\n",
|
|
"newContent": "line1\nNEW\nline3\n",
|
|
"isNewFile": False,
|
|
}
|
|
}
|
|
assert backend.reads == ["/repo/a.py"]
|
|
|
|
|
|
async def test_edit_file_replace_all(register_backend) -> None:
|
|
backend = FakeBackend({"/repo/a.py": FakeReadResult(content="x x x")})
|
|
register_backend("t1", backend)
|
|
request = _request(
|
|
"edit_file",
|
|
{"file_path": "/repo/a.py", "old_string": "x", "new_string": "y", "replace_all": True},
|
|
)
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
return _ok("edit_file")
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact["diff"]["newContent"] == "y y y"
|
|
|
|
|
|
async def test_edit_file_missing_old_string_skips(register_backend) -> None:
|
|
backend = FakeBackend({"/repo/a.py": FakeReadResult(content="nothing here")})
|
|
register_backend("t1", backend)
|
|
request = _request(
|
|
"edit_file", {"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"}
|
|
)
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
return _ok("edit_file")
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact is None
|
|
|
|
|
|
async def test_write_file_new_file(register_backend) -> None:
|
|
backend = FakeBackend({"/repo/new.py": FakeReadResult(error="File not found")})
|
|
register_backend("t1", backend)
|
|
request = _request("write_file", {"file_path": "/repo/new.py", "content": "hello\n"})
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
return _ok("write_file")
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact == {
|
|
"diff": {
|
|
"filePath": "/repo/new.py",
|
|
"originalContent": None,
|
|
"newContent": "hello\n",
|
|
"isNewFile": True,
|
|
}
|
|
}
|
|
|
|
|
|
async def test_write_file_overwrite(register_backend) -> None:
|
|
backend = FakeBackend({"/repo/x.py": FakeReadResult(content="old content\n")})
|
|
register_backend("t1", backend)
|
|
request = _request("write_file", {"file_path": "/repo/x.py", "content": "new content\n"})
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
return _ok("write_file")
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact["diff"]["originalContent"] == "old content\n"
|
|
assert result.artifact["diff"]["newContent"] == "new content\n"
|
|
assert result.artifact["diff"]["isNewFile"] is False
|
|
|
|
|
|
async def test_non_edit_tool_is_untouched(register_backend) -> None:
|
|
backend = FakeBackend({})
|
|
register_backend("t1", backend)
|
|
request = _request("read_file", {"file_path": "/repo/a.py"})
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
return _ok("read_file")
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact is None
|
|
assert backend.reads == []
|
|
|
|
|
|
async def test_error_result_is_not_stamped(register_backend) -> None:
|
|
backend = FakeBackend({"/repo/a.py": FakeReadResult(content="OLD")})
|
|
register_backend("t1", backend)
|
|
request = _request(
|
|
"edit_file", {"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"}
|
|
)
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
return ToolMessage(content="boom", tool_call_id="call-1", status="error", name="edit_file")
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact is None
|
|
|
|
|
|
async def test_missing_backend_is_graceful() -> None:
|
|
request = _request(
|
|
"edit_file",
|
|
{"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"},
|
|
thread_id="absent-thread",
|
|
)
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
return _ok("edit_file")
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact is None
|
|
|
|
|
|
async def test_binary_read_skips(register_backend) -> None:
|
|
backend = FakeBackend({"/repo/img.png": FakeReadResult(content="QkFTRTY0", encoding="base64")})
|
|
register_backend("t1", backend)
|
|
request = _request(
|
|
"edit_file", {"file_path": "/repo/img.png", "old_string": "x", "new_string": "y"}
|
|
)
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
return _ok("edit_file")
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact is None
|
|
|
|
|
|
async def test_existing_artifact_is_merged(register_backend) -> None:
|
|
backend = FakeBackend({"/repo/a.py": FakeReadResult(content="OLD\n")})
|
|
register_backend("t1", backend)
|
|
request = _request(
|
|
"edit_file", {"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"}
|
|
)
|
|
|
|
async def handler(_req: Any) -> ToolMessage:
|
|
message = _ok("edit_file")
|
|
message.artifact = {"existing": "kept"}
|
|
return message
|
|
|
|
result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)
|
|
|
|
assert result.artifact["existing"] == "kept"
|
|
assert result.artifact["diff"]["newContent"] == "NEW\n"
|