fix: stop masking runtime failures
This commit is contained in:
@@ -260,6 +260,42 @@ class TestConsolidatorArchiveErrorHandling:
|
||||
assert len(entries) == 1
|
||||
assert "[RAW]" not in entries[0]["content"]
|
||||
|
||||
async def test_archive_propagates_history_write_failure(
|
||||
self, consolidator, mock_provider, runtime
|
||||
):
|
||||
mock_provider.chat_with_retry.return_value = MagicMock(
|
||||
content="Summary.",
|
||||
finish_reason="stop",
|
||||
)
|
||||
consolidator.store.append_history = MagicMock(side_effect=OSError("disk full"))
|
||||
consolidator.store.raw_archive = MagicMock()
|
||||
|
||||
with pytest.raises(OSError, match="disk full"):
|
||||
await consolidator.archive(
|
||||
[{"role": "user", "content": "important"}],
|
||||
runtime=runtime,
|
||||
)
|
||||
|
||||
consolidator.store.raw_archive.assert_not_called()
|
||||
|
||||
async def test_archive_propagates_template_failure_without_raw_archive(
|
||||
self, consolidator, mock_provider, runtime, monkeypatch
|
||||
):
|
||||
consolidator.store.raw_archive = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"nanobot.agent.memory.render_template",
|
||||
MagicMock(side_effect=RuntimeError("template failed")),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="template failed"):
|
||||
await consolidator.archive(
|
||||
[{"role": "user", "content": "important"}],
|
||||
runtime=runtime,
|
||||
)
|
||||
|
||||
mock_provider.chat_with_retry.assert_not_awaited()
|
||||
consolidator.store.raw_archive.assert_not_called()
|
||||
|
||||
|
||||
class TestConsolidatorTokenBudget:
|
||||
async def test_prompt_below_threshold_does_not_consolidate(
|
||||
@@ -276,6 +312,17 @@ class TestConsolidatorTokenBudget:
|
||||
await consolidator.maybe_consolidate_by_tokens(session, runtime=runtime)
|
||||
consolidator.archive.assert_not_called()
|
||||
|
||||
async def test_token_estimation_failure_propagates(self, consolidator, runtime):
|
||||
session = Session(key="test:estimate-failure")
|
||||
session.add_message("user", "hello")
|
||||
consolidator.sessions._session_cache[session.key] = session
|
||||
consolidator.estimate_session_prompt_tokens = MagicMock(
|
||||
side_effect=RuntimeError("counter failed")
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="counter failed"):
|
||||
await consolidator.maybe_consolidate_by_tokens(session, runtime=runtime)
|
||||
|
||||
async def test_estimate_uses_full_unconsolidated_tail(self, consolidator, runtime):
|
||||
"""Consolidation pressure must see messages hidden by the replay window."""
|
||||
session = Session(key="test:full-tail")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Tests for GitStore — git-backed version control for memory files."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from nanobot.utils.gitstore import CommitInfo, GitStore
|
||||
from nanobot.utils.gitstore import CommitInfo, GitStore, GitStoreError
|
||||
|
||||
TRACKED = ["SOUL.md", "USER.md", "memory/MEMORY.md"]
|
||||
|
||||
@@ -49,6 +50,11 @@ class TestInit:
|
||||
assert len(commits) == 1
|
||||
assert "init" in commits[0].message
|
||||
|
||||
def test_init_failure_is_explicit(self, git):
|
||||
with patch("dulwich.porcelain.init", side_effect=OSError("cannot initialize")):
|
||||
with pytest.raises(GitStoreError, match="init failed"):
|
||||
git.init()
|
||||
|
||||
|
||||
class TestBuildGitignore:
|
||||
def test_subdirectory_dirs(self, git):
|
||||
@@ -97,6 +103,11 @@ class TestAutoCommit:
|
||||
git_ready.auto_commit("nothing 2")
|
||||
assert len(git_ready.log()) == 1 # only init commit
|
||||
|
||||
def test_status_failure_is_explicit(self, git_ready):
|
||||
with patch("dulwich.porcelain.status", side_effect=OSError("broken index")):
|
||||
with pytest.raises(GitStoreError, match="auto-commit failed"):
|
||||
git_ready.auto_commit("update")
|
||||
|
||||
|
||||
class TestLog:
|
||||
def test_empty_when_not_initialized(self, git):
|
||||
|
||||
@@ -58,17 +58,11 @@ def _make_loop(tmp_path):
|
||||
return loop
|
||||
|
||||
|
||||
async def test_runner_uses_raw_messages_when_context_governance_fails():
|
||||
async def test_runner_propagates_context_governance_failure():
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
|
||||
provider = MagicMock()
|
||||
captured_messages: list[dict] = []
|
||||
|
||||
async def chat_with_retry(*, messages, **kwargs):
|
||||
captured_messages[:] = messages
|
||||
return LLMResponse(content="done", tool_calls=[], usage={})
|
||||
|
||||
provider.chat_with_retry = chat_with_retry
|
||||
provider.chat_with_retry = AsyncMock()
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
initial_messages = [
|
||||
@@ -80,16 +74,16 @@ async def test_runner_uses_raw_messages_when_context_governance_fails():
|
||||
runner.context_governor.prepare_for_model = MagicMock( # type: ignore[method-assign]
|
||||
side_effect=RuntimeError("boom")
|
||||
)
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=initial_messages,
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
max_iterations=1,
|
||||
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
|
||||
))
|
||||
with pytest.raises(RuntimeError, match="boom"):
|
||||
await runner.run(make_run_spec(provider,
|
||||
initial_messages=initial_messages,
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
max_iterations=1,
|
||||
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
|
||||
))
|
||||
|
||||
assert result.final_content == "done"
|
||||
assert captured_messages == initial_messages
|
||||
provider.chat_with_retry.assert_not_awaited()
|
||||
|
||||
|
||||
def test_snip_history_drops_orphaned_tool_results_from_trimmed_slice(monkeypatch):
|
||||
|
||||
@@ -149,6 +149,30 @@ def _tool_message(result, tool_call_id: str) -> dict:
|
||||
][0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_propagates_tool_preparation_failure():
|
||||
tools = MagicMock()
|
||||
tools.prepare_call.side_effect = RuntimeError("tool preparation failed")
|
||||
tools.execute = AsyncMock()
|
||||
|
||||
with pytest.raises(RuntimeError, match="tool preparation failed"):
|
||||
await AgentRunner()._run_tool(
|
||||
make_run_spec(
|
||||
MagicMock(),
|
||||
initial_messages=[],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
max_iterations=1,
|
||||
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
|
||||
),
|
||||
ToolCallRequest(id="call-1", name="demo", arguments={}),
|
||||
{},
|
||||
{},
|
||||
)
|
||||
|
||||
tools.execute.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_batches_read_only_tools_before_exclusive_work():
|
||||
tools = ToolRegistry()
|
||||
|
||||
@@ -349,6 +349,7 @@ class TestConsolidationUnaffectedByUnifiedSession:
|
||||
|
||||
session = Session(key="unified:default")
|
||||
session.messages = []
|
||||
sessions.get_or_create.return_value = session
|
||||
|
||||
await consolidator.maybe_consolidate_by_tokens(session, runtime=runtime)
|
||||
|
||||
@@ -378,6 +379,7 @@ class TestConsolidationUnaffectedByUnifiedSession:
|
||||
|
||||
session = Session(key=key)
|
||||
session.messages = [] # empty → exits immediately for both keys
|
||||
sessions.get_or_create.return_value = session
|
||||
|
||||
consolidator.archive = AsyncMock()
|
||||
await consolidator.maybe_consolidate_by_tokens(
|
||||
|
||||
Reference in New Issue
Block a user