fix: isolate run-level hook snapshots

This commit is contained in:
chengyongru
2026-06-05 01:09:45 +08:00
committed by Xubin Ren
parent 8933da1ec5
commit 39454534d4
2 changed files with 133 additions and 7 deletions
+116
View File
@@ -239,6 +239,60 @@ async def test_runner_calls_run_level_hooks_on_success():
]
@pytest.mark.asyncio
async def test_runner_run_level_context_is_detached_snapshot():
from nanobot.agent.hook import AgentHook, AgentRunHookContext
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
call_count = {"n": 0}
request_messages: list[list[dict]] = []
async def chat_with_retry(**kwargs):
call_count["n"] += 1
request_messages.append([dict(msg) for msg in kwargs["messages"]])
if call_count["n"] == 1:
return LLMResponse(
content="thinking",
tool_calls=[ToolCallRequest(id="call_1", name="list_dir", arguments={"path": "."})],
)
return LLMResponse(content="done", tool_calls=[], usage={})
provider.chat_with_retry = chat_with_retry
tools = MagicMock()
tools.get_definitions.return_value = []
tools.execute = AsyncMock(return_value="tool result")
class MutatingRunHook(AgentHook):
async def before_run(self, context: AgentRunHookContext) -> None:
context.messages[0]["content"] = "mutated-before"
async def after_run(self, context: AgentRunHookContext) -> None:
context.messages[0]["content"] = "mutated-after"
context.tool_events[0]["status"] = "mutated"
context.tools_used.append("mutated")
async def on_finally(self, context: AgentRunHookContext) -> None:
context.messages[0]["content"] = "mutated-finally"
runner = AgentRunner(provider)
result = await runner.run(AgentRunSpec(
initial_messages=[{"role": "user", "content": "hi"}],
tools=tools,
model="test-model",
max_iterations=2,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
hook=MutatingRunHook(),
))
assert request_messages[0][0]["content"] == "hi"
assert result.messages[0]["content"] == "hi"
assert result.tools_used == ["list_dir"]
assert result.tool_events == [
{"name": "list_dir", "status": "ok", "detail": "tool result"}
]
@pytest.mark.asyncio
async def test_runner_calls_on_error_for_model_error_result():
from nanobot.agent.hook import AgentHook, AgentRunHookContext
@@ -338,6 +392,36 @@ async def test_runner_calls_on_error_and_finally_for_unhandled_exception():
]
@pytest.mark.asyncio
async def test_runner_preserves_original_exception_when_finally_hook_fails():
from nanobot.agent.hook import AgentHook, AgentRunHookContext
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
async def chat_with_retry(**kwargs):
raise RuntimeError("provider exploded")
provider.chat_with_retry = chat_with_retry
tools = MagicMock()
tools.get_definitions.return_value = []
class BadFinallyHook(AgentHook):
async def on_finally(self, context: AgentRunHookContext) -> None:
raise RuntimeError("finally exploded")
runner = AgentRunner(provider)
with pytest.raises(RuntimeError, match="provider exploded"):
await runner.run(AgentRunSpec(
initial_messages=[{"role": "user", "content": "hi"}],
tools=tools,
model="test-model",
max_iterations=1,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
hook=BadFinallyHook(),
))
@pytest.mark.asyncio
async def test_runner_does_not_report_cancellation_as_error():
import asyncio
@@ -388,3 +472,35 @@ async def test_runner_does_not_report_cancellation_as_error():
("before_run", None),
("on_finally", "cancelled", None, "CancelledError"),
]
@pytest.mark.asyncio
async def test_runner_preserves_cancellation_when_finally_hook_fails():
import asyncio
from nanobot.agent.hook import AgentHook, AgentRunHookContext
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
async def chat_with_retry(**kwargs):
raise asyncio.CancelledError()
provider.chat_with_retry = chat_with_retry
tools = MagicMock()
tools.get_definitions.return_value = []
class BadFinallyHook(AgentHook):
async def on_finally(self, context: AgentRunHookContext) -> None:
raise RuntimeError("finally exploded")
runner = AgentRunner(provider)
with pytest.raises(asyncio.CancelledError):
await runner.run(AgentRunSpec(
initial_messages=[{"role": "user", "content": "hi"}],
tools=tools,
model="test-model",
max_iterations=1,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
hook=BadFinallyHook(),
))