From 727086ddac2da71af7fcf48293850568a76e275c Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Sun, 26 Apr 2026 12:20:07 +0000 Subject: [PATCH] test: tighten consolidation ratio coverage Made-with: Cursor --- tests/agent/test_consolidation_ratio.py | 145 ++++++------------------ 1 file changed, 37 insertions(+), 108 deletions(-) diff --git a/tests/agent/test_consolidation_ratio.py b/tests/agent/test_consolidation_ratio.py index 8e621d358..b1c95ec4b 100644 --- a/tests/agent/test_consolidation_ratio.py +++ b/tests/agent/test_consolidation_ratio.py @@ -1,12 +1,14 @@ -"""Tests for the configurable consolidation_ratio feature.""" +"""Tests for configurable consolidation_ratio.""" from unittest.mock import AsyncMock, MagicMock import pytest +from pydantic import ValidationError -from nanobot.agent.loop import AgentLoop import nanobot.agent.memory as memory_module +from nanobot.agent.loop import AgentLoop from nanobot.bus.queue import MessageBus +from nanobot.config.schema import AgentDefaults from nanobot.providers.base import GenerationSettings, LLMResponse @@ -38,140 +40,67 @@ def _make_loop( return loop -@pytest.mark.asyncio -async def test_default_ratio_uses_half_budget(tmp_path, monkeypatch) -> None: - """With ratio=0.5 (default), target should be half of budget.""" - loop = _make_loop(tmp_path, context_window_tokens=200, consolidation_ratio=0.5) - loop.consolidator.archive = AsyncMock(return_value=True) # type: ignore[method-assign] - +def _session_with_turns(loop: AgentLoop, *, turns: int): session = loop.sessions.get_or_create("cli:test") - session.messages = [ - {"role": "user", "content": "u1", "timestamp": "2026-01-01T00:00:00"}, - {"role": "assistant", "content": "a1", "timestamp": "2026-01-01T00:00:01"}, - {"role": "user", "content": "u2", "timestamp": "2026-01-01T00:00:02"}, - {"role": "assistant", "content": "a2", "timestamp": "2026-01-01T00:00:03"}, - {"role": "user", "content": "u3", "timestamp": "2026-01-01T00:00:04"}, - {"role": "assistant", "content": "a3", "timestamp": "2026-01-01T00:00:05"}, - {"role": "user", "content": "u4", "timestamp": "2026-01-01T00:00:06"}, - ] - loop.sessions.save(session) - - # budget = 200 - 0 (max_tokens) - 0 (safety_buffer) = 200 - # target = int(200 * 0.5) = 100 - # estimated must be >= budget to trigger consolidation - call_count = [0] - - def mock_estimate(_session): - call_count[0] += 1 - if call_count[0] == 1: - return (250, "test") - return (90, "test") - - loop.consolidator.estimate_session_prompt_tokens = mock_estimate # type: ignore[method-assign] - monkeypatch.setattr(memory_module, "estimate_message_tokens", lambda _m: 100) - - await loop.consolidator.maybe_consolidate_by_tokens(session) - - # 250 >= 200 (budget, triggers) → 250 > 100 (target) → archive → 90 < 100, stops. - assert loop.consolidator.archive.await_count == 1 - - -@pytest.mark.asyncio -async def test_low_ratio_aggressively_consolidates(tmp_path, monkeypatch) -> None: - """With ratio=0.1, target is only 10% of budget — more rounds of archiving.""" - loop = _make_loop(tmp_path, context_window_tokens=1000, consolidation_ratio=0.1) - loop.consolidator.archive = AsyncMock(return_value=True) # type: ignore[method-assign] - - session = loop.sessions.get_or_create("cli:test") - # Interleave user/assistant so pick_consolidation_boundary can find boundaries session.messages = [] - for i in range(10): + for i in range(turns): session.messages.append({"role": "user", "content": f"u{i}", "timestamp": f"2026-01-01T00:00:{i:02d}"}) - session.messages.append({"role": "assistant", "content": f"a{i}", "timestamp": f"2026-01-01T00:00:{i:02d}"}) + session.messages.append({"role": "assistant", "content": f"a{i}", "timestamp": f"2026-01-01T00:01:{i:02d}"}) loop.sessions.save(session) - - # budget = 1000, target = int(1000 * 0.1) = 100 - call_count = [0] - - def mock_estimate(_session): - call_count[0] += 1 - if call_count[0] == 1: - return (1200, "test") - if call_count[0] == 2: - return (800, "test") - if call_count[0] == 3: - return (400, "test") - return (50, "test") - - loop.consolidator.estimate_session_prompt_tokens = mock_estimate # type: ignore[method-assign] - monkeypatch.setattr(memory_module, "estimate_message_tokens", lambda _m: 100) - - await loop.consolidator.maybe_consolidate_by_tokens(session) - - # With low ratio, more rounds needed to reach target; at least 2 rounds - assert loop.consolidator.archive.await_count >= 2 + return session @pytest.mark.asyncio -async def test_high_ratio_preserves_more_history(tmp_path, monkeypatch) -> None: - """With ratio=0.9, target is 90% of budget — consolidation stops sooner.""" - loop = _make_loop(tmp_path, context_window_tokens=200, consolidation_ratio=0.9) +@pytest.mark.parametrize( + ("ratio", "context_window_tokens", "estimates", "expected_archives"), + [ + (0.5, 200, [250, 90], 1), + (0.1, 1000, [1200, 800, 400, 50], 2), + (0.9, 200, [300, 175], 1), + ], +) +async def test_consolidation_ratio_controls_target( + tmp_path, + monkeypatch, + ratio: float, + context_window_tokens: int, + estimates: list[int], + expected_archives: int, +) -> None: + loop = _make_loop( + tmp_path, + context_window_tokens=context_window_tokens, + consolidation_ratio=ratio, + ) loop.consolidator.archive = AsyncMock(return_value=True) # type: ignore[method-assign] + session = _session_with_turns(loop, turns=10) - session = loop.sessions.get_or_create("cli:test") - session.messages = [ - {"role": "user", "content": "u1", "timestamp": "2026-01-01T00:00:00"}, - {"role": "assistant", "content": "a1", "timestamp": "2026-01-01T00:00:01"}, - {"role": "user", "content": "u2", "timestamp": "2026-01-01T00:00:02"}, - {"role": "assistant", "content": "a2", "timestamp": "2026-01-01T00:00:03"}, - {"role": "user", "content": "u3", "timestamp": "2026-01-01T00:00:04"}, - {"role": "assistant", "content": "a3", "timestamp": "2026-01-01T00:00:05"}, - {"role": "user", "content": "u4", "timestamp": "2026-01-01T00:00:06"}, - ] - loop.sessions.save(session) + remaining_estimates = list(estimates) - # budget = 200, target = int(200 * 0.9) = 180 - call_count = [0] - - def mock_estimate(_session): - call_count[0] += 1 - if call_count[0] == 1: - return (300, "test") - return (175, "test") + def mock_estimate(_session, *, session_summary=None): + assert session_summary is None + return (remaining_estimates.pop(0), "test") loop.consolidator.estimate_session_prompt_tokens = mock_estimate # type: ignore[method-assign] monkeypatch.setattr(memory_module, "estimate_message_tokens", lambda _m: 100) await loop.consolidator.maybe_consolidate_by_tokens(session) - # 300 >= 200 (triggers) → 300 > 180 → archive → 175 < 180 → stop - assert loop.consolidator.archive.await_count == 1 + assert loop.consolidator.archive.await_count == expected_archives -@pytest.mark.asyncio -async def test_ratio_propagated_from_config_schema() -> None: - """Verify consolidation_ratio is parsed from config with camelCase alias.""" - from nanobot.config.schema import AgentDefaults - - # Default +def test_ratio_propagated_from_config_schema() -> None: defaults = AgentDefaults() assert defaults.consolidation_ratio == 0.5 - # camelCase alias defaults = AgentDefaults.model_validate({"consolidationRatio": 0.3}) assert defaults.consolidation_ratio == 0.3 - # Serialization uses alias dumped = defaults.model_dump(by_alias=True) assert dumped["consolidationRatio"] == 0.3 -@pytest.mark.asyncio -async def test_ratio_validation_rejects_out_of_range() -> None: - """Invalid ratio values should be rejected by validation.""" - from pydantic import ValidationError - from nanobot.config.schema import AgentDefaults - +def test_ratio_validation_rejects_out_of_range() -> None: with pytest.raises(ValidationError): AgentDefaults(consolidation_ratio=0.05)