Snapshot review_agent._session_messages before teardown so close() can clean per-session state without dropping the user-visible self-improvement summary. Adds two regressions: - bg-review summarizer receives captured review-agent tool messages after review_agent.close() runs - context-compressor protected-head handoff rehydration populates _previous_summary and keeps the old handoff out of newly summarized turns Salvaged from PR #26039 onto current main after agent/background_review.py extraction. Original commit 63eaf6055; bg-review test updated to patch the module-level summarize_background_review_actions in agent.background_review instead of the now-forwarder AIAgent._summarize_background_review_actions.
88 lines
3.5 KiB
Python
88 lines
3.5 KiB
Python
"""Regression tests for iterative context-summary continuity."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from agent.context_compressor import ContextCompressor, SUMMARY_PREFIX
|
|
|
|
|
|
def _compressor() -> ContextCompressor:
|
|
with patch("agent.context_compressor.get_model_context_length", return_value=100000):
|
|
return ContextCompressor(
|
|
model="test/model",
|
|
threshold_percent=0.85,
|
|
protect_first_n=1,
|
|
protect_last_n=1,
|
|
quiet_mode=True,
|
|
)
|
|
|
|
|
|
def _response(content: str):
|
|
mock_response = MagicMock()
|
|
mock_response.choices = [MagicMock()]
|
|
mock_response.choices[0].message.content = content
|
|
return mock_response
|
|
|
|
|
|
def _messages_with_handoff(summary_body: str):
|
|
return [
|
|
{"role": "system", "content": "system prompt"},
|
|
{"role": "user", "content": f"{SUMMARY_PREFIX}\n{summary_body}"},
|
|
{"role": "assistant", "content": "handoff acknowledged after resume"},
|
|
{"role": "user", "content": "new user turn after resume"},
|
|
{"role": "assistant", "content": "new assistant work after resume"},
|
|
{"role": "user", "content": "more new work after resume"},
|
|
{"role": "assistant", "content": "latest tail response"},
|
|
{"role": "user", "content": "final active request stays in protected tail"},
|
|
]
|
|
|
|
|
|
def test_existing_previous_summary_is_not_serialized_again_as_new_turn():
|
|
"""Same-process iterative compression should not feed the old handoff twice."""
|
|
compressor = _compressor()
|
|
old_summary = "OLD-SUMMARY-BODY unique continuity facts"
|
|
compressor._previous_summary = old_summary
|
|
|
|
with patch("agent.context_compressor.call_llm", return_value=_response("updated summary")) as mock_call:
|
|
compressor.compress(_messages_with_handoff(old_summary))
|
|
|
|
prompt = mock_call.call_args.kwargs["messages"][0]["content"]
|
|
assert "PREVIOUS SUMMARY:" in prompt
|
|
assert "NEW TURNS TO INCORPORATE:" in prompt
|
|
assert prompt.count(old_summary) == 1
|
|
assert f"[USER]: {SUMMARY_PREFIX}" not in prompt
|
|
|
|
|
|
def test_resume_rehydrates_previous_summary_from_handoff_message():
|
|
"""After restart/resume, the persisted handoff should regain summary identity."""
|
|
compressor = _compressor()
|
|
old_summary = "RESUMED-SUMMARY-BODY durable continuity facts"
|
|
assert compressor._previous_summary is None
|
|
|
|
with patch("agent.context_compressor.call_llm", return_value=_response("updated summary")) as mock_call:
|
|
compressor.compress(_messages_with_handoff(old_summary))
|
|
|
|
prompt = mock_call.call_args.kwargs["messages"][0]["content"]
|
|
assert "PREVIOUS SUMMARY:" in prompt
|
|
assert "NEW TURNS TO INCORPORATE:" in prompt
|
|
assert "TURNS TO SUMMARIZE:" not in prompt
|
|
assert prompt.count(old_summary) == 1
|
|
assert f"[USER]: {SUMMARY_PREFIX}" not in prompt
|
|
|
|
|
|
def test_handoff_in_protected_head_populates_previous_summary_before_update():
|
|
"""A resumed protected-head handoff should restore iterative-summary state."""
|
|
compressor = _compressor()
|
|
old_summary = "PROTECTED-HEAD-SUMMARY durable facts from before restart"
|
|
seen_turns = []
|
|
|
|
def fake_generate_summary(turns_to_summarize, focus_topic=None):
|
|
seen_turns.extend(turns_to_summarize)
|
|
return "new summary from resumed turns"
|
|
|
|
with patch.object(compressor, "_generate_summary", side_effect=fake_generate_summary):
|
|
compressor.compress(_messages_with_handoff(old_summary))
|
|
|
|
assert compressor._previous_summary == old_summary
|
|
assert seen_turns
|
|
assert all(old_summary not in str(msg.get("content", "")) for msg in seen_turns)
|