Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
"""Tests for post-compression historical-media stripping.
|
||||
|
||||
Port of Kilo-Org/kilocode#9434 (adapted for OpenAI-style message lists).
|
||||
Without this pass, tail messages keep their original multi-MB base-64 image
|
||||
payloads after context compression, and every subsequent request re-ships
|
||||
them — sometimes breaching provider body-size limits and wedging the
|
||||
session.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.context_compressor import (
|
||||
ContextCompressor,
|
||||
_content_has_images,
|
||||
_is_image_part,
|
||||
_strip_historical_media,
|
||||
_strip_images_from_content,
|
||||
)
|
||||
|
||||
|
||||
IMG_URL = {
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/png;base64," + ("A" * 1024)},
|
||||
}
|
||||
INPUT_IMG = {
|
||||
"type": "input_image",
|
||||
"image_url": "data:image/png;base64," + ("B" * 1024),
|
||||
}
|
||||
ANTHROPIC_IMG = {
|
||||
"type": "image",
|
||||
"source": {"type": "base64", "media_type": "image/png", "data": "C" * 1024},
|
||||
}
|
||||
TEXT = {"type": "text", "text": "hi"}
|
||||
INPUT_TEXT = {"type": "input_text", "text": "hi"}
|
||||
|
||||
|
||||
class TestIsImagePart:
|
||||
|
||||
|
||||
|
||||
def test_text_part_is_not_image(self):
|
||||
assert _is_image_part(TEXT) is False
|
||||
assert _is_image_part(INPUT_TEXT) is False
|
||||
|
||||
def test_non_dict_rejected(self):
|
||||
assert _is_image_part("image") is False
|
||||
assert _is_image_part(None) is False
|
||||
assert _is_image_part(42) is False
|
||||
|
||||
|
||||
class TestContentHasImages:
|
||||
|
||||
def test_empty_list(self):
|
||||
assert _content_has_images([]) is False
|
||||
|
||||
|
||||
|
||||
def test_none(self):
|
||||
assert _content_has_images(None) is False
|
||||
|
||||
|
||||
class TestStripImagesFromContent:
|
||||
|
||||
|
||||
|
||||
def test_replaces_image_with_placeholder(self):
|
||||
parts = [TEXT, IMG_URL]
|
||||
out = _strip_images_from_content(parts)
|
||||
assert len(out) == 2
|
||||
assert out[0] == TEXT
|
||||
assert out[1] == {
|
||||
"type": "text",
|
||||
"text": "[Attached image — stripped after compression]",
|
||||
}
|
||||
|
||||
|
||||
def test_handles_all_three_shapes(self):
|
||||
parts = [IMG_URL, INPUT_IMG, ANTHROPIC_IMG, TEXT]
|
||||
out = _strip_images_from_content(parts)
|
||||
assert sum(1 for p in out if p.get("type") == "text") == 4
|
||||
assert not any(_is_image_part(p) for p in out)
|
||||
|
||||
|
||||
class TestStripHistoricalMedia:
|
||||
def test_empty_passthrough(self):
|
||||
assert _strip_historical_media([]) == []
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_idempotent(self):
|
||||
msgs = [
|
||||
{"role": "user", "content": [TEXT, IMG_URL]},
|
||||
{"role": "assistant", "content": "k"},
|
||||
{"role": "user", "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
first = _strip_historical_media(msgs)
|
||||
second = _strip_historical_media(first)
|
||||
# Second pass is a no-op — no images left before the anchor.
|
||||
assert second is first
|
||||
|
||||
def test_strips_stale_tool_result_images_when_no_user_image_exists(self):
|
||||
"""#89938: vision_analyze results are the only images in the session.
|
||||
|
||||
Before this rule the anchor stayed at -1 and the list came back
|
||||
untouched, so every base64 blob rode along on every request.
|
||||
"""
|
||||
msgs = [
|
||||
{"role": "user", "content": "look at these"},
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT, IMG_URL]},
|
||||
{"role": "tool", "tool_call_id": "b", "content": [TEXT, IMG_URL]},
|
||||
{"role": "tool", "tool_call_id": "c", "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
out = _strip_historical_media(msgs)
|
||||
|
||||
assert not _content_has_images(out[1]["content"])
|
||||
assert not _content_has_images(out[2]["content"])
|
||||
# The newest tool image is what the model is reasoning about.
|
||||
assert _content_has_images(out[3]["content"])
|
||||
|
||||
def test_first_message_user_image_no_longer_blocks_tool_stripping(self):
|
||||
"""#89938's other half: ``anchor <= 0`` used to return early.
|
||||
|
||||
One attachment on the opening message plus a run of vision tool calls
|
||||
is the exact reproduction in the report - and the old early return
|
||||
meant the 413 recovery compaction freed nothing.
|
||||
"""
|
||||
msgs = [
|
||||
{"role": "user", "content": [TEXT, IMG_URL]},
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT, IMG_URL]},
|
||||
{"role": "tool", "tool_call_id": "b", "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
out = _strip_historical_media(msgs)
|
||||
|
||||
# Rule 1b: newer tool images supersede the opening attachment, so
|
||||
# its bytes age out too — the row survives with a text placeholder.
|
||||
assert not _content_has_images(out[0]["content"])
|
||||
assert out[0]["role"] == "user"
|
||||
assert not _content_has_images(out[1]["content"])
|
||||
assert _content_has_images(out[2]["content"])
|
||||
|
||||
def test_first_message_image_kept_when_it_is_the_only_image(self):
|
||||
"""Rule 1b only fires when something newer supersedes the opener."""
|
||||
msgs = [
|
||||
{"role": "user", "content": [TEXT, IMG_URL]},
|
||||
{"role": "assistant", "content": "looked"},
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT]},
|
||||
]
|
||||
assert _strip_historical_media(msgs) is msgs
|
||||
|
||||
def test_multimodal_envelope_tool_results_age_out(self):
|
||||
"""The native ``{_multimodal: True}`` dict envelope must strip too.
|
||||
|
||||
vision_analyze hands back this shape before adapters unwrap it; the
|
||||
bare list matcher never saw it, so envelope-shaped results kept their
|
||||
base64 through every compaction (#89965's shape-coverage gap).
|
||||
"""
|
||||
env = {
|
||||
"_multimodal": True,
|
||||
"text_summary": "a poster",
|
||||
"content": [TEXT, IMG_URL],
|
||||
}
|
||||
msgs = [
|
||||
{"role": "user", "content": "look"},
|
||||
{"role": "tool", "tool_call_id": "a", "content": dict(env), "api_content": "stale"},
|
||||
{"role": "tool", "tool_call_id": "b", "content": dict(env)},
|
||||
]
|
||||
out = _strip_historical_media(msgs)
|
||||
|
||||
# Older envelope collapses to its text summary; sidecar dropped.
|
||||
assert isinstance(out[1]["content"], str)
|
||||
assert "a poster" in out[1]["content"]
|
||||
assert "api_content" not in out[1]
|
||||
assert out[1]["tool_call_id"] == "a"
|
||||
# Newest envelope is the anchor and survives byte-for-byte.
|
||||
assert out[2] is msgs[2]
|
||||
|
||||
def test_all_three_wire_shapes_strip_in_tool_results(self):
|
||||
"""Chat Completions, Responses, and Anthropic-native parts all age."""
|
||||
for img in (IMG_URL, INPUT_IMG, ANTHROPIC_IMG):
|
||||
msgs = [
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT, dict(img)]},
|
||||
{"role": "tool", "tool_call_id": "b", "content": [TEXT, dict(img)]},
|
||||
]
|
||||
out = _strip_historical_media(msgs)
|
||||
assert not _content_has_images(out[0]["content"]), img["type"]
|
||||
assert _content_has_images(out[1]["content"]), img["type"]
|
||||
|
||||
def test_deterministic_double_run(self):
|
||||
"""Running the strip twice yields byte-identical output."""
|
||||
import json
|
||||
|
||||
msgs = [
|
||||
{"role": "user", "content": [TEXT, IMG_URL]},
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT, IMG_URL]},
|
||||
{"role": "tool", "tool_call_id": "b", "content": [TEXT, INPUT_IMG]},
|
||||
]
|
||||
first = _strip_historical_media(msgs)
|
||||
second = _strip_historical_media(first)
|
||||
assert json.dumps(first, sort_keys=True) == json.dumps(second, sort_keys=True)
|
||||
assert second is first # second pass is a no-op
|
||||
|
||||
def test_newest_tool_image_survives_inside_the_protected_tail(self):
|
||||
msgs = [
|
||||
{"role": "user", "content": "hi"},
|
||||
{"role": "user", "content": [TEXT, IMG_URL]},
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT, IMG_URL]},
|
||||
{"role": "tool", "tool_call_id": "b", "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
out = _strip_historical_media(msgs)
|
||||
|
||||
# The user anchor is index 1, so nothing before it changes and the
|
||||
# anchor itself is kept byte-for-byte (test_compressor_zero_user_guard
|
||||
# depends on that).
|
||||
assert out[1] is msgs[1]
|
||||
assert not _content_has_images(out[2]["content"])
|
||||
assert _content_has_images(out[3]["content"])
|
||||
|
||||
def test_tool_image_before_the_user_anchor_is_still_stripped(self):
|
||||
"""Rule 1 keeps precedence over rule 2 where the two disagree."""
|
||||
msgs = [
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT, IMG_URL]},
|
||||
{"role": "assistant", "content": "ok"},
|
||||
{"role": "user", "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
out = _strip_historical_media(msgs)
|
||||
|
||||
# Index 0 is the newest *tool* image, but it sits before the user
|
||||
# anchor, which has always stripped it. That must not regress.
|
||||
assert not _content_has_images(out[0]["content"])
|
||||
assert _content_has_images(out[2]["content"])
|
||||
|
||||
def test_unchanged_when_nothing_carries_images(self):
|
||||
msgs = [
|
||||
{"role": "user", "content": [TEXT]},
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT]},
|
||||
]
|
||||
assert _strip_historical_media(msgs) is msgs
|
||||
|
||||
def test_single_tool_image_is_left_alone(self):
|
||||
msgs = [
|
||||
{"role": "user", "content": "look"},
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
assert _strip_historical_media(msgs) is msgs
|
||||
|
||||
def test_idempotent_over_tool_images(self):
|
||||
msgs = [
|
||||
{"role": "tool", "tool_call_id": "a", "content": [TEXT, IMG_URL]},
|
||||
{"role": "tool", "tool_call_id": "b", "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
first = _strip_historical_media(msgs)
|
||||
assert first is not msgs
|
||||
assert _strip_historical_media(first) is first
|
||||
|
||||
def test_stripped_tool_message_drops_its_api_content_sidecar(self):
|
||||
"""Replaying the sidecar would resend the bytes the strip removed."""
|
||||
msgs = [
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "a",
|
||||
"content": [TEXT, IMG_URL],
|
||||
"api_content": "the exact multimodal bytes sent last turn",
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "b", "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
out = _strip_historical_media(msgs)
|
||||
|
||||
assert "api_content" not in out[0]
|
||||
# The input list is never mutated.
|
||||
assert "api_content" in msgs[0]
|
||||
|
||||
def test_non_dict_messages_pass_through(self):
|
||||
msgs = [
|
||||
"not-a-dict", # shouldn't crash
|
||||
{"role": "user", "content": [TEXT, IMG_URL]},
|
||||
{"role": "assistant", "content": "ok"},
|
||||
{"role": "user", "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
out = _strip_historical_media(msgs)
|
||||
assert out[0] == "not-a-dict"
|
||||
# Image-bearing user at index 1 is before the anchor (index 3) → stripped.
|
||||
assert not _content_has_images(out[1]["content"])
|
||||
|
||||
|
||||
class TestCompressIntegration:
|
||||
"""Verify the stripping runs inside ContextCompressor.compress()."""
|
||||
|
||||
@pytest.fixture
|
||||
def compressor(self):
|
||||
with patch("agent.context_compressor.get_model_context_length", return_value=100_000):
|
||||
c = ContextCompressor(
|
||||
model="test/model",
|
||||
threshold_percent=0.50,
|
||||
protect_first_n=1,
|
||||
protect_last_n=2,
|
||||
quiet_mode=True,
|
||||
)
|
||||
return c
|
||||
|
||||
def test_compress_strips_historical_images(self, compressor):
|
||||
# Enough messages to trigger the summarize path. protect_first_n=1 +
|
||||
# protect_last_n=2 + a middle window of at least 3 with a summary.
|
||||
msgs = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": [TEXT, IMG_URL]}, # old image-bearing user
|
||||
{"role": "assistant", "content": "looked at it"},
|
||||
{"role": "user", "content": "follow-up"},
|
||||
{"role": "assistant", "content": "ack"},
|
||||
{"role": "user", "content": "more"},
|
||||
{"role": "assistant", "content": "ok"},
|
||||
{"role": "user", "content": [TEXT, IMG_URL]}, # newest image-bearing user (tail)
|
||||
{"role": "assistant", "content": "done"},
|
||||
]
|
||||
# Bypass the real LLM summary — return a stub so compress() proceeds.
|
||||
with patch.object(compressor, "_generate_summary", return_value="SUMMARY TEXT"):
|
||||
out = compressor.compress(msgs, current_tokens=60_000)
|
||||
|
||||
# Newest user turn with image should still have it (it's in the tail).
|
||||
user_imgs = [m for m in out if m.get("role") == "user" and _content_has_images(m.get("content"))]
|
||||
assert len(user_imgs) == 1, (
|
||||
"Expected exactly one user message with images after compression "
|
||||
f"(the newest one); got {len(user_imgs)}"
|
||||
)
|
||||
# No assistant or tool messages should carry images either.
|
||||
for m in out:
|
||||
if m is user_imgs[0]:
|
||||
continue
|
||||
assert not _content_has_images(m.get("content")), (
|
||||
f"Stale image in {m.get('role')!r} message after compression"
|
||||
)
|
||||
|
||||
def test_compress_frees_stale_vision_tool_results(self, compressor):
|
||||
"""#89938 end to end: the 413 recovery compaction must free bytes.
|
||||
|
||||
The reported session had one attachment on the opening message and a
|
||||
run of ``vision_analyze`` results after it. ``anchor <= 0`` made this
|
||||
pass a no-op, so every recovery compaction returned a body that was
|
||||
still multi-MB and the provider answered 413 again - seven times in
|
||||
thirteen minutes.
|
||||
"""
|
||||
|
||||
def call(idx: str):
|
||||
return [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": idx,
|
||||
"type": "function",
|
||||
"function": {"name": "vision_analyze", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": idx, "content": [TEXT, IMG_URL]},
|
||||
]
|
||||
|
||||
msgs = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": [TEXT, IMG_URL]}, # the ONLY user image, first
|
||||
*call("a"),
|
||||
*call("b"),
|
||||
{"role": "user", "content": "and this one?"},
|
||||
*call("c"),
|
||||
]
|
||||
with patch.object(compressor, "_generate_summary", return_value="SUMMARY TEXT"):
|
||||
out = compressor.compress(msgs, current_tokens=60_000)
|
||||
|
||||
with_images = [m for m in out if isinstance(m, dict) and _content_has_images(m.get("content"))]
|
||||
# Exactly one survivor: the newest vision result. The opening
|
||||
# attachment is protect_first_n material and may or may not survive
|
||||
# the summary window, so assert on what must NOT be there instead.
|
||||
assert len(with_images) <= 2
|
||||
stale_tool_images = [
|
||||
m for m in out
|
||||
if isinstance(m, dict)
|
||||
and m.get("role") == "tool"
|
||||
and _content_has_images(m.get("content"))
|
||||
and m.get("tool_call_id") != "c"
|
||||
]
|
||||
assert stale_tool_images == [], (
|
||||
"vision_analyze results a, b still carry base64 after compression"
|
||||
)
|
||||
Reference in New Issue
Block a user