To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for."
6 | MAX_RESPONSE_LEN: int = 16000
7 |
8 |
9 | def maybe_truncate(content: str, truncate_after: int | None = MAX_RESPONSE_LEN):
10 | """Truncate content and append a notice if content exceeds the specified length."""
11 | return (
12 | content
13 | if not truncate_after or len(content) <= truncate_after
14 | else content[:truncate_after] + TRUNCATED_MESSAGE
15 | )
16 |
17 |
18 | async def run(
19 | cmd: str,
20 | timeout: float | None = 120.0, # seconds
21 | truncate_after: int | None = MAX_RESPONSE_LEN,
22 | ):
23 | """Run a shell command asynchronously with a timeout."""
24 | process = await asyncio.create_subprocess_shell(
25 | cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
26 | )
27 |
28 | try:
29 | stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
30 | return (
31 | process.returncode or 0,
32 | maybe_truncate(stdout.decode(), truncate_after=truncate_after),
33 | maybe_truncate(stderr.decode(), truncate_after=truncate_after),
34 | )
35 | except asyncio.TimeoutError as exc:
36 | try:
37 | process.kill()
38 | except ProcessLookupError:
39 | pass
40 | raise TimeoutError(
41 | f"Command '{cmd}' timed out after {timeout} seconds"
42 | ) from exc
43 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | -r computer_use_demo/requirements.txt
2 | ruff==0.6.7
3 | pre-commit==3.8.0
4 | pytest==8.3.3
5 | pytest-asyncio==0.23.6
6 |
--------------------------------------------------------------------------------
/image/.config/tint2/applications/firefox-custom.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Firefox Custom
3 | Comment=Open Firefox with custom URL
4 | Exec=firefox-esr -new-window
5 | Icon=firefox-esr
6 | Terminal=false
7 | Type=Application
8 | Categories=Network;WebBrowser;
9 |
--------------------------------------------------------------------------------
/image/.config/tint2/applications/gedit.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Gedit
3 | Comment=Open gedit
4 | Exec=gedit
5 | Icon=text-editor-symbolic
6 | Terminal=false
7 | Type=Application
8 | Categories=TextEditor;
9 |
--------------------------------------------------------------------------------
/image/.config/tint2/applications/terminal.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Terminal
3 | Comment=Open Terminal
4 | Exec=xterm
5 | Icon=utilities-terminal
6 | Terminal=false
7 | Type=Application
8 | Categories=System;TerminalEmulator;
9 |
--------------------------------------------------------------------------------
/image/.config/tint2/tint2rc:
--------------------------------------------------------------------------------
1 | #-------------------------------------
2 | # Panel
3 | panel_items = TL
4 | panel_size = 100% 60
5 | panel_margin = 0 0
6 | panel_padding = 2 0 2
7 | panel_background_id = 1
8 | wm_menu = 0
9 | panel_dock = 0
10 | panel_position = bottom center horizontal
11 | panel_layer = top
12 | panel_monitor = all
13 | panel_shrink = 0
14 | autohide = 0
15 | autohide_show_timeout = 0
16 | autohide_hide_timeout = 0.5
17 | autohide_height = 2
18 | strut_policy = follow_size
19 | panel_window_name = tint2
20 | disable_transparency = 1
21 | mouse_effects = 1
22 | font_shadow = 0
23 | mouse_hover_icon_asb = 100 0 10
24 | mouse_pressed_icon_asb = 100 0 0
25 | scale_relative_to_dpi = 0
26 | scale_relative_to_screen_height = 0
27 |
28 | #-------------------------------------
29 | # Taskbar
30 | taskbar_mode = single_desktop
31 | taskbar_hide_if_empty = 0
32 | taskbar_padding = 0 0 2
33 | taskbar_background_id = 0
34 | taskbar_active_background_id = 0
35 | taskbar_name = 1
36 | taskbar_hide_inactive_tasks = 0
37 | taskbar_hide_different_monitor = 0
38 | taskbar_hide_different_desktop = 0
39 | taskbar_always_show_all_desktop_tasks = 0
40 | taskbar_name_padding = 4 2
41 | taskbar_name_background_id = 0
42 | taskbar_name_active_background_id = 0
43 | taskbar_name_font_color = #e3e3e3 100
44 | taskbar_name_active_font_color = #ffffff 100
45 | taskbar_distribute_size = 0
46 | taskbar_sort_order = none
47 | task_align = left
48 |
49 | #-------------------------------------
50 | # Launcher
51 | launcher_padding = 4 8 4
52 | launcher_background_id = 0
53 | launcher_icon_background_id = 0
54 | launcher_icon_size = 48
55 | launcher_icon_asb = 100 0 0
56 | launcher_icon_theme_override = 0
57 | startup_notifications = 1
58 | launcher_tooltip = 1
59 |
60 | #-------------------------------------
61 | # Launcher icon
62 | launcher_item_app = /usr/share/applications/libreoffice-calc.desktop
63 | launcher_item_app = /home/computeruse/.config/tint2/applications/terminal.desktop
64 | launcher_item_app = /home/computeruse/.config/tint2/applications/firefox-custom.desktop
65 | launcher_item_app = /usr/share/applications/xpaint.desktop
66 | launcher_item_app = /usr/share/applications/xpdf.desktop
67 | launcher_item_app = /home/computeruse/.config/tint2/applications/gedit.desktop
68 | launcher_item_app = /usr/share/applications/galculator.desktop
69 |
70 | #-------------------------------------
71 | # Background definitions
72 | # ID 1
73 | rounded = 0
74 | border_width = 0
75 | background_color = #000000 60
76 | border_color = #000000 30
77 |
78 | # ID 2
79 | rounded = 4
80 | border_width = 1
81 | background_color = #777777 20
82 | border_color = #777777 30
83 |
84 | # ID 3
85 | rounded = 4
86 | border_width = 1
87 | background_color = #777777 20
88 | border_color = #ffffff 40
89 |
90 | # ID 4
91 | rounded = 4
92 | border_width = 1
93 | background_color = #aa4400 100
94 | border_color = #aa7733 100
95 |
96 | # ID 5
97 | rounded = 4
98 | border_width = 1
99 | background_color = #aaaa00 100
100 | border_color = #aaaa00 100
101 |
--------------------------------------------------------------------------------
/image/.streamlit/config.toml:
--------------------------------------------------------------------------------
1 | [server]
2 | fileWatcherType = "auto"
3 | runOnSave = true
4 |
5 | [browser]
6 | gatherUsageStats = false
7 |
--------------------------------------------------------------------------------
/image/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | ./start_all.sh
5 | ./novnc_startup.sh
6 |
7 | python http_server.py > /tmp/server_logs.txt 2>&1 &
8 |
9 | STREAMLIT_SERVER_PORT=8501 python -m streamlit run computer_use_demo/streamlit.py > /tmp/streamlit_stdout.log &
10 |
11 | echo "✨ Computer Use Demo is ready!"
12 | echo "➡️ Open http://localhost:8080 in your browser to begin"
13 |
14 | # Keep the container running
15 | tail -f /dev/null
16 |
--------------------------------------------------------------------------------
/image/http_server.py:
--------------------------------------------------------------------------------
1 | import os
2 | import socket
3 | from http.server import HTTPServer, SimpleHTTPRequestHandler
4 |
5 |
6 | class HTTPServerV6(HTTPServer):
7 | address_family = socket.AF_INET6
8 |
9 |
10 | def run_server():
11 | os.chdir(os.path.dirname(__file__) + "/static_content")
12 | server_address = ("::", 8080)
13 | httpd = HTTPServerV6(server_address, SimpleHTTPRequestHandler)
14 | print("Starting HTTP server on port 8080...") # noqa: T201
15 | httpd.serve_forever()
16 |
17 |
18 | if __name__ == "__main__":
19 | run_server()
20 |
--------------------------------------------------------------------------------
/image/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Computer Use Demo
5 |
6 |
28 |
29 |
30 |
31 |
36 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/image/mutter_startup.sh:
--------------------------------------------------------------------------------
1 | echo "starting mutter"
2 | XDG_SESSION_TYPE=x11 mutter --replace --sm-disable 2>/tmp/mutter_stderr.log &
3 |
4 | # Wait for tint2 window properties to appear
5 | timeout=30
6 | while [ $timeout -gt 0 ]; do
7 | if xdotool search --class "mutter" >/dev/null 2>&1; then
8 | break
9 | fi
10 | sleep 1
11 | ((timeout--))
12 | done
13 |
14 | if [ $timeout -eq 0 ]; then
15 | echo "mutter stderr output:" >&2
16 | cat /tmp/mutter_stderr.log >&2
17 | exit 1
18 | fi
19 |
20 | rm /tmp/mutter_stderr.log
21 |
--------------------------------------------------------------------------------
/image/novnc_startup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo "starting noVNC"
3 |
4 | # Start noVNC with explicit websocket settings
5 | /opt/noVNC/utils/novnc_proxy \
6 | --vnc localhost:5900 \
7 | --listen 6080 \
8 | --web /opt/noVNC \
9 | > /tmp/novnc.log 2>&1 &
10 |
11 | # Wait for noVNC to start
12 | timeout=10
13 | while [ $timeout -gt 0 ]; do
14 | if netstat -tuln | grep -q ":6080 "; then
15 | break
16 | fi
17 | sleep 1
18 | ((timeout--))
19 | done
20 |
21 | echo "noVNC started successfully"
22 |
--------------------------------------------------------------------------------
/image/start_all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | export DISPLAY=:${DISPLAY_NUM}
6 | ./xvfb_startup.sh
7 | ./tint2_startup.sh
8 | ./mutter_startup.sh
9 | ./x11vnc_startup.sh
10 |
--------------------------------------------------------------------------------
/image/static_content/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Computer Use Demo
5 |
6 |
28 |
29 |
30 |
31 |
36 |
42 |
48 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/image/tint2_startup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo "starting tint2 on display :$DISPLAY_NUM ..."
3 |
4 | # Start tint2 and capture its stderr
5 | tint2 -c $HOME/.config/tint2/tint2rc 2>/tmp/tint2_stderr.log &
6 |
7 | # Wait for tint2 window properties to appear
8 | timeout=30
9 | while [ $timeout -gt 0 ]; do
10 | if xdotool search --class "tint2" >/dev/null 2>&1; then
11 | break
12 | fi
13 | sleep 1
14 | ((timeout--))
15 | done
16 |
17 | if [ $timeout -eq 0 ]; then
18 | echo "tint2 stderr output:" >&2
19 | cat /tmp/tint2_stderr.log >&2
20 | exit 1
21 | fi
22 |
23 | # Remove the temporary stderr log file
24 | rm /tmp/tint2_stderr.log
25 |
--------------------------------------------------------------------------------
/image/x11vnc_startup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo "starting vnc"
3 |
4 | (x11vnc -display $DISPLAY \
5 | -forever \
6 | -shared \
7 | -wait 50 \
8 | -timeout 60 \
9 | -noxrecord \
10 | -noxfixes \
11 | -noxdamage \
12 | -rfbport 5900 \
13 | 2>/tmp/x11vnc_stderr.log) &
14 |
15 | x11vnc_pid=$!
16 |
17 | # Wait for x11vnc to start
18 | timeout=10
19 | while [ $timeout -gt 0 ]; do
20 | if netstat -tuln | grep -q ":5900 "; then
21 | break
22 | fi
23 | sleep 1
24 | ((timeout--))
25 | done
26 |
27 | if [ $timeout -eq 0 ]; then
28 | echo "x11vnc failed to start, stderr output:" >&2
29 | cat /tmp/x11vnc_stderr.log >&2
30 | exit 1
31 | fi
32 |
33 | : > /tmp/x11vnc_stderr.log
34 |
35 | # Monitor x11vnc process in the background
36 | (
37 | while true; do
38 | if ! kill -0 $x11vnc_pid 2>/dev/null; then
39 | echo "x11vnc process crashed, restarting..." >&2
40 | if [ -f /tmp/x11vnc_stderr.log ]; then
41 | echo "x11vnc stderr output:" >&2
42 | cat /tmp/x11vnc_stderr.log >&2
43 | rm /tmp/x11vnc_stderr.log
44 | fi
45 | exec "$0"
46 | fi
47 | sleep 5
48 | done
49 | ) &
50 |
--------------------------------------------------------------------------------
/image/xvfb_startup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e # Exit on error
3 |
4 | DPI=96
5 | RES_AND_DEPTH=${WIDTH}x${HEIGHT}x24
6 |
7 | # Function to check if Xvfb is already running
8 | check_xvfb_running() {
9 | if [ -e /tmp/.X${DISPLAY_NUM}-lock ]; then
10 | return 0 # Xvfb is already running
11 | else
12 | return 1 # Xvfb is not running
13 | fi
14 | }
15 |
16 | # Function to check if Xvfb is ready
17 | wait_for_xvfb() {
18 | local timeout=10
19 | local start_time=$(date +%s)
20 | while ! xdpyinfo >/dev/null 2>&1; do
21 | if [ $(($(date +%s) - start_time)) -gt $timeout ]; then
22 | echo "Xvfb failed to start within $timeout seconds" >&2
23 | return 1
24 | fi
25 | sleep 0.1
26 | done
27 | return 0
28 | }
29 |
30 | # Check if Xvfb is already running
31 | if check_xvfb_running; then
32 | echo "Xvfb is already running on display ${DISPLAY}"
33 | exit 0
34 | fi
35 |
36 | # Start Xvfb
37 | Xvfb $DISPLAY -ac -screen 0 $RES_AND_DEPTH -retro -dpi $DPI -nolisten tcp -nolisten unix &
38 | XVFB_PID=$!
39 |
40 | # Wait for Xvfb to start
41 | if wait_for_xvfb; then
42 | echo "Xvfb started successfully on display ${DISPLAY}"
43 | echo "Xvfb PID: $XVFB_PID"
44 | else
45 | echo "Xvfb failed to start"
46 | kill $XVFB_PID
47 | exit 1
48 | fi
49 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ObservedObserver/claude-minecraft-use/8b52ef2a5aa175a49475db07ad7168b33089f8b6/main.py
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.pyright]
2 | venvPath = "."
3 | venv = ".venv"
4 | useLibraryCodeForTypes = false
5 |
6 | [tool.pytest.ini_options]
7 | pythonpath = "."
8 | asyncio_mode = "auto"
9 |
--------------------------------------------------------------------------------
/ruff.toml:
--------------------------------------------------------------------------------
1 | extend-exclude = [".venv"]
2 |
3 | [format]
4 | docstring-code-format = true
5 |
6 | [lint]
7 | select = [
8 | "A",
9 | "ASYNC",
10 | "B",
11 | "E",
12 | "F",
13 | "I",
14 | "PIE",
15 | "RUF200",
16 | "T20",
17 | "UP",
18 | "W",
19 | ]
20 |
21 | ignore = ["E501", "ASYNC230"]
22 |
23 | [lint.isort]
24 | combine-as-imports = true
25 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | PYTHON_MINOR_VERSION=$(python3 --version | awk -F. '{print $2}')
3 |
4 | if [ "$PYTHON_MINOR_VERSION" -gt 12 ]; then
5 | echo "Python version 3.$PYTHON_MINOR_VERSION detected. Python 3.12 or lower is required for setup to complete."
6 | echo "If you have multiple versions of Python installed, you can set the correct one by adjusting setup.sh to use a specific version, for example:"
7 | echo "'python3 -m venv .venv' -> 'python3.12 -m venv .venv'"
8 | exit 1
9 | fi
10 |
11 | if ! command -v cargo &> /dev/null; then
12 | echo "Cargo (the package manager for Rust) is not present. This is required for one of this module's dependencies."
13 | echo "See https://www.rust-lang.org/tools/install for installation instructions."
14 | exit 1
15 | fi
16 |
17 | python3 -m venv .venv
18 | source .venv/bin/activate
19 | pip install --upgrade pip
20 | pip install -r dev-requirements.txt
21 | pre-commit install
22 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import mock
3 |
4 | import pytest
5 |
6 |
7 | @pytest.fixture(autouse=True)
8 | def mock_screen_dimensions():
9 | with mock.patch.dict(
10 | os.environ, {"HEIGHT": "768", "WIDTH": "1024", "DISPLAY_NUM": "1"}
11 | ):
12 | yield
13 |
--------------------------------------------------------------------------------
/tests/loop_test.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 |
3 | from anthropic.types import TextBlock, ToolUseBlock
4 | from anthropic.types.beta import BetaMessage, BetaMessageParam
5 |
6 | from computer_use_demo.loop import APIProvider, sampling_loop
7 |
8 |
9 | async def test_loop():
10 | client = mock.Mock()
11 | client.beta.messages.with_raw_response.create.return_value = mock.Mock()
12 | client.beta.messages.with_raw_response.create.return_value.parse.side_effect = [
13 | mock.Mock(
14 | spec=BetaMessage,
15 | content=[
16 | TextBlock(type="text", text="Hello"),
17 | ToolUseBlock(
18 | type="tool_use", id="1", name="computer", input={"action": "test"}
19 | ),
20 | ],
21 | ),
22 | mock.Mock(spec=BetaMessage, content=[TextBlock(type="text", text="Done!")]),
23 | ]
24 |
25 | tool_collection = mock.AsyncMock()
26 | tool_collection.run.return_value = mock.Mock(
27 | output="Tool output", error=None, base64_image=None
28 | )
29 |
30 | output_callback = mock.Mock()
31 | tool_output_callback = mock.Mock()
32 | api_response_callback = mock.Mock()
33 |
34 | with mock.patch(
35 | "computer_use_demo.loop.Anthropic", return_value=client
36 | ), mock.patch(
37 | "computer_use_demo.loop.ToolCollection", return_value=tool_collection
38 | ):
39 | messages: list[BetaMessageParam] = [{"role": "user", "content": "Test message"}]
40 | result = await sampling_loop(
41 | model="test-model",
42 | provider=APIProvider.ANTHROPIC,
43 | system_prompt_suffix="",
44 | messages=messages,
45 | output_callback=output_callback,
46 | tool_output_callback=tool_output_callback,
47 | api_response_callback=api_response_callback,
48 | api_key="test-key",
49 | )
50 |
51 | assert len(result) == 4
52 | assert result[0] == {"role": "user", "content": "Test message"}
53 | assert result[1]["role"] == "assistant"
54 | assert result[2]["role"] == "user"
55 | assert result[3]["role"] == "assistant"
56 |
57 | assert client.beta.messages.with_raw_response.create.call_count == 2
58 | tool_collection.run.assert_called_once_with(
59 | name="computer", tool_input={"action": "test"}
60 | )
61 | output_callback.assert_called_with(TextBlock(text="Done!", type="text"))
62 | assert output_callback.call_count == 3
63 | assert tool_output_callback.call_count == 1
64 | assert api_response_callback.call_count == 2
65 |
--------------------------------------------------------------------------------
/tests/streamlit_test.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 |
3 | import pytest
4 | from streamlit.testing.v1 import AppTest
5 |
6 | from computer_use_demo.streamlit import Sender, TextBlock
7 |
8 |
9 | @pytest.fixture
10 | def streamlit_app():
11 | return AppTest.from_file("computer_use_demo/streamlit.py")
12 |
13 |
14 | def test_streamlit(streamlit_app: AppTest):
15 | streamlit_app.run()
16 | streamlit_app.text_input[1].set_value("sk-ant-0000000000000").run()
17 | with mock.patch("computer_use_demo.loop.sampling_loop") as patch:
18 | streamlit_app.chat_input[0].set_value("Hello").run()
19 | assert patch.called
20 | assert patch.call_args.kwargs["messages"] == [
21 | {"role": Sender.USER, "content": [TextBlock(text="Hello", type="text")]}
22 | ]
23 | assert not streamlit_app.exception
24 |
--------------------------------------------------------------------------------
/tests/tools/bash_test.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from computer_use_demo.tools.bash import BashTool, ToolError
4 |
5 |
6 | @pytest.fixture
7 | def bash_tool():
8 | return BashTool()
9 |
10 |
11 | @pytest.mark.asyncio
12 | async def test_bash_tool_restart(bash_tool):
13 | result = await bash_tool(restart=True)
14 | assert result.system == "tool has been restarted."
15 |
16 | # Verify the tool can be used after restart
17 | result = await bash_tool(command="echo 'Hello after restart'")
18 | assert "Hello after restart" in result.output
19 |
20 |
21 | @pytest.mark.asyncio
22 | async def test_bash_tool_run_command(bash_tool):
23 | result = await bash_tool(command="echo 'Hello, World!'")
24 | assert result.output.strip() == "Hello, World!"
25 | assert result.error == ""
26 |
27 |
28 | @pytest.mark.asyncio
29 | async def test_bash_tool_no_command(bash_tool):
30 | with pytest.raises(ToolError, match="no command provided."):
31 | await bash_tool()
32 |
33 |
34 | @pytest.mark.asyncio
35 | async def test_bash_tool_session_creation(bash_tool):
36 | result = await bash_tool(command="echo 'Session created'")
37 | assert bash_tool._session is not None
38 | assert "Session created" in result.output
39 |
40 |
41 | @pytest.mark.asyncio
42 | async def test_bash_tool_session_reuse(bash_tool):
43 | result1 = await bash_tool(command="echo 'First command'")
44 | result2 = await bash_tool(command="echo 'Second command'")
45 |
46 | assert "First command" in result1.output
47 | assert "Second command" in result2.output
48 |
49 |
50 | @pytest.mark.asyncio
51 | async def test_bash_tool_session_error(bash_tool):
52 | result = await bash_tool(command="invalid_command_that_does_not_exist")
53 | assert "command not found" in result.error
54 |
55 |
56 | @pytest.mark.asyncio
57 | async def test_bash_tool_non_zero_exit(bash_tool):
58 | result = await bash_tool(command="bash -c 'exit 1'")
59 | assert result.error.strip() == ""
60 | assert result.output.strip() == ""
61 |
62 |
63 | @pytest.mark.asyncio
64 | async def test_bash_tool_timeout(bash_tool):
65 | await bash_tool(command="echo 'Hello, World!'")
66 | bash_tool._session._timeout = 0.1 # Set a very short timeout for testing
67 | with pytest.raises(
68 | ToolError,
69 | match="timed out: bash has not returned in 0.1 seconds and must be restarted",
70 | ):
71 | await bash_tool(command="sleep 1")
72 |
--------------------------------------------------------------------------------
/tests/tools/computer_test.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import AsyncMock, patch
2 |
3 | import pytest
4 |
5 | from computer_use_demo.tools.computer import (
6 | ComputerTool,
7 | ScalingSource,
8 | ToolError,
9 | ToolResult,
10 | )
11 |
12 |
13 | @pytest.fixture
14 | def computer_tool():
15 | return ComputerTool()
16 |
17 |
18 | @pytest.mark.asyncio
19 | async def test_computer_tool_mouse_move(computer_tool):
20 | with patch.object(computer_tool, "shell", new_callable=AsyncMock) as mock_shell:
21 | mock_shell.return_value = ToolResult(output="Mouse moved")
22 | result = await computer_tool(action="mouse_move", coordinate=[100, 200])
23 | mock_shell.assert_called_once_with(
24 | f"{computer_tool.xdotool} mousemove --sync 100 200"
25 | )
26 | assert result.output == "Mouse moved"
27 |
28 |
29 | @pytest.mark.asyncio
30 | async def test_computer_tool_type(computer_tool):
31 | with (
32 | patch.object(computer_tool, "shell", new_callable=AsyncMock) as mock_shell,
33 | patch.object(
34 | computer_tool, "screenshot", new_callable=AsyncMock
35 | ) as mock_screenshot,
36 | ):
37 | mock_shell.return_value = ToolResult(output="Text typed")
38 | mock_screenshot.return_value = ToolResult(base64_image="base64_screenshot")
39 | result = await computer_tool(action="type", text="Hello, World!")
40 | assert mock_shell.call_count == 1
41 | assert "type --delay 12 -- 'Hello, World!'" in mock_shell.call_args[0][0]
42 | assert result.output == "Text typed"
43 | assert result.base64_image == "base64_screenshot"
44 |
45 |
46 | @pytest.mark.asyncio
47 | async def test_computer_tool_screenshot(computer_tool):
48 | with patch.object(
49 | computer_tool, "screenshot", new_callable=AsyncMock
50 | ) as mock_screenshot:
51 | mock_screenshot.return_value = ToolResult(base64_image="base64_screenshot")
52 | result = await computer_tool(action="screenshot")
53 | mock_screenshot.assert_called_once()
54 | assert result.base64_image == "base64_screenshot"
55 |
56 |
57 | @pytest.mark.asyncio
58 | async def test_computer_tool_scaling(computer_tool):
59 | computer_tool._scaling_enabled = True
60 | computer_tool.width = 1920
61 | computer_tool.height = 1080
62 |
63 | # Test scaling from API to computer
64 | x, y = computer_tool.scale_coordinates(ScalingSource.API, 1366, 768)
65 | assert x == 1920
66 | assert y == 1080
67 |
68 | # Test scaling from computer to API
69 | x, y = computer_tool.scale_coordinates(ScalingSource.COMPUTER, 1920, 1080)
70 | assert x == 1366
71 | assert y == 768
72 |
73 | # Test no scaling when disabled
74 | computer_tool._scaling_enabled = False
75 | x, y = computer_tool.scale_coordinates(ScalingSource.API, 1366, 768)
76 | assert x == 1366
77 | assert y == 768
78 |
79 |
80 | @pytest.mark.asyncio
81 | async def test_computer_tool_scaling_with_different_aspect_ratio(computer_tool):
82 | computer_tool._scaling_enabled = True
83 | computer_tool.width = 1920
84 | computer_tool.height = 1200 # 16:10 aspect ratio
85 |
86 | # Test scaling from API to computer
87 | x, y = computer_tool.scale_coordinates(ScalingSource.API, 1280, 800)
88 | assert x == 1920
89 | assert y == 1200
90 |
91 | # Test scaling from computer to API
92 | x, y = computer_tool.scale_coordinates(ScalingSource.COMPUTER, 1920, 1200)
93 | assert x == 1280
94 | assert y == 800
95 |
96 |
97 | @pytest.mark.asyncio
98 | async def test_computer_tool_no_scaling_for_unsupported_resolution(computer_tool):
99 | computer_tool._scaling_enabled = True
100 | computer_tool.width = 4096
101 | computer_tool.height = 2160
102 |
103 | # Test no scaling for unsupported resolution
104 | x, y = computer_tool.scale_coordinates(ScalingSource.API, 4096, 2160)
105 | assert x == 4096
106 | assert y == 2160
107 |
108 | x, y = computer_tool.scale_coordinates(ScalingSource.COMPUTER, 4096, 2160)
109 | assert x == 4096
110 | assert y == 2160
111 |
112 |
113 | @pytest.mark.asyncio
114 | async def test_computer_tool_scaling_out_of_bounds(computer_tool):
115 | computer_tool._scaling_enabled = True
116 | computer_tool.width = 1920
117 | computer_tool.height = 1080
118 |
119 | # Test scaling from API with out of bounds coordinates
120 | with pytest.raises(ToolError, match="Coordinates .*, .* are out of bounds"):
121 | x, y = computer_tool.scale_coordinates(ScalingSource.API, 2000, 1500)
122 |
123 |
124 | @pytest.mark.asyncio
125 | async def test_computer_tool_invalid_action(computer_tool):
126 | with pytest.raises(ToolError, match="Invalid action: invalid_action"):
127 | await computer_tool(action="invalid_action")
128 |
129 |
130 | @pytest.mark.asyncio
131 | async def test_computer_tool_missing_coordinate(computer_tool):
132 | with pytest.raises(ToolError, match="coordinate is required for mouse_move"):
133 | await computer_tool(action="mouse_move")
134 |
135 |
136 | @pytest.mark.asyncio
137 | async def test_computer_tool_missing_text(computer_tool):
138 | with pytest.raises(ToolError, match="text is required for type"):
139 | await computer_tool(action="type")
140 |
--------------------------------------------------------------------------------
/tests/tools/edit_test.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from unittest.mock import patch
3 |
4 | import pytest
5 |
6 | from computer_use_demo.tools.base import CLIResult, ToolError, ToolResult
7 | from computer_use_demo.tools.edit import EditTool
8 |
9 |
10 | @pytest.mark.asyncio
11 | async def test_view_command():
12 | edit_tool = EditTool()
13 |
14 | # Test viewing a file that exists
15 | with patch("pathlib.Path.exists", return_value=True), patch(
16 | "pathlib.Path.is_dir", return_value=False
17 | ), patch("pathlib.Path.read_text") as mock_read_text:
18 | mock_read_text.return_value = "File content"
19 | result = await edit_tool(command="view", path="/test/file.txt")
20 | assert isinstance(result, CLIResult)
21 | assert result.output
22 | assert "File content" in result.output
23 |
24 | # Test viewing a directory
25 | with patch("pathlib.Path.exists", return_value=True), patch(
26 | "pathlib.Path.is_dir", return_value=True
27 | ), patch("computer_use_demo.tools.edit.run") as mock_run:
28 | mock_run.return_value = (None, "file1.txt\nfile2.txt", None)
29 | result = await edit_tool(command="view", path="/test/dir")
30 | assert isinstance(result, CLIResult)
31 | assert result.output
32 | assert "file1.txt" in result.output
33 | assert "file2.txt" in result.output
34 |
35 | # Test viewing a file with a specific range
36 | with patch("pathlib.Path.exists", return_value=True), patch(
37 | "pathlib.Path.is_dir", return_value=False
38 | ), patch("pathlib.Path.read_text") as mock_read_text:
39 | mock_read_text.return_value = "Line 1\nLine 2\nLine 3\nLine 4"
40 | result = await edit_tool(
41 | command="view", path="/test/file.txt", view_range=[2, 3]
42 | )
43 | assert isinstance(result, CLIResult)
44 | assert result.output
45 | assert "\n 2\tLine 2\n 3\tLine 3\n" in result.output
46 |
47 | # Test viewing a file with an invalid range
48 | with patch("pathlib.Path.exists", return_value=True), patch(
49 | "pathlib.Path.is_dir", return_value=False
50 | ), patch("pathlib.Path.read_text") as mock_read_text:
51 | mock_read_text.return_value = "Line 1\nLine 2\nLine 3\nLine 4"
52 | with pytest.raises(ToolError, match="Invalid `view_range`"):
53 | await edit_tool(command="view", path="/test/file.txt", view_range=[3, 2])
54 |
55 | # Test viewing a non-existent file
56 | with patch("pathlib.Path.exists", return_value=False):
57 | with pytest.raises(ToolError, match="does not exist"):
58 | await edit_tool(command="view", path="/nonexistent/file.txt")
59 |
60 | # Test viewing a directory with a view_range
61 | with patch("pathlib.Path.exists", return_value=True), patch(
62 | "pathlib.Path.is_dir", return_value=True
63 | ):
64 | with pytest.raises(ToolError, match="view_range` parameter is not allowed"):
65 | await edit_tool(command="view", path="/test/dir", view_range=[1, 2])
66 |
67 |
68 | @pytest.mark.asyncio
69 | async def test_create_command():
70 | edit_tool = EditTool()
71 |
72 | # Test creating a new file with content
73 | with patch("pathlib.Path.exists", return_value=False), patch(
74 | "pathlib.Path.write_text"
75 | ) as mock_write_text:
76 | result = await edit_tool(
77 | command="create", path="/test/newfile.txt", file_text="New file content"
78 | )
79 | assert isinstance(result, ToolResult)
80 | assert result.output
81 | assert "File created successfully" in result.output
82 | mock_write_text.assert_called_once_with("New file content")
83 |
84 | # Test attempting to create a file without content
85 | with patch("pathlib.Path.exists", return_value=False):
86 | with pytest.raises(ToolError, match="Parameter `file_text` is required"):
87 | await edit_tool(command="create", path="/test/newfile.txt")
88 |
89 | # Test attempting to create a file that already exists
90 | with patch("pathlib.Path.exists", return_value=True):
91 | with pytest.raises(ToolError, match="File already exists"):
92 | await edit_tool(
93 | command="create", path="/test/existingfile.txt", file_text="Content"
94 | )
95 |
96 |
97 | @pytest.mark.asyncio
98 | async def test_str_replace_command():
99 | edit_tool = EditTool()
100 |
101 | # Test replacing a unique string in a file
102 | with patch("pathlib.Path.exists", return_value=True), patch(
103 | "pathlib.Path.is_dir", return_value=False
104 | ), patch("pathlib.Path.read_text") as mock_read_text, patch(
105 | "pathlib.Path.write_text"
106 | ) as mock_write_text:
107 | mock_read_text.return_value = "Original content"
108 | result = await edit_tool(
109 | command="str_replace",
110 | path="/test/file.txt",
111 | old_str="Original",
112 | new_str="New",
113 | )
114 | assert isinstance(result, CLIResult)
115 | assert result.output
116 | assert "has been edited" in result.output
117 | mock_write_text.assert_called_once_with("New content")
118 |
119 | # Test attempting to replace a non-existent string
120 | with patch("pathlib.Path.exists", return_value=True), patch(
121 | "pathlib.Path.is_dir", return_value=False
122 | ), patch("pathlib.Path.read_text") as mock_read_text:
123 | mock_read_text.return_value = "Original content"
124 | with pytest.raises(ToolError, match="did not appear verbatim"):
125 | await edit_tool(
126 | command="str_replace",
127 | path="/test/file.txt",
128 | old_str="Nonexistent",
129 | new_str="New",
130 | )
131 |
132 | # Test attempting to replace a string that appears multiple times
133 | with patch("pathlib.Path.exists", return_value=True), patch(
134 | "pathlib.Path.is_dir", return_value=False
135 | ), patch("pathlib.Path.read_text") as mock_read_text:
136 | mock_read_text.return_value = "Test test test"
137 | with pytest.raises(ToolError, match="Multiple occurrences"):
138 | await edit_tool(
139 | command="str_replace",
140 | path="/test/file.txt",
141 | old_str="test",
142 | new_str="example",
143 | )
144 |
145 | edit_tool._file_history.clear()
146 | # Verify that the file history is updated after replacement
147 | with patch("pathlib.Path.exists", return_value=True), patch(
148 | "pathlib.Path.is_dir", return_value=False
149 | ), patch("pathlib.Path.read_text") as mock_read_text, patch(
150 | "pathlib.Path.write_text"
151 | ):
152 | mock_read_text.return_value = "Original content"
153 | await edit_tool(
154 | command="str_replace",
155 | path="/test/file.txt",
156 | old_str="Original",
157 | new_str="New",
158 | )
159 | assert edit_tool._file_history[Path("/test/file.txt")] == ["Original content"]
160 |
161 |
162 | @pytest.mark.asyncio
163 | async def test_insert_command():
164 | edit_tool = EditTool()
165 |
166 | # Test inserting a string at a valid line number
167 | with patch("pathlib.Path.exists", return_value=True), patch(
168 | "pathlib.Path.is_dir", return_value=False
169 | ), patch("pathlib.Path.read_text") as mock_read_text, patch(
170 | "pathlib.Path.write_text"
171 | ) as mock_write_text:
172 | mock_read_text.return_value = "Line 1\nLine 2\nLine 3"
173 | result = await edit_tool(
174 | command="insert", path="/test/file.txt", insert_line=2, new_str="New Line"
175 | )
176 | assert isinstance(result, CLIResult)
177 | assert result.output
178 | assert "has been edited" in result.output
179 | mock_write_text.assert_called_once_with("Line 1\nLine 2\nNew Line\nLine 3")
180 |
181 | # Test inserting a string at the beginning of the file (line 0)
182 | with patch("pathlib.Path.exists", return_value=True), patch(
183 | "pathlib.Path.is_dir", return_value=False
184 | ), patch("pathlib.Path.read_text") as mock_read_text, patch(
185 | "pathlib.Path.write_text"
186 | ) as mock_write_text:
187 | mock_read_text.return_value = "Line 1\nLine 2"
188 | result = await edit_tool(
189 | command="insert",
190 | path="/test/file.txt",
191 | insert_line=0,
192 | new_str="New First Line",
193 | )
194 | assert isinstance(result, CLIResult)
195 | assert result.output
196 | assert "has been edited" in result.output
197 | mock_write_text.assert_called_once_with("New First Line\nLine 1\nLine 2")
198 |
199 | # Test inserting a string at the end of the file
200 | with patch("pathlib.Path.exists", return_value=True), patch(
201 | "pathlib.Path.is_dir", return_value=False
202 | ), patch("pathlib.Path.read_text") as mock_read_text, patch(
203 | "pathlib.Path.write_text"
204 | ) as mock_write_text:
205 | mock_read_text.return_value = "Line 1\nLine 2"
206 | result = await edit_tool(
207 | command="insert",
208 | path="/test/file.txt",
209 | insert_line=2,
210 | new_str="New Last Line",
211 | )
212 | assert isinstance(result, CLIResult)
213 | assert result.output
214 | assert "has been edited" in result.output
215 | mock_write_text.assert_called_once_with("Line 1\nLine 2\nNew Last Line")
216 |
217 | # Test attempting to insert at an invalid line number
218 | with patch("pathlib.Path.exists", return_value=True), patch(
219 | "pathlib.Path.is_dir", return_value=False
220 | ), patch("pathlib.Path.read_text") as mock_read_text:
221 | mock_read_text.return_value = "Line 1\nLine 2"
222 | with pytest.raises(ToolError, match="Invalid `insert_line` parameter"):
223 | await edit_tool(
224 | command="insert",
225 | path="/test/file.txt",
226 | insert_line=5,
227 | new_str="Invalid Line",
228 | )
229 |
230 | # Verify that the file history is updated after insertion
231 | edit_tool._file_history.clear()
232 | with patch("pathlib.Path.exists", return_value=True), patch(
233 | "pathlib.Path.is_dir", return_value=False
234 | ), patch("pathlib.Path.read_text") as mock_read_text, patch(
235 | "pathlib.Path.write_text"
236 | ):
237 | mock_read_text.return_value = "Original content"
238 | await edit_tool(
239 | command="insert", path="/test/file.txt", insert_line=1, new_str="New Line"
240 | )
241 | assert edit_tool._file_history[Path("/test/file.txt")] == ["Original content"]
242 |
243 |
244 | @pytest.mark.asyncio
245 | async def test_undo_edit_command():
246 | edit_tool = EditTool()
247 |
248 | # Test undoing a str_replace operation
249 | with patch("pathlib.Path.exists", return_value=True), patch(
250 | "pathlib.Path.is_dir", return_value=False
251 | ), patch("pathlib.Path.read_text") as mock_read_text, patch(
252 | "pathlib.Path.write_text"
253 | ) as mock_write_text:
254 | mock_read_text.return_value = "Original content"
255 | await edit_tool(
256 | command="str_replace",
257 | path="/test/file.txt",
258 | old_str="Original",
259 | new_str="New",
260 | )
261 | mock_read_text.return_value = "New content"
262 | result = await edit_tool(command="undo_edit", path="/test/file.txt")
263 | assert isinstance(result, CLIResult)
264 | assert result.output
265 | assert "Last edit to /test/file.txt undone successfully" in result.output
266 | mock_write_text.assert_called_with("Original content")
267 |
268 | # Test undoing an insert operation
269 | edit_tool._file_history.clear()
270 | with patch("pathlib.Path.exists", return_value=True), patch(
271 | "pathlib.Path.is_dir", return_value=False
272 | ), patch("pathlib.Path.read_text") as mock_read_text, patch(
273 | "pathlib.Path.write_text"
274 | ) as mock_write_text:
275 | mock_read_text.return_value = "Line 1\nLine 2"
276 | await edit_tool(
277 | command="insert", path="/test/file.txt", insert_line=1, new_str="New Line"
278 | )
279 | mock_read_text.return_value = "Line 1\nNew Line\nLine 2"
280 | result = await edit_tool(command="undo_edit", path="/test/file.txt")
281 | assert isinstance(result, CLIResult)
282 | assert result.output
283 | assert "Last edit to /test/file.txt undone successfully" in result.output
284 | mock_write_text.assert_called_with("Line 1\nLine 2")
285 |
286 | # Test attempting to undo when there's no history
287 | edit_tool._file_history.clear()
288 | with patch("pathlib.Path.exists", return_value=True), patch(
289 | "pathlib.Path.is_dir", return_value=False
290 | ):
291 | with pytest.raises(ToolError, match="No edit history found"):
292 | await edit_tool(command="undo_edit", path="/test/file.txt")
293 |
294 |
295 | @pytest.mark.asyncio
296 | async def test_validate_path():
297 | edit_tool = EditTool()
298 |
299 | # Test with valid absolute paths
300 | with patch("pathlib.Path.exists", return_value=True), patch(
301 | "pathlib.Path.is_dir", return_value=False
302 | ):
303 | edit_tool.validate_path("view", Path("/valid/path.txt"))
304 |
305 | # Test with relative paths (should raise an error)
306 | with pytest.raises(ToolError, match="not an absolute path"):
307 | edit_tool.validate_path("view", Path("relative/path.txt"))
308 |
309 | # Test with non-existent paths for non-create commands (should raise an error)
310 | with patch("pathlib.Path.exists", return_value=False):
311 | with pytest.raises(ToolError, match="does not exist"):
312 | edit_tool.validate_path("view", Path("/nonexistent/file.txt"))
313 |
314 | # Test with existing paths for create command (should raise an error)
315 | with patch("pathlib.Path.exists", return_value=True):
316 | with pytest.raises(ToolError, match="File already exists"):
317 | edit_tool.validate_path("create", Path("/existing/file.txt"))
318 |
319 | # Test with directory paths for non-view commands (should raise an error)
320 | with patch("pathlib.Path.exists", return_value=True), patch(
321 | "pathlib.Path.is_dir", return_value=True
322 | ):
323 | with pytest.raises(ToolError, match="is a directory"):
324 | edit_tool.validate_path("str_replace", Path("/directory/path"))
325 |
326 | # Test with directory path for view command (should not raise an error)
327 | with patch("pathlib.Path.exists", return_value=True), patch(
328 | "pathlib.Path.is_dir", return_value=True
329 | ):
330 | edit_tool.validate_path("view", Path("/directory/path"))
331 |
--------------------------------------------------------------------------------