Newer
Older
navi-1 / tests / unit / tools / test_code_exec.py
"""Unit tests for code_exec tool."""

import pytest

from navi.tools.code_exec import CodeExecTool, _resolve_working_dir


class TestResolveWorkingDirSandbox:
    """Non-admin cwd lives in the sandbox or in the current session directory."""

    @pytest.fixture(autouse=True)
    def _areas(self, monkeypatch, tmp_path):
        import navi.session_files as _sf_mod
        from navi.config import Settings
        from navi.tools._internal.base import current_session_id

        monkeypatch.setattr(_sf_mod, "settings", Settings(session_files_dir=str(tmp_path / "sessions")))
        monkeypatch.chdir(tmp_path)
        self.root = tmp_path
        token = current_session_id.set("sess 1")
        yield
        current_session_id.reset(token)

    @property
    def sandbox(self):
        return (self.root / "user_data" / "7").resolve()

    def test_defaults_to_the_sandbox(self):
        assert _resolve_working_dir(None, "7", "user") == self.sandbox

    def test_relative_resolves_inside_the_sandbox(self):
        assert _resolve_working_dir("scripts", "7", "user") == self.sandbox / "scripts"

    def test_session_dir_is_allowed(self):
        session = self.root / "sessions" / "sess 1"
        session.mkdir(parents=True)
        assert _resolve_working_dir(str(session), "7", "user") == session.resolve()

    def test_path_outside_falls_back_to_the_sandbox(self):
        outside = self.root / "elsewhere"
        outside.mkdir()
        assert _resolve_working_dir(str(outside), "7", "user") == self.sandbox


class TestCodeExecTool:
    @pytest.fixture
    def tool(self):
        return CodeExecTool()

    async def test_hello_world(self, tool):
        result = await tool.execute({"code": "print('hello')"})
        assert result.success
        assert "hello" in result.output

    async def test_math(self, tool):
        result = await tool.execute({"code": "print(2 + 3)"})
        assert result.success
        assert "5" in result.output

    async def test_stderr(self, tool):
        result = await tool.execute({"code": "import sys; print('err', file=sys.stderr)"})
        # stderr is captured but the tool may or may not consider it an error
        assert "err" in (result.output or result.error or "")

    async def test_syntax_error(self, tool):
        result = await tool.execute({"code": "print("})
        assert not result.success

    async def test_metadata_carries_returncode_and_language(self, tool):
        result = await tool.execute({"code": "print('hi')"})
        assert result.success
        assert result.metadata["returncode"] == 0
        assert result.metadata["language"] == "python"

    async def test_timeout_param_clamped_to_max(self, tool):
        # 99999 -> clamped to _MAX_TIMEOUT (300). Fast code still succeeds; we
        # only assert the clamp path doesn't error and metadata is well-formed.
        result = await tool.execute({"code": "print('ok')", "timeout": 99999})
        assert result.success
        assert result.metadata["language"] == "python"

    async def test_timeout_param_triggers_timeout(self, tool):
        result = await tool.execute({"code": "import time; time.sleep(5)", "timeout": 1})
        assert not result.success
        assert result.error == "timeout"
        assert "timed out after 1s" in result.output
        assert result.metadata["language"] == "python"
        assert result.metadata["timeout"] == 1