Newer
Older
navi-1 / tests / unit / tools / test_terminal.py
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 26 May 7 KB Move terminal_manager to _internal subpackage
"""Unit tests for terminal tool."""

import asyncio

import pytest

from navi.tools.terminal import TerminalTool
from navi.tools._internal.terminal_manager import TerminalManager, _MAX_TERMINALS_PER_SESSION

class TestTerminalTool:
    @pytest.fixture
    def tool(self):
        return TerminalTool()

    async def test_run_echo(self, tool):
        result = await tool.execute({"action": "run", "command": "echo hello"})
        assert result.success
        assert "hello" in result.output

    async def test_run_pwd(self, tool):
        result = await tool.execute({"action": "run", "command": "pwd"})
        assert result.success
        assert "/" in result.output

    async def test_run_empty_command(self, tool):
        result = await tool.execute({"action": "run", "command": "  "})
        assert not result.success
        assert "empty" in result.output.lower()

    async def test_run_invalid_command(self, tool):
        result = await tool.execute({"action": "run", "command": "this_command_does_not_exist_12345"})
        assert not result.success

    async def test_missing_action(self, tool):
        result = await tool.execute({"command": "echo hi"})
        assert not result.success
        assert "missing" in result.output.lower()

    async def test_unknown_action(self, tool):
        result = await tool.execute({"action": "fly", "command": "echo hi"})
        assert not result.success
        assert "unknown" in result.output.lower()


class TestTerminalToolPersistent:
    @pytest.fixture
    async def manager(self):
        tm = TerminalManager(max_idle_seconds=1)
        tm.start()
        yield tm
        # cleanup
        await tm.shutdown()

    @pytest.fixture
    def tool(self, manager):
        return TerminalTool(terminal_manager=manager)

    async def test_open_foreground_closes(self, tool, manager):
        """Foreground open should auto-close after gathering output."""
        from navi.tools._internal.base import ToolContext

        ctx = ToolContext(session_id="s1", stop_event=None, model="test")
        result = await tool.execute(
            {
                "action": "open",
                "terminal_name": "t1",
                "description": "test terminal",
                "command": "echo hello",
            },
            ctx=ctx,
        )
        assert result.success
        assert result.metadata["name"] == "t1"
        assert "hello" in result.output

        # Foreground terminal should be auto-closed
        result = await tool.execute({"action": "list"}, ctx=ctx)
        assert result.success
        assert "No active terminals" in result.output

    async def test_open_background_list_close(self, tool, manager):
        """Background open should persist until explicit close."""
        from navi.tools._internal.base import ToolContext

        ctx = ToolContext(session_id="s2", stop_event=None, model="test")
        result = await tool.execute(
            {
                "action": "open",
                "terminal_name": "bg1",
                "description": "bg test",
                "command": "sleep 10",
                "background": True,
            },
            ctx=ctx,
        )
        assert result.success
        assert "background" in result.output.lower()
        assert result.metadata["name"] == "bg1"

        # list should show it
        result = await tool.execute({"action": "list"}, ctx=ctx)
        assert result.success
        assert "bg1" in result.output
        assert "bg test" in result.output

        # status should show busy
        result = await tool.execute(
            {"action": "status", "terminal_name": "bg1"},
            ctx=ctx,
        )
        assert result.success
        assert "busy" in result.output.lower()

        # close it
        result = await tool.execute(
            {"action": "close", "terminal_name": "bg1"},
            ctx=ctx,
        )
        assert result.success

        result = await tool.execute({"action": "list"}, ctx=ctx)
        assert result.success
        assert "No active terminals" in result.output

    async def test_send_input(self, tool, manager):
        from navi.tools._internal.base import ToolContext

        ctx = ToolContext(session_id="s3", stop_event=None, model="test")
        result = await tool.execute(
            {
                "action": "open",
                "terminal_name": "in1",
                "description": "input test",
                "command": "cat",
                "background": True,
            },
            ctx=ctx,
        )
        assert result.success

        result = await tool.execute(
            {"action": "send_input", "terminal_name": "in1", "input": "hello\n"},
            ctx=ctx,
        )
        assert result.success

        # give cat a moment to echo back
        await asyncio.sleep(0.2)

        result = await tool.execute(
            {"action": "status", "terminal_name": "in1"},
            ctx=ctx,
        )
        assert result.success
        assert "hello" in result.output

        await tool.execute({"action": "close", "terminal_name": "in1"}, ctx=ctx)

    async def test_no_manager(self):
        tool = TerminalTool(terminal_manager=None)
        from navi.tools._internal.base import ToolContext

        ctx = ToolContext(session_id="s4", stop_event=None, model="test")
        result = await tool.execute(
            {"action": "open", "terminal_name": "x", "description": "d", "command": "echo hi"},
            ctx=ctx,
        )
        assert not result.success
        assert "manager" in result.output.lower()

    async def test_open_missing_name(self, tool):
        from navi.tools._internal.base import ToolContext

        ctx = ToolContext(session_id="s5", stop_event=None, model="test")
        result = await tool.execute(
            {"action": "open", "description": "d", "command": "echo hi"},
            ctx=ctx,
        )
        assert not result.success
        assert "missing" in result.output.lower()

    async def test_max_terminals(self, tool, manager):
        """Opening more than _MAX_TERMINALS_PER_SESSION should fail."""
        from navi.tools._internal.base import ToolContext

        ctx = ToolContext(session_id="s6", stop_event=None, model="test")

        for i in range(_MAX_TERMINALS_PER_SESSION):
            result = await tool.execute(
                {
                    "action": "open",
                    "terminal_name": f"t{i}",
                    "description": f"term {i}",
                    "command": "sleep 30",
                    "background": True,
                },
                ctx=ctx,
            )
            assert result.success, f"Failed to open terminal {i}"

        # Next one should fail
        result = await tool.execute(
            {
                "action": "open",
                "terminal_name": "overflow",
                "description": "overflow",
                "command": "sleep 30",
                "background": True,
            },
            ctx=ctx,
        )
        assert not result.success
        assert "max" in result.output.lower()

        # Cleanup
        for i in range(_MAX_TERMINALS_PER_SESSION):
            await tool.execute({"action": "close", "terminal_name": f"t{i}"}, ctx=ctx)