Files
2026-07-13 12:39:17 +08:00

125 lines
4.6 KiB
Python

from __future__ import annotations
import uuid
from pathlib import Path
from typing import Any, cast
import pytest
from agents.editor import ApplyPatchOperation
from agents.sandbox import Manifest
from agents.sandbox.capabilities import Filesystem, FilesystemToolSet
from agents.sandbox.capabilities.tools import SandboxApplyPatchTool, ViewImageTool
from agents.sandbox.sandboxes.unix_local import (
UnixLocalSandboxSession,
UnixLocalSandboxSessionState,
)
from agents.sandbox.snapshot import NoopSnapshot
from agents.sandbox.types import User
from agents.tool import CustomTool, FunctionTool
def _make_session(tmp_path: Path) -> UnixLocalSandboxSession:
return UnixLocalSandboxSession(
state=UnixLocalSandboxSessionState(
manifest=Manifest(root=str(tmp_path / "workspace")),
snapshot=NoopSnapshot(id=str(uuid.uuid4())),
workspace_root_owned=False,
)
)
class TestFilesystemCapability:
def test_tools_requires_bound_session(self) -> None:
capability = Filesystem()
with pytest.raises(
ValueError,
match="Filesystem capability is not bound to a SandboxSession",
):
capability.tools()
def test_tools_exposes_view_image_and_apply_patch_after_bind(self, tmp_path: Path) -> None:
capability = Filesystem()
capability.bind(_make_session(tmp_path))
tools = capability.tools()
assert len(tools) == 2
assert isinstance(tools[0], ViewImageTool)
assert isinstance(tools[1], SandboxApplyPatchTool)
assert isinstance(tools[0], FunctionTool)
assert isinstance(tools[1], CustomTool)
assert tools[0].name == "view_image"
assert tools[1].name == "apply_patch"
def test_configure_tools_can_customize_approvals_after_clone(self, tmp_path: Path) -> None:
async def view_image_needs_approval(
_ctx: Any, params: dict[str, Any], _call_id: str
) -> bool:
return str(params["path"]).startswith("sensitive/")
async def apply_patch_needs_approval(
_ctx: Any, operation: ApplyPatchOperation, _call_id: str
) -> bool:
return operation.type != "create_file"
def configure_tools(toolset: FilesystemToolSet) -> None:
toolset.view_image.needs_approval = view_image_needs_approval
toolset.apply_patch.needs_approval = apply_patch_needs_approval
capability = Filesystem(configure_tools=configure_tools).clone()
capability.bind(_make_session(tmp_path))
tools = capability.tools()
view_image_tool = cast(ViewImageTool, tools[0])
apply_patch_tool = cast(SandboxApplyPatchTool, tools[1])
assert isinstance(view_image_tool, ViewImageTool)
assert isinstance(apply_patch_tool, SandboxApplyPatchTool)
assert cast(object, view_image_tool.needs_approval) is view_image_needs_approval
assert cast(object, apply_patch_tool.needs_approval) is apply_patch_needs_approval
def test_configure_tools_can_replace_tool_instances(self, tmp_path: Path) -> None:
replacement_view_image: ViewImageTool | None = None
def configure_tools(toolset: FilesystemToolSet) -> None:
nonlocal replacement_view_image
replacement_view_image = ViewImageTool(
session=toolset.view_image.session,
needs_approval=True,
)
toolset.view_image = replacement_view_image
capability = Filesystem(configure_tools=configure_tools)
capability.bind(_make_session(tmp_path))
tools = capability.tools()
view_image_tool = cast(ViewImageTool, tools[0])
assert replacement_view_image is not None
assert view_image_tool is replacement_view_image
assert view_image_tool.needs_approval is True
assert isinstance(tools[1], SandboxApplyPatchTool)
def test_tools_passes_bound_run_as_to_file_tools(self, tmp_path: Path) -> None:
run_as = User(name="sandbox-user")
capability = Filesystem()
capability.bind(_make_session(tmp_path))
capability.bind_run_as(run_as)
tools = capability.tools()
assert isinstance(tools[0], ViewImageTool)
assert isinstance(tools[1], SandboxApplyPatchTool)
assert tools[0].user == run_as
assert tools[1].editor.user == run_as
@pytest.mark.asyncio
async def test_instructions_default_to_none(self) -> None:
capability = Filesystem()
instructions = await capability.instructions(Manifest(root="/workspace"))
assert instructions is None