Newer
Older
navi-1 / tests / unit / tools / test_scratchpad.py
"""Unit tests for navi.tools.scratchpad."""

import pytest

from navi.tools.scratchpad import ScratchpadTool, get_section
from navi.tools._internal.base import ToolContext
from navi.store import KvStore
from tests.conftest_factory import FakePool


class FakeKvStore(KvStore):
    def __init__(self):
        self._data: dict[tuple, str] = {}

    async def _get_pool(self):
        return FakePool()

    async def get(self, user_id, session_id, scope, key):
        return self._data.get((user_id or "", session_id, scope, key))

    async def set(self, user_id, session_id, scope, key, value):
        self._data[(user_id or "", session_id, scope, key)] = value

    async def get_all(self, user_id, session_id, scope):
        return {
            k[3]: v
            for k, v in self._data.items()
            if k[:3] == (user_id or "", session_id, scope)
        }

    async def delete(self, user_id, session_id, scope, key):
        self._data.pop((user_id or "", session_id, scope, key), None)

    async def clear_scope(self, user_id, session_id, scope):
        keys = [k for k in self._data if k[:3] == (user_id or "", session_id, scope)]
        for k in keys:
            del self._data[k]


@pytest.fixture(autouse=True)
def _fake_kv():
    store = FakeKvStore()
    from navi.tools import scratchpad as _mod
    _mod._kv_store = store
    yield store
    _mod._kv_store = None


@pytest.mark.asyncio
async def test_write_then_read(_fake_kv):
    tool = ScratchpadTool()
    result = await tool.execute({"action": "write", "section": "findings", "content": "found X"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert result.success is True

    result = await tool.execute({"action": "read", "section": "findings"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert result.success is True
    assert "found X" in result.output


@pytest.mark.asyncio
async def test_append(_fake_kv):
    tool = ScratchpadTool()
    await tool.execute({"action": "write", "section": "findings", "content": "line1"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    result = await tool.execute({"action": "append", "section": "findings", "content": "line2"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert result.success is True

    result = await tool.execute({"action": "read", "section": "findings"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert "line1" in result.output
    assert "line2" in result.output


@pytest.mark.asyncio
async def test_read_all_sections(_fake_kv):
    tool = ScratchpadTool()
    await tool.execute({"action": "write", "section": "goal", "content": "do thing"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    await tool.execute({"action": "write", "section": "findings", "content": "found"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    result = await tool.execute({"action": "read"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert result.success is True
    assert "goal" in result.output
    assert "findings" in result.output


@pytest.mark.asyncio
async def test_clear_section(_fake_kv):
    tool = ScratchpadTool()
    await tool.execute({"action": "write", "section": "goal", "content": "do thing"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    result = await tool.execute({"action": "clear", "section": "goal"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert result.success is True

    result = await tool.execute({"action": "read", "section": "goal"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert "empty" in result.output.lower()


@pytest.mark.asyncio
async def test_scope_isolation(_fake_kv):
    tool = ScratchpadTool()
    await tool.execute({"action": "write", "section": "main", "content": "sess1 data"}, ctx=ToolContext(session_id="sess1", user_id="user1"))

    result = await tool.execute({"action": "read", "section": "main"}, ctx=ToolContext(session_id="sess2", user_id="user1"))
    assert "empty" in result.output.lower()


@pytest.mark.asyncio
async def test_get_section(_fake_kv):
    tool = ScratchpadTool()
    await tool.execute({"action": "write", "section": "artifacts", "content": "path/to/file"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    text = await get_section("sess1", "artifacts", user_id="user1")
    assert text == "path/to/file"


@pytest.mark.asyncio
async def test_legacy_op_fallback(_fake_kv):
    """Backward compatibility: old calls using 'op' instead of 'action' still work."""
    tool = ScratchpadTool()
    result = await tool.execute({"op": "write", "section": "legacy", "content": "old format"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert result.success is True

    result = await tool.execute({"action": "read", "section": "legacy"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert "old format" in result.output


@pytest.mark.asyncio
async def test_write_missing_content_gives_helpful_error(_fake_kv):
    tool = ScratchpadTool()
    result = await tool.execute({"action": "write", "section": "findings"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert result.success is False
    assert "content" in result.error.lower()
    assert "example" in result.error.lower()


@pytest.mark.asyncio
async def test_append_missing_content_gives_helpful_error(_fake_kv):
    tool = ScratchpadTool()
    result = await tool.execute({"action": "append", "section": "findings"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert result.success is False
    assert "content" in result.error.lower()
    assert "example" in result.error.lower()


@pytest.mark.asyncio
async def test_unknown_action_gives_helpful_error(_fake_kv):
    tool = ScratchpadTool()
    result = await tool.execute({"action": "delete", "section": "findings"}, ctx=ToolContext(session_id="sess1", user_id="user1"))
    assert result.success is False
    assert "expected one of" in result.error.lower()
    assert "example" in result.error.lower()