Files
aiturk-hermes-ide/tests/agent/test_outbound_stale_vision.py
T

120 lines
4.2 KiB
Python

"""Send-path eviction of stale vision_analyze / screenshot tool payloads.
Issue #89296: compression only retires older image-bearing tool results when
prune/compress fires, so OpenAI-style screenshots are re-serialized on every
later turn until a 413. ``evict_stale_outbound_tool_images`` is the
unconditional per-call chokepoint.
"""
from __future__ import annotations
from agent.agent_runtime_helpers import sanitize_api_messages
from agent.context_compressor import (
_MAX_KEEP_TOOL_IMAGES,
_tool_content_has_images,
evict_stale_outbound_tool_images,
)
def _image_tool(i: int, *, blob: str = "A" * 80) -> list[dict]:
return [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": f"call_{i}",
"type": "function",
"function": {
"name": "vision_analyze",
"arguments": f'{{"image_url":"shot{i}.png"}}',
},
}
],
},
{
"role": "tool",
"tool_call_id": f"call_{i}",
"content": [
{"type": "text", "text": f"Image attached natively shot {i}"},
{
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{blob}{i}"},
},
],
},
]
def _history_with_screenshots(n: int) -> list[dict]:
msgs: list[dict] = [{"role": "user", "content": "look at these"}]
for i in range(n):
msgs.extend(_image_tool(i))
msgs.append({"role": "user", "content": "compare them"})
return msgs
def _image_bearing_tool_ids(messages: list[dict]) -> list[str]:
return [
m["tool_call_id"]
for m in messages
if m.get("role") == "tool" and _tool_content_has_images(m.get("content"))
]
class TestOutboundStaleVisionEviction:
def test_sanitize_alone_keeps_every_screenshot(self):
"""The previous send chokepoint does not close #89296 by itself."""
history = _history_with_screenshots(5)
sanitized = sanitize_api_messages(history)
assert _image_bearing_tool_ids(sanitized) == [f"call_{i}" for i in range(5)]
def test_eviction_keeps_only_newest_window(self):
history = _history_with_screenshots(5)
outbound = sanitize_api_messages(history)
pruned = evict_stale_outbound_tool_images(outbound)
assert pruned == 5 - _MAX_KEEP_TOOL_IMAGES
kept = _image_bearing_tool_ids(outbound)
assert kept == [f"call_{i}" for i in range(5 - _MAX_KEEP_TOOL_IMAGES, 5)]
oldest = next(m for m in outbound if m.get("tool_call_id") == "call_0")
assert isinstance(oldest["content"], list)
assert not _tool_content_has_images(oldest["content"])
assert any(
isinstance(part, dict)
and part.get("type") == "text"
and "screenshot removed" in str(part.get("text", ""))
for part in oldest["content"]
)
def test_does_not_rewrite_persisted_history(self):
from agent.conversation_loop import _clone_message_for_send
history = _history_with_screenshots(5)
outbound = [_clone_message_for_send(m) for m in history]
evict_stale_outbound_tool_images(outbound)
assert _image_bearing_tool_ids(history) == [f"call_{i}" for i in range(5)]
assert _image_bearing_tool_ids(outbound) == [
f"call_{i}" for i in range(5 - _MAX_KEEP_TOOL_IMAGES, 5)
]
def test_user_uploads_are_not_evicted(self):
history = [
{
"role": "user",
"content": [
{"type": "text", "text": "look"},
{
"type": "image_url",
"image_url": {"url": "data:image/png;base64,USERUPLOAD"},
},
],
}
]
for i in range(_MAX_KEEP_TOOL_IMAGES + 2):
history.extend(_image_tool(i))
outbound = sanitize_api_messages(history)
evict_stale_outbound_tool_images(outbound)
user = next(m for m in outbound if m.get("role") == "user")
assert user["content"][1]["image_url"]["url"].endswith("USERUPLOAD")