mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-03 07:13:33 +00:00
Reminder hook now also counts and fires on usages of serena's non-symbolic tools
This commit is contained in:
1 parent
a4092ca276
commit
ab98ea6762
3 files changed
+68
-49
No files matched your search
@@ -36,6 +36,7 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
`permission_mode == "auto"`, in addition to the existing `acceptEdits` behavior. #1386
|
||||
- Extension: heuristics for parsing commands and firing a hook on too many greps or reads. Important for clients that, unlike claude code, don't have dedicated grep/read tools.
|
||||
- Read hook now only fires on reads of code files (using heuristics to parse the read command string)
|
||||
- Reminder hook now also counts and fires on usages of serena's non-symbolic tools.
|
||||
|
||||
# v1.2.0 (2026-04-27)
|
||||
|
||||
|
||||
+28
-10
@@ -47,6 +47,22 @@ class Hook(ABC):
|
||||
|
||||
|
||||
class PreToolUseHook(Hook, ABC):
|
||||
_NON_SYMBOLIC_SERENA_TOOL_NAME_SUBSTRINGS = frozenset(
|
||||
(
|
||||
"pattern",
|
||||
"read",
|
||||
"diagnostics",
|
||||
"memory",
|
||||
"onboarding",
|
||||
"config",
|
||||
"list_file",
|
||||
"find_file",
|
||||
"shell",
|
||||
"dashboard",
|
||||
"restart_language_server",
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, client: HookClient):
|
||||
super().__init__(client)
|
||||
_tool_name = self._input_data.get("tool_name") or self._input_data.get("toolName", "") or ""
|
||||
@@ -78,11 +94,13 @@ class PreToolUseHook(Hook, ABC):
|
||||
hook_output["hookSpecificOutput"]["additionalContext"] = self.additional_context
|
||||
return json.dumps(hook_output)
|
||||
|
||||
def is_serena_tool(self) -> bool:
|
||||
return "serena" in self._tool_name
|
||||
def is_serena_symbolic_tool(self) -> bool:
|
||||
return "serena" in self._tool_name and not any(
|
||||
substring in self._tool_name for substring in self._NON_SYMBOLIC_SERENA_TOOL_NAME_SUBSTRINGS
|
||||
)
|
||||
|
||||
|
||||
class PreToolUseRemindAboutSerenaHook(PreToolUseHook):
|
||||
class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
|
||||
"""Pre-tool-use hook that nudges the agent toward Serena's symbolic tools.
|
||||
|
||||
Tracks consecutive uses of grep and read-file tools via a persisted
|
||||
@@ -185,8 +203,8 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def update(self, hook: "PreToolUseRemindAboutSerenaHook") -> None:
|
||||
if hook.is_serena_tool():
|
||||
def update(self, hook: "PreToolUseRemindAboutSymbolicToolsHook") -> None:
|
||||
if hook.is_serena_symbolic_tool():
|
||||
self.reset()
|
||||
return
|
||||
|
||||
@@ -246,7 +264,7 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook):
|
||||
|
||||
#: Shell commands that perform grep-like search; used to classify Codex
|
||||
#: shell-command tool calls whose ``cmd`` or ``command`` field starts with one of these.
|
||||
_GREP_SHELL_COMMANDS: frozenset[str] = frozenset(("grep", "rg", "ag", "ack", "fgrep", "egrep"))
|
||||
_GREP_SHELL_COMMANDS: frozenset[str] = frozenset(("grep", "rg", "ag", "ack", "fgrep", "egrep", "search_for_pattern"))
|
||||
|
||||
#: Shell commands that perform file-read operations; used to classify Codex
|
||||
#: shell-command tool calls whose ``cmd`` or ``command`` field starts with one of these.
|
||||
@@ -330,7 +348,7 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook):
|
||||
|
||||
def is_grep_call(self) -> bool:
|
||||
if self._client == HookClient.CLAUDE_CODE:
|
||||
return self._tool_name == "grep"
|
||||
return self._tool_name == "grep" or "search_for_pattern" in self._tool_name
|
||||
if self._client == HookClient.CODEX and self._is_shell_command_call():
|
||||
return self._command_name in self._GREP_SHELL_COMMANDS
|
||||
# heuristic for other clients
|
||||
@@ -338,7 +356,7 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook):
|
||||
|
||||
def is_read_call(self) -> bool:
|
||||
if self._client == HookClient.CLAUDE_CODE:
|
||||
return self._tool_name == "read"
|
||||
return self._tool_name == "read" or "read_file" in self._tool_name
|
||||
if self._client == HookClient.CODEX and self._is_shell_command_call():
|
||||
return self._command_name in self._READ_SHELL_COMMANDS
|
||||
# heuristic for other clients
|
||||
@@ -516,7 +534,7 @@ class PreToolUseAutoApproveSerenaHook(PreToolUseHook):
|
||||
|
||||
def execute(self) -> None:
|
||||
# only emit a decision when both the tool and the mode match; stay silent otherwise
|
||||
if not self.is_serena_tool() or not self.is_auto_approve_mode():
|
||||
if not self.is_serena_symbolic_tool() or not self.is_auto_approve_mode():
|
||||
return
|
||||
|
||||
# name the actual mode in the reason so logs/debug output are unambiguous
|
||||
@@ -563,7 +581,7 @@ class HookCommands(AutoRegisteringGroup):
|
||||
)
|
||||
@_client_option
|
||||
def remind(client: str) -> None:
|
||||
PreToolUseRemindAboutSerenaHook(HookClient(client)).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient(client)).execute()
|
||||
|
||||
@staticmethod
|
||||
@click.command(
|
||||
|
||||
+39
-39
@@ -11,12 +11,12 @@ from click.testing import CliRunner
|
||||
from serena.hooks import (
|
||||
HookClient,
|
||||
PreToolUseAutoApproveSerenaHook,
|
||||
PreToolUseRemindAboutSerenaHook,
|
||||
PreToolUseRemindAboutSymbolicToolsHook,
|
||||
SessionEndCleanupHook,
|
||||
hook_commands,
|
||||
)
|
||||
|
||||
ToolUseCounter = PreToolUseRemindAboutSerenaHook.ToolUseCounter
|
||||
ToolUseCounter = PreToolUseRemindAboutSymbolicToolsHook.ToolUseCounter
|
||||
|
||||
|
||||
def _make_stdin(data: dict) -> StringIO:
|
||||
@@ -50,13 +50,13 @@ class TestHookClientDetection:
|
||||
def test_claude_code_client(self, tmp_path: Path):
|
||||
stdin_data = _base_input()
|
||||
with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE)
|
||||
assert hook._client == HookClient.CLAUDE_CODE
|
||||
|
||||
def test_vscode_client(self, tmp_path: Path):
|
||||
stdin_data = _base_input()
|
||||
with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.VSCODE)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.VSCODE)
|
||||
assert hook._client == HookClient.VSCODE
|
||||
|
||||
|
||||
@@ -67,26 +67,26 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
stdin_data = {"session_id": "s1"}
|
||||
with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
with pytest.raises(ValueError, match="Tool name is required"):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE)
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE)
|
||||
|
||||
def test_missing_session_id_raises(self, tmp_path: Path):
|
||||
stdin_data = {"tool_name": "grep"}
|
||||
with patch("sys.stdin", _make_stdin(stdin_data)), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
with pytest.raises(ValueError, match="Session ID is required"):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE)
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE)
|
||||
|
||||
def test_grep_tool_detection_claude_code(self, tmp_path: Path):
|
||||
"""Claude Code uses the exact tool name ``Grep`` (lowercased to ``grep``)."""
|
||||
for name, expected in [("grep", True), ("grep_search", False), ("mcp_grep", False), ("read", False)]:
|
||||
with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE)
|
||||
assert hook.is_grep_call() == expected, f"is_grep_tool() wrong for {name} (claude-code)"
|
||||
|
||||
def test_grep_tool_detection_non_claude_code(self, tmp_path: Path):
|
||||
"""Non-Claude-Code clients fall back to substring matching to cover verbose tool names."""
|
||||
for name, expected in [("grep_search", True), ("mcp_grep", True), ("read_file", False), ("serena_find", False)]:
|
||||
with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.VSCODE)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.VSCODE)
|
||||
assert hook.is_grep_call() == expected, f"is_grep_tool() wrong for {name} (vscode)"
|
||||
|
||||
def test_grep_tool_detection_codex_shell_commands(self, tmp_path: Path):
|
||||
@@ -101,17 +101,17 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
patch("sys.stdin", _make_stdin(_base_input(tool_name=tool_name, tool_input=tool_input))),
|
||||
patch("serena.hooks.serena_home_dir", str(tmp_path)),
|
||||
):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.CODEX)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.CODEX)
|
||||
assert hook.is_grep_call() == expected, f"is_grep_tool() wrong for {tool_name} / {tool_input}"
|
||||
|
||||
def test_read_file_tool_detection_claude_code(self, tmp_path: Path):
|
||||
"""Claude Code uses the exact tool name ``Read`` (lowercased to ``read``)."""
|
||||
for name, expected in [("read", True), ("read_file", False), ("readFile", False), ("grep", False)]:
|
||||
for name, expected in [("read", True), ("mcp__serena__read_file", True), ("grep", False), ("serena_search_for_pattern", False)]:
|
||||
with (
|
||||
patch("sys.stdin", _make_stdin(_read_input(tool_name=name))),
|
||||
patch("serena.hooks.serena_home_dir", str(tmp_path)),
|
||||
):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE)
|
||||
assert hook.is_read_file_call() == expected, f"is_read_file_tool() wrong for {name} (claude-code)"
|
||||
|
||||
def test_read_file_tool_detection_non_claude_code(self, tmp_path: Path):
|
||||
@@ -137,14 +137,14 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
patch("sys.stdin", _make_stdin(_read_input(tool_name=name))),
|
||||
patch("serena.hooks.serena_home_dir", str(tmp_path)),
|
||||
):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.VSCODE)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.VSCODE)
|
||||
assert hook.is_read_file_call() == expected, f"is_read_file_tool() wrong for {name} (vscode)"
|
||||
|
||||
def test_read_non_code_file_counts_as_file_read(self, tmp_path: Path):
|
||||
"""A ``Read`` call against a non-source file still counts as a file read."""
|
||||
payload = _read_input(tool_name="read", file_path="notes/todo.txt")
|
||||
with patch("sys.stdin", _make_stdin(payload)), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE)
|
||||
assert hook.is_read_call() is True
|
||||
assert hook.is_read_file_call() is True
|
||||
assert hook.is_read_code_file_call() is False
|
||||
@@ -159,7 +159,7 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
]
|
||||
for client, payload, expected in cases:
|
||||
with patch("sys.stdin", _make_stdin(payload)), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
hook = PreToolUseRemindAboutSerenaHook(client)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(client)
|
||||
assert hook.is_read_code_file_call() == expected, f"is_read_code_file_call() wrong for {client} / {payload}"
|
||||
|
||||
def test_read_file_tool_detection_codex_shell_commands(self, tmp_path: Path):
|
||||
@@ -174,27 +174,27 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
patch("sys.stdin", _make_stdin(_base_input(tool_name=tool_name, tool_input=tool_input))),
|
||||
patch("serena.hooks.serena_home_dir", str(tmp_path)),
|
||||
):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.CODEX)
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.CODEX)
|
||||
assert hook.is_read_file_call() == expected, f"is_read_file_tool() wrong for {tool_name} / {tool_input}"
|
||||
|
||||
def test_serena_tool_detection(self, tmp_path: Path):
|
||||
for name, expected in [("mcp_serena_find_symbol", True), ("serena_overview", True), ("grep_search", False)]:
|
||||
with patch("sys.stdin", _make_stdin(_base_input(tool_name=name))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
hook = PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE)
|
||||
assert hook.is_serena_tool() == expected, f"is_serena_tool() wrong for {name}"
|
||||
hook = PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE)
|
||||
assert hook.is_serena_symbolic_tool() == expected, f"is_serena_tool() wrong for {name}"
|
||||
|
||||
def test_no_output_below_threshold(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]):
|
||||
"""Below the threshold, the hook should produce no output (tool is allowed)."""
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD - 1):
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
def test_deny_output_after_threshold_greps_claude_code(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]):
|
||||
"""After reaching the grep threshold, the hook should output a deny."""
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
|
||||
output = capsys.readouterr().out.strip()
|
||||
result = json.loads(output)
|
||||
@@ -206,7 +206,7 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
"""After reaching the grep threshold, the hook should output a deny for VS Code."""
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep_search"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.VSCODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.VSCODE).execute()
|
||||
|
||||
output = capsys.readouterr().out.strip()
|
||||
result = json.loads(output)
|
||||
@@ -219,7 +219,7 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
payload = _base_input("functions.shell_command", tool_input={"command": "rg -n foo README.md"})
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(payload)), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CODEX).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CODEX).execute()
|
||||
|
||||
output = capsys.readouterr().out.strip()
|
||||
result = json.loads(output)
|
||||
@@ -235,7 +235,7 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
"""
|
||||
for _ in range(ToolUseCounter._READ_FILE_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(_read_input("read"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
|
||||
output = capsys.readouterr().out.strip()
|
||||
result = json.loads(output)
|
||||
@@ -249,7 +249,7 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
payload = _base_input("functions.shell_command", tool_input={"command": "Get-Content README.md"})
|
||||
for _ in range(ToolUseCounter._READ_FILE_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(payload)), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CODEX).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CODEX).execute()
|
||||
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
@@ -258,7 +258,7 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
payload = _base_input("functions.shell_command", tool_input={"command": "Get-Content src/foo.py"})
|
||||
for _ in range(ToolUseCounter._READ_FILE_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(payload)), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CODEX).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CODEX).execute()
|
||||
|
||||
result = json.loads(capsys.readouterr().out.strip())
|
||||
hook_output = result["hookSpecificOutput"]
|
||||
@@ -270,13 +270,13 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
"""Using a Serena tool should reset counters, so the threshold is not reached."""
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD - 1):
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
|
||||
with patch("sys.stdin", _make_stdin(_base_input("mcp_serena_find_symbol"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
@@ -284,11 +284,11 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
"""After a deny is emitted, the counter is reset so the next burst starts fresh."""
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
capsys.readouterr()
|
||||
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
@@ -299,12 +299,12 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
# first burst: should emit a deny
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
first_output = capsys.readouterr().out.strip()
|
||||
assert first_output, "first burst should have emitted a deny"
|
||||
|
||||
# snapshot the persisted counter immediately after the deny was emitted
|
||||
stub_for_path = object.__new__(PreToolUseRemindAboutSerenaHook)
|
||||
stub_for_path = object.__new__(PreToolUseRemindAboutSymbolicToolsHook)
|
||||
stub_for_path.session_persistence_dir = str(tmp_path / "hook_data" / _base_input()["session_id"])
|
||||
counter_before = ToolUseCounter.load(stub_for_path)
|
||||
|
||||
@@ -312,7 +312,7 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
# hook must short-circuit — no deny output and no counter mutation
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
@@ -324,11 +324,11 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
# first burst: emits a deny
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
capsys.readouterr()
|
||||
|
||||
# backdate the persisted last_deny_timestamp so the rate-limit window has expired
|
||||
stub_for_path = object.__new__(PreToolUseRemindAboutSerenaHook)
|
||||
stub_for_path = object.__new__(PreToolUseRemindAboutSymbolicToolsHook)
|
||||
stub_for_path.session_persistence_dir = str(tmp_path / "hook_data" / _base_input()["session_id"])
|
||||
counter = ToolUseCounter.load(stub_for_path)
|
||||
assert counter.last_deny_timestamp is not None
|
||||
@@ -338,7 +338,7 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
# second burst should now emit a deny again
|
||||
for _ in range(ToolUseCounter._GREP_USES_THRESHOLD):
|
||||
with patch("sys.stdin", _make_stdin(_base_input("grep"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
|
||||
second_output = capsys.readouterr().out.strip()
|
||||
assert second_output, "after the rate-limit window elapsed, a new burst should emit a deny"
|
||||
@@ -360,14 +360,14 @@ class TestPreToolUseRemindAboutSerenaHook:
|
||||
last_read_file_use_timestamp=datetime.now(),
|
||||
last_non_symbolic_use_timestamp=datetime.now(),
|
||||
)
|
||||
stub_for_path = object.__new__(PreToolUseRemindAboutSerenaHook)
|
||||
stub_for_path = object.__new__(PreToolUseRemindAboutSymbolicToolsHook)
|
||||
stub_for_path.session_persistence_dir = str(session_dir)
|
||||
counter.save(stub_for_path)
|
||||
|
||||
# invoke execute with a neutral (non-grep, non-read, non-serena) tool so that
|
||||
# update() leaves the counters untouched and the fall-through branch is taken
|
||||
with patch("sys.stdin", _make_stdin(_base_input("Edit"))), patch("serena.hooks.serena_home_dir", str(tmp_path)):
|
||||
PreToolUseRemindAboutSerenaHook(HookClient.CLAUDE_CODE).execute()
|
||||
PreToolUseRemindAboutSymbolicToolsHook(HookClient.CLAUDE_CODE).execute()
|
||||
|
||||
output = capsys.readouterr().out.strip()
|
||||
assert output, "expected a non-symbolic deny to be emitted"
|
||||
@@ -522,7 +522,7 @@ class TestToolUseCounter:
|
||||
timestamp: datetime,
|
||||
file_path: str | None = None,
|
||||
command_args_str: str | None = None,
|
||||
) -> PreToolUseRemindAboutSerenaHook:
|
||||
) -> PreToolUseRemindAboutSymbolicToolsHook:
|
||||
"""Create a minimal stub that satisfies ToolUseCounter.update without reading stdin.
|
||||
|
||||
Uses ``HookClient.VSCODE`` so that ``is_grep_tool`` / ``is_read_file_tool`` apply the
|
||||
@@ -535,7 +535,7 @@ class TestToolUseCounter:
|
||||
:param command_args_str: optional shell-argument tail; equivalent to ``file_path`` but
|
||||
populates the ``cmd``-derived field instead of ``file_path``.
|
||||
"""
|
||||
stub = object.__new__(PreToolUseRemindAboutSerenaHook)
|
||||
stub = object.__new__(PreToolUseRemindAboutSymbolicToolsHook)
|
||||
stub._tool_name = tool_name.lower()
|
||||
stub._client = HookClient.VSCODE
|
||||
stub.triggered_at_timestamp = timestamp
|
||||
|
||||
Reference in new issue
Block a user