personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_openhands_read_tools.py
4.4 kB 140 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6from pathlib import Path 7 8import pytest 9 10from solstone.think.cogitate_read_tools import ( 11 glob, 12 grep_search, 13 list_directory, 14 read_file, 15) 16from solstone.think.providers import read_tools 17from tests.openhands_fakes import install_fake_openhands 18 19 20@pytest.fixture 21def fake_openhands(monkeypatch): 22 return install_fake_openhands(monkeypatch) 23 24 25def _build_journal(tmp_path: Path) -> Path: 26 journal = tmp_path / "journal" 27 (journal / "notes").mkdir(parents=True) 28 (journal / "notes" / "a.txt").write_text("hello x\n", encoding="utf-8") 29 (journal / ".git").mkdir() 30 (journal / ".git" / "config").write_text("[core]\n", encoding="utf-8") 31 (journal / "bin.dat").write_bytes(b"hello\x00x") 32 (tmp_path / "outside.txt").write_text("outside\n", encoding="utf-8") 33 (journal / "outside-link.txt").symlink_to(tmp_path / "outside.txt") 34 return journal 35 36 37def _build_tools(journal: Path, read_call_budget: int = 20) -> dict[str, object]: 38 read_tools._READ_TYPES.clear() 39 tools = read_tools.build_read_tools( 40 journal=journal, 41 read_call_budget=read_call_budget, 42 ) 43 return {tool.name: tool for tool in tools} 44 45 46def _read_file_args(path: str) -> dict[str, object]: 47 return {"path": path, "start_line": 1} 48 49 50def _list_directory_args(path: str = ".") -> dict[str, object]: 51 return { 52 "path": path, 53 "recursive": False, 54 "include_hidden": False, 55 "pattern": None, 56 } 57 58 59def test_read_tools_names_and_promoted_qualnames(fake_openhands, tmp_path): 60 journal = _build_journal(tmp_path) 61 tools = read_tools.build_read_tools(journal=journal, read_call_budget=20) 62 63 assert [tool.name for tool in tools] == [ 64 read_file.__name__, 65 list_directory.__name__, 66 glob.__name__, 67 grep_search.__name__, 68 ] 69 for tool in tools: 70 assert "<locals>" not in type(tool).__qualname__ 71 72 73def test_read_file_allowed_returns_file_content(fake_openhands, tmp_path): 74 journal = _build_journal(tmp_path) 75 tools = _build_tools(journal) 76 tool = tools[read_file.__name__] 77 78 observation = tool(tool.action_from_arguments(_read_file_args("notes/a.txt"))) 79 80 assert observation.is_error is False 81 assert "hello x" in observation.text 82 83 84def test_read_file_denies_blocked_component(fake_openhands, tmp_path): 85 journal = _build_journal(tmp_path) 86 tools = _build_tools(journal) 87 tool = tools[read_file.__name__] 88 89 observation = tool(tool.action_from_arguments(_read_file_args(".git/config"))) 90 91 assert observation.is_error is True 92 assert observation.text.startswith("denied_component:") 93 94 95def test_read_file_denies_path_escape_outside_journal(fake_openhands, tmp_path): 96 journal = _build_journal(tmp_path) 97 tools = _build_tools(journal) 98 tool = tools[read_file.__name__] 99 100 observation = tool(tool.action_from_arguments(_read_file_args("outside-link.txt"))) 101 102 assert observation.is_error is True 103 assert observation.text.startswith("path_escape:") 104 105 106def test_read_file_denies_binary_file(fake_openhands, tmp_path): 107 journal = _build_journal(tmp_path) 108 tools = _build_tools(journal) 109 tool = tools[read_file.__name__] 110 111 observation = tool(tool.action_from_arguments(_read_file_args("bin.dat"))) 112 113 assert observation.is_error is True 114 assert observation.text.startswith("binary_file:") 115 116 117def test_read_tools_share_one_cross_tool_budget(fake_openhands, tmp_path): 118 journal = _build_journal(tmp_path) 119 tools = _build_tools(journal, read_call_budget=1) 120 read_tool = tools[read_file.__name__] 121 list_tool = tools[list_directory.__name__] 122 123 first = read_tool(read_tool.action_from_arguments(_read_file_args("notes/a.txt"))) 124 second = list_tool(list_tool.action_from_arguments(_list_directory_args())) 125 126 assert first.is_error is False 127 assert second.is_error is True 128 assert second.text.startswith("budget_exhausted:") 129 130 131def test_list_directory_renders_newline_joined_paths(fake_openhands, tmp_path): 132 journal = _build_journal(tmp_path) 133 tools = _build_tools(journal) 134 tool = tools[list_directory.__name__] 135 136 observation = tool(tool.action_from_arguments(_list_directory_args())) 137 138 assert observation.is_error is False 139 assert "notes/" in observation.text 140 assert "Entry(" not in observation.text