137 lines
5.0 KiB
Python
137 lines
5.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
# pylint: disable=protected-access
|
|
"""Tests for ``CommandHandler``'s scroll-aware ``/compact`` wiring.
|
|
|
|
The standalone command handler builds a bare AgentScope ``Agent`` whose
|
|
``compress_context`` is always *native*. Under the scroll strategy it must
|
|
instead drive a ``ScrollContextManager`` — but only then. These pin that
|
|
branch (and the native no-op) without constructing a real model/agent.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from pineagents.agents.command_handler import CommandHandler
|
|
|
|
|
|
def _config(strategy="scroll"):
|
|
scroll_config = SimpleNamespace(db_filename="history.db")
|
|
lcc = SimpleNamespace(strategy=strategy, scroll_config=scroll_config)
|
|
running = SimpleNamespace(light_context_config=lcc)
|
|
return SimpleNamespace(running=running, id="ag1")
|
|
|
|
|
|
def _handler(tmp_path, *, strategy="scroll", workspace=True, monkeypatch):
|
|
state = SimpleNamespace(context=[], session_id="sess-x")
|
|
handler = CommandHandler(
|
|
agent_name="QwenPaw",
|
|
state=state,
|
|
agent_id="ag1",
|
|
workspace_dir=str(tmp_path) if workspace else None,
|
|
session_id="sess-x",
|
|
)
|
|
monkeypatch.setattr(
|
|
handler,
|
|
"_get_agent_config",
|
|
lambda: _config(strategy),
|
|
)
|
|
return handler
|
|
|
|
|
|
def test_builds_scroll_manager_under_scroll(tmp_path, monkeypatch):
|
|
handler = _handler(tmp_path, strategy="scroll", monkeypatch=monkeypatch)
|
|
mgr = handler._build_standalone_scroll_manager()
|
|
try:
|
|
assert mgr is not None
|
|
# Bound to the authoritative session id, not state.session_id.
|
|
assert mgr._session_id == "sess-x"
|
|
assert (tmp_path / "history.db").exists()
|
|
finally:
|
|
if mgr is not None:
|
|
mgr.close()
|
|
|
|
|
|
def test_native_strategy_stays_on_native(tmp_path, monkeypatch):
|
|
handler = _handler(tmp_path, strategy="native", monkeypatch=monkeypatch)
|
|
assert handler._build_standalone_scroll_manager() is None
|
|
|
|
|
|
def test_no_workspace_stays_on_native(tmp_path, monkeypatch):
|
|
handler = _handler(
|
|
tmp_path,
|
|
strategy="scroll",
|
|
workspace=False,
|
|
monkeypatch=monkeypatch,
|
|
)
|
|
assert handler._build_standalone_scroll_manager() is None
|
|
|
|
|
|
class _FakeCtxConfig:
|
|
"""Minimal stand-in for AgentScope ``ContextConfig`` (pydantic-like)."""
|
|
|
|
def __init__(self, trigger_ratio, reserve_ratio):
|
|
self.trigger_ratio = trigger_ratio
|
|
self.reserve_ratio = reserve_ratio
|
|
|
|
def model_copy(self, *, update):
|
|
merged = {
|
|
"trigger_ratio": self.trigger_ratio,
|
|
"reserve_ratio": self.reserve_ratio,
|
|
**update,
|
|
}
|
|
return _FakeCtxConfig(**merged)
|
|
|
|
|
|
def test_forced_compact_drops_trigger_keeps_reserve(monkeypatch):
|
|
"""Under scroll, manual /compact forces the trigger but keeps the
|
|
configured recent-tail reserve — same as auto compaction. /compact only
|
|
means "compact now", not "shrink the tail more aggressively"; a
|
|
conversation that already fits inside the reserve has nothing to evict."""
|
|
from pineagents.agents.command_handler import _FORCE_TRIGGER_RATIO
|
|
|
|
handler = CommandHandler(agent_name="QwenPaw")
|
|
monkeypatch.setattr(
|
|
handler,
|
|
"_get_agent_config",
|
|
lambda: _config("scroll"),
|
|
)
|
|
agent = SimpleNamespace(context_config=_FakeCtxConfig(0.8, 0.1))
|
|
forced = handler._forced_context_config(agent)
|
|
assert forced.trigger_ratio == _FORCE_TRIGGER_RATIO
|
|
# Reserve stays at the configured base — no scroll-specific shrink.
|
|
assert forced.reserve_ratio == 0.1
|
|
# The original is untouched (model_copy, not in-place mutation).
|
|
assert agent.context_config.reserve_ratio == 0.1
|
|
|
|
|
|
def test_forced_compact_under_native_keeps_reserve(monkeypatch):
|
|
"""Under native, manual /compact forces the trigger but must NOT shrink the
|
|
reserve. Native compaction is lossy (the non-reserved middle is summarized
|
|
away and the originals dropped), so it keeps the configured reserve to
|
|
preserve the same recent-tail continuity as auto compaction."""
|
|
from pineagents.agents.command_handler import _FORCE_TRIGGER_RATIO
|
|
|
|
handler = CommandHandler(agent_name="QwenPaw")
|
|
monkeypatch.setattr(
|
|
handler,
|
|
"_get_agent_config",
|
|
lambda: _config("native"),
|
|
)
|
|
agent = SimpleNamespace(context_config=_FakeCtxConfig(0.8, 0.1))
|
|
forced = handler._forced_context_config(agent)
|
|
assert forced.trigger_ratio == _FORCE_TRIGGER_RATIO
|
|
# Reserve stays at the configured base, not the scroll-only shrink.
|
|
assert forced.reserve_ratio == 0.1
|
|
|
|
|
|
def test_agent_backed_mode_never_builds_a_throwaway_manager():
|
|
"""When a live agent is present, /compact uses its own compress_context;
|
|
the standalone builder is irrelevant (and updated_scroll_state stays None
|
|
so the adapter preserves the live agent's own saved block)."""
|
|
from unittest.mock import MagicMock
|
|
|
|
agent = MagicMock()
|
|
agent.state = SimpleNamespace(context=[], session_id="s")
|
|
agent.memory_manager = None
|
|
handler = CommandHandler(agent_name="QwenPaw", agent=agent)
|
|
assert handler.updated_scroll_state is None
|