mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-06 08:16:00 +00:00
* update * refactor(memory): remove unnecessary type check and update error logging * refactor(core): standardize logger import and update agentscope dependency * fix(memory): disable console output and add logging for summarizer component * feat(core): replace OpenAI token counter with custom ReMe token counter - Replace OpenAITokenCounter with ReMeTokenCounter implementation - Add support for HuggingFace mirror and configurable tokenizer - Register ReMeTokenCounter as default token counter in registry - Update config to use hf backend with Qwen2.5-7B-Instruct model refactor(memory): convert token counting methods to async in message handlers - Change count_str_token, stat_message, count_msgs_token to async methods - Update format_msgs_to_str and context_check to use async token counting - Modify _format_tool_result_output to support async token counting - Adjust all dependent methods to await async token counting calls feat(memory): add dialog persistence to in-memory storage - Implement _append_messages_to_dialog for saving messages to JSONL files - Add dialog_path parameter to ReMeInMemoryMemory constructor - Persist messages to daily JSONL files based on timestamp grouping - Update mark_messages_compressed to save and remove compressed messages - Modify clear_content to persist all messages before clearing memory refactor(ops): update token counter type hints and initialization - Change BaseOp to use HuggingFaceTokenCounter instead of TokenCounterBase - Update type annotations for as_token_counter property and parameters - Remove direct token counter injection from Compactor and ContextChecker - Pass as_token_counter parameter through service context mechanism style(logging): improve error logging with exception details - Replace logger.error with logger.exception in browser control tool - Change logger.error to logger.exception in memory get tool error handling - Add proper exception logging with stack trace information chore(config): add token counter configuration to light YAML - Add as_token_counters section with default hf backend configuration - Configure Qwen/Qwen2.5-7B-Instruct model with mirror support enabled - Set up pretrained_model_name_or_path and use_mirror parameters test(context): update context check tests to async implementation - Convert verify_context_check_invariants to async function - Update context check test methods to use async calls - Change stat_message calls to await async implementation - Modify test_empty_messages and test_below_threshold_returns_all to async * feat(core): implement context checking and memory management features * refactor(core): replace direct loguru import with logger utility function * refactor(reme): remove RuntimeContext dependency and simplify context checking * feat(docs): add raw conversation persistence to ReMe framework
1353 lines
45 KiB
Python
1353 lines
45 KiB
Python
"""Tests for AsMsgHandler.context_check method."""
|
|
|
|
import asyncio
|
|
|
|
from agentscope.message import Msg
|
|
from test_utils import get_token_counter
|
|
|
|
from reme.core.utils import get_logger
|
|
from reme.memory.file_based.utils import AsMsgHandler
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
# ANSI color codes
|
|
class Colors:
|
|
"""ANSI color codes for terminal output."""
|
|
|
|
GREEN = "\033[92m"
|
|
RED = "\033[91m"
|
|
YELLOW = "\033[93m"
|
|
BLUE = "\033[94m"
|
|
CYAN = "\033[96m"
|
|
BOLD = "\033[1m"
|
|
RESET = "\033[0m"
|
|
|
|
|
|
def print_pass(test_name: str):
|
|
"""Print test passed message."""
|
|
print(f"{Colors.GREEN}{Colors.BOLD}✓ {test_name} PASSED{Colors.RESET}")
|
|
|
|
|
|
def print_fail(test_name: str, error: str):
|
|
"""Print test failed message."""
|
|
print(f"{Colors.RED}{Colors.BOLD}✗ {test_name} FAILED: {error}{Colors.RESET}")
|
|
|
|
|
|
def print_error(test_name: str, error: str):
|
|
"""Print test error message."""
|
|
print(f"{Colors.YELLOW}{Colors.BOLD}⚠ {test_name} ERROR: {error}{Colors.RESET}")
|
|
|
|
|
|
def print_test_header(test_name: str):
|
|
"""Print test header."""
|
|
print(f"\n{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
print(f"{Colors.BLUE}{Colors.BOLD}Running: {test_name}{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
|
|
|
|
def create_handler() -> AsMsgHandler:
|
|
"""Create an AsMsgHandler instance for testing."""
|
|
return AsMsgHandler(token_counter=get_token_counter())
|
|
|
|
|
|
def verify_context_check_invariants(
|
|
handler: AsMsgHandler,
|
|
messages: list[Msg],
|
|
to_compact: list[Msg],
|
|
to_keep: list[Msg],
|
|
memory_compact_threshold: int,
|
|
memory_compact_reserve: int,
|
|
test_name: str,
|
|
):
|
|
"""Verify that context_check results satisfy all invariants.
|
|
|
|
This function checks:
|
|
1. Threshold requirement: If total tokens <= threshold, no compaction should occur
|
|
2. Reserve requirement: Kept messages' total tokens should not exceed reserve
|
|
3. Order requirement: Both to_compact and to_keep should preserve original order
|
|
|
|
Args:
|
|
handler: The AsMsgHandler instance
|
|
messages: Original messages list
|
|
to_compact: Messages to compact returned by context_check
|
|
to_keep: Messages to keep returned by context_check
|
|
memory_compact_threshold: The threshold parameter used
|
|
memory_compact_reserve: The reserve parameter used
|
|
test_name: Name of the test for error reporting
|
|
|
|
Raises:
|
|
AssertionError: If any invariant is violated
|
|
"""
|
|
# Calculate total tokens of original messages
|
|
total_tokens = sum(asyncio.run(handler.stat_message(m)).total_tokens for m in messages)
|
|
|
|
# 1. Threshold requirement check
|
|
if total_tokens <= memory_compact_threshold:
|
|
assert len(to_compact) == 0, (
|
|
f"[{test_name}] Threshold violation: total_tokens ({total_tokens}) <= "
|
|
f"threshold ({memory_compact_threshold}), but to_compact is not empty "
|
|
f"(has {len(to_compact)} messages)"
|
|
)
|
|
assert to_keep == messages, (
|
|
f"[{test_name}] Threshold violation: total_tokens ({total_tokens}) <= "
|
|
f"threshold ({memory_compact_threshold}), but to_keep differs from original messages"
|
|
)
|
|
|
|
# 2. Reserve requirement check
|
|
kept_tokens = sum(asyncio.run(handler.stat_message(m)).total_tokens for m in to_keep)
|
|
assert kept_tokens <= memory_compact_reserve or len(to_keep) == 0, (
|
|
f"[{test_name}] Reserve violation: kept_tokens ({kept_tokens}) > " f"reserve ({memory_compact_reserve})"
|
|
)
|
|
|
|
# 3. Order requirement check - both lists should preserve original order
|
|
# Create a mapping of message id to original index
|
|
msg_to_idx = {id(m): i for i, m in enumerate(messages)}
|
|
|
|
# Check to_compact order
|
|
compact_indices = [msg_to_idx.get(id(m), -1) for m in to_compact]
|
|
for i in range(len(compact_indices) - 1):
|
|
assert compact_indices[i] < compact_indices[i + 1], (
|
|
f"[{test_name}] Order violation in to_compact: message at original index "
|
|
f"{compact_indices[i]} appears before message at index {compact_indices[i + 1]}"
|
|
)
|
|
|
|
# Check to_keep order
|
|
keep_indices = [msg_to_idx.get(id(m), -1) for m in to_keep]
|
|
for i in range(len(keep_indices) - 1):
|
|
assert keep_indices[i] < keep_indices[i + 1], (
|
|
f"[{test_name}] Order violation in to_keep: message at original index "
|
|
f"{keep_indices[i]} appears before message at index {keep_indices[i + 1]}"
|
|
)
|
|
|
|
# 4. Additional check: to_compact indices should all be less than to_keep indices
|
|
# (compact messages come from the beginning, keep messages come from the end)
|
|
if to_compact and to_keep:
|
|
max_compact_idx = max(compact_indices) if compact_indices else -1
|
|
min_keep_idx = min(keep_indices) if keep_indices else len(messages)
|
|
assert max_compact_idx < min_keep_idx, (
|
|
f"[{test_name}] Partition violation: max compact index ({max_compact_idx}) >= "
|
|
f"min keep index ({min_keep_idx}). Compact and keep should be a clean partition."
|
|
)
|
|
|
|
# 5. Check that all messages are accounted for (no duplicates, no missing)
|
|
assert len(to_compact) + len(to_keep) == len(messages), (
|
|
f"[{test_name}] Count mismatch: to_compact ({len(to_compact)}) + "
|
|
f"to_keep ({len(to_keep)}) != original ({len(messages)})"
|
|
)
|
|
|
|
all_returned = set(id(m) for m in to_compact) | set(id(m) for m in to_keep)
|
|
all_original = set(id(m) for m in messages)
|
|
assert all_returned == all_original, f"[{test_name}] Message set mismatch: returned messages differ from original"
|
|
|
|
|
|
def create_user_msg(content: str) -> Msg:
|
|
"""Create a user message."""
|
|
return Msg(name="user", role="user", content=content)
|
|
|
|
|
|
def create_assistant_msg(content: str) -> Msg:
|
|
"""Create an assistant message."""
|
|
return Msg(name="assistant", role="assistant", content=content)
|
|
|
|
|
|
def create_tool_use_msg(tool_id: str, tool_name: str, tool_input: dict) -> Msg:
|
|
"""Create a message with tool_use content block."""
|
|
return Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
{
|
|
"type": "tool_use",
|
|
"id": tool_id,
|
|
"name": tool_name,
|
|
"input": tool_input,
|
|
},
|
|
],
|
|
)
|
|
|
|
|
|
def create_tool_result_msg(tool_id: str, tool_name: str, output: str) -> Msg:
|
|
"""Create a message with tool_result content block."""
|
|
return Msg(
|
|
name="tool",
|
|
role="user",
|
|
content=[
|
|
{
|
|
"type": "tool_result",
|
|
"id": tool_id,
|
|
"name": tool_name,
|
|
"output": output,
|
|
},
|
|
],
|
|
)
|
|
|
|
|
|
def create_mixed_tool_msg(
|
|
tool_use_id: str,
|
|
tool_use_name: str,
|
|
tool_use_input: dict,
|
|
tool_result_id: str,
|
|
tool_result_name: str,
|
|
tool_result_output: str,
|
|
) -> Msg:
|
|
"""Create a message with both tool_use and tool_result blocks."""
|
|
return Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
{
|
|
"type": "tool_use",
|
|
"id": tool_use_id,
|
|
"name": tool_use_name,
|
|
"input": tool_use_input,
|
|
},
|
|
{
|
|
"type": "tool_result",
|
|
"id": tool_result_id,
|
|
"name": tool_result_name,
|
|
"output": tool_result_output,
|
|
},
|
|
],
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Normal Cases
|
|
# =============================================================================
|
|
|
|
|
|
def test_empty_messages():
|
|
"""Test context_check with empty messages list."""
|
|
handler = create_handler()
|
|
messages = []
|
|
threshold, reserve = 1000, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert not to_compact, f"Expected empty compact list, got: {to_compact}"
|
|
assert to_keep == [], f"Expected empty keep list, got: {to_keep}"
|
|
verify_context_check_invariants(handler, messages, to_compact, to_keep, threshold, reserve, "test_empty_messages")
|
|
print_pass("test_empty_messages")
|
|
|
|
|
|
def test_below_threshold_returns_all():
|
|
"""Test that messages below threshold are all kept."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Hello"),
|
|
create_assistant_msg("Hi there!"),
|
|
create_user_msg("How are you?"),
|
|
]
|
|
threshold, reserve = 10000, 5000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Very high threshold
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert not to_compact, f"Expected empty compact list, got: {len(to_compact)}"
|
|
assert len(to_keep) == 3, f"Expected 3 messages to keep, got: {len(to_keep)}"
|
|
assert to_keep == messages, "Messages to keep should be the original messages"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_below_threshold_returns_all",
|
|
)
|
|
print_pass("test_below_threshold_returns_all")
|
|
|
|
|
|
def test_above_threshold_triggers_compaction():
|
|
"""Test that messages above threshold are split correctly."""
|
|
handler = create_handler()
|
|
# Create messages that will exceed threshold
|
|
messages = [
|
|
create_user_msg("First message " * 100),
|
|
create_assistant_msg("Second message " * 100),
|
|
create_user_msg("Third message " * 100),
|
|
create_assistant_msg("Fourth message " * 100),
|
|
]
|
|
threshold, reserve = 100, 200
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Low threshold to trigger compaction
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Should have some messages compacted and some kept
|
|
assert len(to_compact) + len(to_keep) == len(messages), "Total messages should match"
|
|
assert len(to_compact) > 0, "Expected some messages to be compacted"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_above_threshold_triggers_compaction",
|
|
)
|
|
print_pass("test_above_threshold_triggers_compaction")
|
|
|
|
|
|
def test_message_order_preserved():
|
|
"""Test that message order is preserved in both lists."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("First " * 50),
|
|
create_assistant_msg("Second " * 50),
|
|
create_user_msg("Third " * 50),
|
|
create_assistant_msg("Fourth " * 50),
|
|
create_user_msg("Fifth " * 10),
|
|
]
|
|
threshold, reserve = 100, 150
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Low threshold
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Check order preservation - compact messages should appear first in original
|
|
all_messages = to_compact + to_keep
|
|
for i, msg in enumerate(all_messages):
|
|
assert msg in messages, f"Message {i} not found in original messages"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_message_order_preserved",
|
|
)
|
|
print_pass("test_message_order_preserved")
|
|
|
|
|
|
# =============================================================================
|
|
# Edge Cases - Threshold and Reserve Boundaries
|
|
# =============================================================================
|
|
|
|
|
|
def test_single_message_below_threshold():
|
|
"""Test single message below threshold."""
|
|
handler = create_handler()
|
|
messages = [create_user_msg("Short message")]
|
|
threshold, reserve = 1000, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert not to_compact, "Should not compact single message below threshold"
|
|
assert len(to_keep) == 1, "Should keep the single message"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_single_message_below_threshold",
|
|
)
|
|
print_pass("test_single_message_below_threshold")
|
|
|
|
|
|
def test_single_message_above_threshold():
|
|
"""Test single message that exceeds threshold - nothing can be kept in reserve."""
|
|
handler = create_handler()
|
|
long_content = "Very long message " * 1000
|
|
messages = [create_user_msg(long_content)]
|
|
threshold, reserve = 10, 5
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Very low threshold
|
|
memory_compact_reserve=reserve, # Even lower reserve
|
|
),
|
|
)
|
|
# Message exceeds both threshold and reserve, so it's compacted
|
|
assert len(to_compact) == 1, "Single large message should be compacted"
|
|
assert len(to_keep) == 0, "Nothing can fit in reserve"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_single_message_above_threshold",
|
|
)
|
|
print_pass("test_single_message_above_threshold")
|
|
|
|
|
|
def test_reserve_zero():
|
|
"""Test with reserve=0, no messages can be kept."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Hello"),
|
|
create_assistant_msg("Hi there!"),
|
|
]
|
|
threshold, reserve = 1, 0
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Trigger compaction
|
|
memory_compact_reserve=reserve, # Zero reserve
|
|
),
|
|
)
|
|
# All messages should be compacted since reserve is 0
|
|
assert len(to_compact) == 2, f"All messages should be compacted, got {len(to_compact)}"
|
|
assert len(to_keep) == 0, f"No messages should be kept, got {len(to_keep)}"
|
|
verify_context_check_invariants(handler, messages, to_compact, to_keep, threshold, reserve, "test_reserve_zero")
|
|
print_pass("test_reserve_zero")
|
|
|
|
|
|
def test_threshold_zero():
|
|
"""Test with threshold=0, always triggers compaction."""
|
|
handler = create_handler()
|
|
messages = [create_user_msg("A")] # Minimal message
|
|
threshold, reserve = 0, 1000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Zero threshold - always triggers
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Even minimal message triggers compaction with threshold=0
|
|
# But reserve is high so it should be kept
|
|
assert len(to_compact) == 0 or len(to_keep) == 1, "Message should fit in reserve"
|
|
verify_context_check_invariants(handler, messages, to_compact, to_keep, threshold, reserve, "test_threshold_zero")
|
|
print_pass("test_threshold_zero")
|
|
|
|
|
|
def test_exact_threshold_boundary():
|
|
"""Test messages exactly at threshold boundary."""
|
|
handler = create_handler()
|
|
messages = [create_user_msg("Test message")]
|
|
|
|
# Get exact token count
|
|
stat = asyncio.run(handler.stat_message(messages[0]))
|
|
exact_tokens = stat.total_tokens
|
|
threshold, reserve = exact_tokens, exact_tokens
|
|
|
|
# Test at exact boundary
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Exactly at boundary
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# At exact boundary (<=), should not trigger compaction
|
|
assert not to_compact, "Should not compact at exact boundary"
|
|
assert len(to_keep) == 1, "Should keep message at exact boundary"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_exact_threshold_boundary",
|
|
)
|
|
print_pass("test_exact_threshold_boundary")
|
|
|
|
|
|
def test_reserve_larger_than_threshold():
|
|
"""Test when reserve is larger than threshold (unusual but valid config)."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Message one " * 20),
|
|
create_assistant_msg("Message two " * 20),
|
|
]
|
|
threshold, reserve = 50, 10000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Low threshold
|
|
memory_compact_reserve=reserve, # High reserve
|
|
),
|
|
)
|
|
# Compaction triggered but reserve can hold everything
|
|
# Total messages should be preserved
|
|
assert len(to_compact) + len(to_keep) == 2
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_reserve_larger_than_threshold",
|
|
)
|
|
print_pass("test_reserve_larger_than_threshold")
|
|
|
|
|
|
# =============================================================================
|
|
# Edge Cases - Tool Use/Result Pairing
|
|
# =============================================================================
|
|
|
|
|
|
def test_tool_use_result_paired():
|
|
"""Test that tool_use and tool_result pairs are kept together."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Please run the tool " * 50),
|
|
create_tool_use_msg("call_001", "test_tool", {"arg": "value"}),
|
|
create_tool_result_msg("call_001", "test_tool", "Tool output"),
|
|
create_assistant_msg("The tool returned results"),
|
|
]
|
|
threshold, reserve = 50, 1000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Trigger compaction
|
|
memory_compact_reserve=reserve, # Enough for tool pair
|
|
),
|
|
)
|
|
|
|
# If tool_result is kept, tool_use should also be kept
|
|
tool_result_in_keep = any(any(b.get("type") == "tool_result" for b in m.get_content_blocks()) for m in to_keep)
|
|
tool_use_in_keep = any(any(b.get("type") == "tool_use" for b in m.get_content_blocks()) for m in to_keep)
|
|
|
|
if tool_result_in_keep:
|
|
assert tool_use_in_keep, "tool_use should be kept when tool_result is kept"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_tool_use_result_paired",
|
|
)
|
|
print_pass("test_tool_use_result_paired")
|
|
|
|
|
|
def test_tool_use_without_result():
|
|
"""Test tool_use message without corresponding tool_result."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Run the tool"),
|
|
create_tool_use_msg("call_orphan", "orphan_tool", {"arg": "value"}),
|
|
create_assistant_msg("Something happened"),
|
|
]
|
|
threshold, reserve = 10, 1000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Should not crash, just process normally
|
|
assert len(to_compact) + len(to_keep) == 3
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_tool_use_without_result",
|
|
)
|
|
print_pass("test_tool_use_without_result")
|
|
|
|
|
|
def test_tool_result_without_use():
|
|
"""Test tool_result message without corresponding tool_use."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Here's a result"),
|
|
create_tool_result_msg("call_orphan", "orphan_tool", "Some output"),
|
|
create_assistant_msg("Got it"),
|
|
]
|
|
threshold, reserve = 10, 1000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Should not crash even with orphan tool_result
|
|
assert len(to_compact) + len(to_keep) == 3
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_tool_result_without_use",
|
|
)
|
|
print_pass("test_tool_result_without_use")
|
|
|
|
|
|
def test_multiple_tool_pairs():
|
|
"""Test multiple tool_use/tool_result pairs."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Start task " * 50),
|
|
create_tool_use_msg("call_001", "tool_a", {"a": 1}),
|
|
create_tool_result_msg("call_001", "tool_a", "Result A"),
|
|
create_tool_use_msg("call_002", "tool_b", {"b": 2}),
|
|
create_tool_result_msg("call_002", "tool_b", "Result B"),
|
|
create_tool_use_msg("call_003", "tool_c", {"c": 3}),
|
|
create_tool_result_msg("call_003", "tool_c", "Result C"),
|
|
create_assistant_msg("All done"),
|
|
]
|
|
threshold, reserve = 50, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
|
|
# Verify tool pairs integrity - for each kept tool_result, its tool_use should be kept
|
|
for msg in to_keep:
|
|
for block in msg.get_content_blocks("tool_result"):
|
|
tool_id = block.get("id", "")
|
|
if tool_id:
|
|
# Find corresponding tool_use
|
|
tool_use_found = False
|
|
for keep_msg in to_keep:
|
|
for use_block in keep_msg.get_content_blocks("tool_use"):
|
|
if use_block.get("id") == tool_id:
|
|
tool_use_found = True
|
|
break
|
|
assert tool_use_found, f"tool_use for {tool_id} should be kept with tool_result"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_multiple_tool_pairs",
|
|
)
|
|
print_pass("test_multiple_tool_pairs")
|
|
|
|
|
|
def test_tool_dependency_causes_extra_inclusion():
|
|
"""Test that tool_use is included even if it exceeds simple reserve calculation."""
|
|
handler = create_handler()
|
|
# Create a scenario where:
|
|
# - First message (tool_use) is large
|
|
# - Later message (tool_result) references it
|
|
# - Reserve alone wouldn't fit tool_use, but dependency requires it
|
|
large_tool_input = {"data": "x" * 200}
|
|
messages = [
|
|
create_user_msg("Start " * 100), # Large message
|
|
create_tool_use_msg("call_dep", "dep_tool", large_tool_input), # Medium
|
|
create_user_msg("Middle " * 100), # Large message
|
|
create_tool_result_msg("call_dep", "dep_tool", "Result"), # Small
|
|
create_assistant_msg("End"), # Small
|
|
]
|
|
threshold, reserve = 100, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Trigger compaction
|
|
memory_compact_reserve=reserve, # Medium reserve
|
|
),
|
|
)
|
|
|
|
# Check pair integrity
|
|
result_kept = any(
|
|
any(b.get("id") == "call_dep" and b.get("type") == "tool_result" for b in m.get_content_blocks())
|
|
for m in to_keep
|
|
)
|
|
use_kept = any(
|
|
any(b.get("id") == "call_dep" and b.get("type") == "tool_use" for b in m.get_content_blocks()) for m in to_keep
|
|
)
|
|
|
|
if result_kept:
|
|
assert use_kept, "Dependent tool_use should be included with tool_result"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_tool_dependency_causes_extra_inclusion",
|
|
)
|
|
print_pass("test_tool_dependency_causes_extra_inclusion")
|
|
|
|
|
|
def test_tool_dependency_exceeds_reserve():
|
|
"""Test when tool_result + its tool_use dependency would exceed reserve."""
|
|
handler = create_handler()
|
|
# tool_use is very large, making the pair not fit in reserve
|
|
very_large_input = {"data": "x" * 2000}
|
|
messages = [
|
|
create_user_msg("First"),
|
|
create_tool_use_msg("call_big", "big_tool", very_large_input), # Very large
|
|
create_tool_result_msg("call_big", "big_tool", "Small result"),
|
|
create_assistant_msg("Last message"),
|
|
]
|
|
threshold, reserve = 10, 100
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Trigger compaction
|
|
memory_compact_reserve=reserve, # Small reserve - can't fit the pair
|
|
),
|
|
)
|
|
|
|
# The tool pair is too large, so it should be excluded or partially handled
|
|
# Either both are compacted (pair excluded) or neither is kept
|
|
result_kept = any(
|
|
any(b.get("id") == "call_big" and b.get("type") == "tool_result" for b in m.get_content_blocks())
|
|
for m in to_keep
|
|
)
|
|
|
|
if result_kept:
|
|
# If result is kept, use must also be kept (pair integrity)
|
|
use_kept = any(
|
|
any(b.get("id") == "call_big" and b.get("type") == "tool_use" for b in m.get_content_blocks())
|
|
for m in to_keep
|
|
)
|
|
assert use_kept, "Pair integrity violated"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_tool_dependency_exceeds_reserve",
|
|
)
|
|
print_pass("test_tool_dependency_exceeds_reserve")
|
|
|
|
|
|
def test_interleaved_tool_pairs():
|
|
"""Test interleaved tool_use/tool_result (not strictly sequential)."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Multi-tool task " * 30),
|
|
create_tool_use_msg("call_a", "tool_a", {"a": 1}),
|
|
create_tool_use_msg("call_b", "tool_b", {"b": 2}), # Two uses before results
|
|
create_tool_result_msg("call_a", "tool_a", "Result A"),
|
|
create_tool_result_msg("call_b", "tool_b", "Result B"),
|
|
create_assistant_msg("Both done"),
|
|
]
|
|
threshold, reserve = 50, 1000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
|
|
# Verify pair integrity for interleaved pairs
|
|
for msg in to_keep:
|
|
for block in msg.get_content_blocks("tool_result"):
|
|
tool_id = block.get("id", "")
|
|
if tool_id:
|
|
use_found = any(
|
|
any(ub.get("id") == tool_id and ub.get("type") == "tool_use" for ub in km.get_content_blocks())
|
|
for km in to_keep
|
|
)
|
|
assert use_found, f"Interleaved tool_use {tool_id} should be kept"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_interleaved_tool_pairs",
|
|
)
|
|
print_pass("test_interleaved_tool_pairs")
|
|
|
|
|
|
# =============================================================================
|
|
# Edge Cases - Message Content Variations
|
|
# =============================================================================
|
|
|
|
|
|
def test_message_with_empty_content():
|
|
"""Test message with empty string content."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg(""), # Empty content
|
|
create_assistant_msg("Response"),
|
|
]
|
|
threshold, reserve = 1000, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert len(to_compact) + len(to_keep) == 2
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_message_with_empty_content",
|
|
)
|
|
print_pass("test_message_with_empty_content")
|
|
|
|
|
|
def test_message_with_whitespace_only():
|
|
"""Test message with whitespace-only content."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg(" \n\t "), # Whitespace only
|
|
create_assistant_msg("Response"),
|
|
]
|
|
threshold, reserve = 1000, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert len(to_compact) + len(to_keep) == 2
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_message_with_whitespace_only",
|
|
)
|
|
print_pass("test_message_with_whitespace_only")
|
|
|
|
|
|
def test_very_long_single_message():
|
|
"""Test very long single message that exceeds any reasonable reserve."""
|
|
handler = create_handler()
|
|
huge_content = "x" * 100000 # Very long
|
|
messages = [create_user_msg(huge_content)]
|
|
threshold, reserve = 100, 1000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Single huge message - either kept alone or compacted
|
|
assert len(to_compact) + len(to_keep) == 1
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_very_long_single_message",
|
|
)
|
|
print_pass("test_very_long_single_message")
|
|
|
|
|
|
def test_many_small_messages():
|
|
"""Test many small messages."""
|
|
handler = create_handler()
|
|
messages = [create_user_msg(f"Msg {i}") for i in range(100)]
|
|
threshold, reserve = 100, 200
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Low threshold
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Should compact older messages and keep recent ones
|
|
assert len(to_compact) + len(to_keep) == 100
|
|
assert len(to_keep) > 0, "Should keep some messages"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_many_small_messages",
|
|
)
|
|
print_pass("test_many_small_messages")
|
|
|
|
|
|
def test_unicode_content():
|
|
"""Test messages with unicode characters."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("你好世界!🎉 Emoji and 中文"),
|
|
create_assistant_msg("مرحبا العالم 🌍 Arabic and more"),
|
|
create_user_msg("日本語テスト 🇯🇵"),
|
|
]
|
|
threshold, reserve = 1000, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert len(to_compact) + len(to_keep) == 3
|
|
verify_context_check_invariants(handler, messages, to_compact, to_keep, threshold, reserve, "test_unicode_content")
|
|
print_pass("test_unicode_content")
|
|
|
|
|
|
def test_special_characters_content():
|
|
"""Test messages with special characters."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Special chars: <>&\"'`~!@#$%^&*()[]{}|\\"),
|
|
create_assistant_msg("More: \n\r\t\0 nulls and newlines"),
|
|
]
|
|
threshold, reserve = 1000, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert len(to_compact) + len(to_keep) == 2
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_special_characters_content",
|
|
)
|
|
print_pass("test_special_characters_content")
|
|
|
|
|
|
# =============================================================================
|
|
# Edge Cases - Boundary Conditions
|
|
# =============================================================================
|
|
|
|
|
|
def test_all_messages_fit_exactly_in_reserve():
|
|
"""Test when all messages fit exactly in reserve after threshold exceeded."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Message 1"),
|
|
create_assistant_msg("Message 2"),
|
|
]
|
|
|
|
# Calculate total tokens
|
|
total = sum(asyncio.run(handler.stat_message(m)).total_tokens for m in messages)
|
|
threshold, reserve = total - 1, total
|
|
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Just below total to trigger
|
|
memory_compact_reserve=reserve, # Exactly fits all
|
|
),
|
|
)
|
|
# All should be kept since reserve can hold everything
|
|
assert len(to_keep) == 2, f"All messages should fit in reserve, got {len(to_keep)}"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_all_messages_fit_exactly_in_reserve",
|
|
)
|
|
print_pass("test_all_messages_fit_exactly_in_reserve")
|
|
|
|
|
|
def test_first_message_only_compacted():
|
|
"""Test when only the first message is compacted."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Large first message " * 100), # Large
|
|
create_assistant_msg("Small"), # Small
|
|
create_user_msg("Tiny"), # Tiny
|
|
]
|
|
|
|
# Calculate tokens to set appropriate reserve
|
|
small_msg_tokens = asyncio.run(handler.stat_message(messages[1])).total_tokens
|
|
tiny_msg_tokens = asyncio.run(handler.stat_message(messages[2])).total_tokens
|
|
threshold, reserve = 50, small_msg_tokens + tiny_msg_tokens + 10
|
|
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Low to trigger
|
|
memory_compact_reserve=reserve, # Fits last 2
|
|
),
|
|
)
|
|
|
|
assert len(to_compact) >= 1, "At least first message should be compacted"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_first_message_only_compacted",
|
|
)
|
|
print_pass("test_first_message_only_compacted")
|
|
|
|
|
|
def test_last_message_only_kept():
|
|
"""Test when only the last message can be kept."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Large " * 200),
|
|
create_assistant_msg("Large " * 200),
|
|
create_user_msg("Tiny"), # Only this fits
|
|
]
|
|
|
|
tiny_tokens = asyncio.run(handler.stat_message(messages[2])).total_tokens
|
|
threshold, reserve = 10, tiny_tokens + 5
|
|
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve, # Only fits last message
|
|
),
|
|
)
|
|
|
|
if len(to_keep) == 1:
|
|
# Last message should be the one kept
|
|
assert to_keep[0] == messages[2], "Only last message should be kept"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_last_message_only_kept",
|
|
)
|
|
print_pass("test_last_message_only_kept")
|
|
|
|
|
|
def test_all_messages_compacted():
|
|
"""Test when all messages need to be compacted (nothing fits in reserve)."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Large message " * 100),
|
|
create_assistant_msg("Large message " * 100),
|
|
]
|
|
threshold, reserve = 10, 1
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold, # Trigger compaction
|
|
memory_compact_reserve=reserve, # Too small for anything
|
|
),
|
|
)
|
|
assert len(to_compact) == 2, "All messages should be compacted"
|
|
assert len(to_keep) == 0, "No messages should be kept"
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_all_messages_compacted",
|
|
)
|
|
print_pass("test_all_messages_compacted")
|
|
|
|
|
|
# =============================================================================
|
|
# Edge Cases - Message Roles
|
|
# =============================================================================
|
|
|
|
|
|
def test_system_message():
|
|
"""Test handling of system role messages."""
|
|
handler = create_handler()
|
|
system_msg = Msg(name="system", role="system", content="You are a helpful assistant.")
|
|
messages = [
|
|
system_msg,
|
|
create_user_msg("Hello"),
|
|
create_assistant_msg("Hi there!"),
|
|
]
|
|
threshold, reserve = 1000, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert len(to_compact) + len(to_keep) == 3
|
|
verify_context_check_invariants(handler, messages, to_compact, to_keep, threshold, reserve, "test_system_message")
|
|
print_pass("test_system_message")
|
|
|
|
|
|
def test_mixed_roles():
|
|
"""Test messages with various roles (user, assistant, system)."""
|
|
handler = create_handler()
|
|
# agentscope.message.Msg only supports: user, assistant, system
|
|
messages = [
|
|
Msg(name="system", role="system", content="System prompt"),
|
|
Msg(name="user", role="user", content="User message"),
|
|
Msg(name="assistant", role="assistant", content="Assistant response"),
|
|
Msg(name="tool", role="user", content="Tool output as user role"),
|
|
Msg(name="helper", role="assistant", content="Another assistant message"),
|
|
]
|
|
threshold, reserve = 1000, 500
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert len(to_compact) + len(to_keep) == 5
|
|
verify_context_check_invariants(handler, messages, to_compact, to_keep, threshold, reserve, "test_mixed_roles")
|
|
print_pass("test_mixed_roles")
|
|
|
|
|
|
# =============================================================================
|
|
# Edge Cases - Tool Block Variations
|
|
# =============================================================================
|
|
|
|
|
|
def test_tool_use_with_empty_id():
|
|
"""Test tool_use block with empty id."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Run tool"),
|
|
create_tool_use_msg("", "test_tool", {"arg": "value"}), # Empty ID
|
|
create_assistant_msg("Done"),
|
|
]
|
|
threshold, reserve = 10, 1000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Should handle gracefully
|
|
assert len(to_compact) + len(to_keep) == 3
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_tool_use_with_empty_id",
|
|
)
|
|
print_pass("test_tool_use_with_empty_id")
|
|
|
|
|
|
def test_tool_result_with_empty_id():
|
|
"""Test tool_result block with empty id."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_user_msg("Got result"),
|
|
create_tool_result_msg("", "test_tool", "Output"), # Empty ID
|
|
create_assistant_msg("Noted"),
|
|
]
|
|
threshold, reserve = 10, 1000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Should handle gracefully
|
|
assert len(to_compact) + len(to_keep) == 3
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_tool_result_with_empty_id",
|
|
)
|
|
print_pass("test_tool_result_with_empty_id")
|
|
|
|
|
|
def test_duplicate_tool_ids():
|
|
"""Test messages with duplicate tool IDs (unusual but possible)."""
|
|
handler = create_handler()
|
|
messages = [
|
|
create_tool_use_msg("call_dup", "tool_a", {"a": 1}),
|
|
create_tool_result_msg("call_dup", "tool_a", "Result A"),
|
|
create_tool_use_msg("call_dup", "tool_b", {"b": 2}), # Same ID, different tool
|
|
create_tool_result_msg("call_dup", "tool_b", "Result B"),
|
|
]
|
|
threshold, reserve = 10, 1000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
# Should not crash with duplicate IDs
|
|
assert len(to_compact) + len(to_keep) == 4
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_duplicate_tool_ids",
|
|
)
|
|
print_pass("test_duplicate_tool_ids")
|
|
|
|
|
|
def test_message_with_multiple_tool_blocks():
|
|
"""Test single message containing multiple tool blocks."""
|
|
handler = create_handler()
|
|
msg_with_multiple_tools = Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
{"type": "tool_use", "id": "call_1", "name": "tool1", "input": {}},
|
|
{"type": "tool_use", "id": "call_2", "name": "tool2", "input": {}},
|
|
{"type": "tool_use", "id": "call_3", "name": "tool3", "input": {}},
|
|
],
|
|
)
|
|
messages = [
|
|
create_user_msg("Do multiple things"),
|
|
msg_with_multiple_tools,
|
|
create_tool_result_msg("call_1", "tool1", "Result 1"),
|
|
create_tool_result_msg("call_2", "tool2", "Result 2"),
|
|
create_tool_result_msg("call_3", "tool3", "Result 3"),
|
|
]
|
|
threshold, reserve = 10, 2000
|
|
to_compact, to_keep, _ = asyncio.run(
|
|
handler.context_check(
|
|
messages=messages,
|
|
memory_compact_threshold=threshold,
|
|
memory_compact_reserve=reserve,
|
|
),
|
|
)
|
|
assert len(to_compact) + len(to_keep) == 5
|
|
verify_context_check_invariants(
|
|
handler,
|
|
messages,
|
|
to_compact,
|
|
to_keep,
|
|
threshold,
|
|
reserve,
|
|
"test_message_with_multiple_tool_blocks",
|
|
)
|
|
print_pass("test_message_with_multiple_tool_blocks")
|
|
|
|
|
|
# =============================================================================
|
|
# Run All Tests
|
|
# =============================================================================
|
|
|
|
|
|
def run_all_tests():
|
|
"""Run all tests."""
|
|
tests = [
|
|
# Normal cases
|
|
test_empty_messages,
|
|
test_below_threshold_returns_all,
|
|
test_above_threshold_triggers_compaction,
|
|
test_message_order_preserved,
|
|
# Edge cases - boundaries
|
|
test_single_message_below_threshold,
|
|
test_single_message_above_threshold,
|
|
test_reserve_zero,
|
|
test_threshold_zero,
|
|
test_exact_threshold_boundary,
|
|
test_reserve_larger_than_threshold,
|
|
# Edge cases - tool pairing
|
|
test_tool_use_result_paired,
|
|
test_tool_use_without_result,
|
|
test_tool_result_without_use,
|
|
test_multiple_tool_pairs,
|
|
test_tool_dependency_causes_extra_inclusion,
|
|
test_tool_dependency_exceeds_reserve,
|
|
test_interleaved_tool_pairs,
|
|
# Edge cases - content variations
|
|
test_message_with_empty_content,
|
|
test_message_with_whitespace_only,
|
|
test_very_long_single_message,
|
|
test_many_small_messages,
|
|
test_unicode_content,
|
|
test_special_characters_content,
|
|
# Edge cases - boundaries
|
|
test_all_messages_fit_exactly_in_reserve,
|
|
test_first_message_only_compacted,
|
|
test_last_message_only_kept,
|
|
test_all_messages_compacted,
|
|
# Edge cases - roles
|
|
test_system_message,
|
|
test_mixed_roles,
|
|
# Edge cases - tool blocks
|
|
test_tool_use_with_empty_id,
|
|
test_tool_result_with_empty_id,
|
|
test_duplicate_tool_ids,
|
|
test_message_with_multiple_tool_blocks,
|
|
]
|
|
|
|
passed = 0
|
|
failed = 0
|
|
|
|
for test in tests:
|
|
try:
|
|
print_test_header(test.__name__)
|
|
test()
|
|
passed += 1
|
|
except AssertionError as e:
|
|
print_fail(test.__name__, str(e))
|
|
failed += 1
|
|
except Exception as e:
|
|
print_error(test.__name__, str(e))
|
|
failed += 1
|
|
|
|
# Print summary
|
|
print(f"\n{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
print(f"{Colors.BOLD}Test Results Summary{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
print(f"{Colors.GREEN}{Colors.BOLD}✓ Passed: {passed}{Colors.RESET}")
|
|
if failed > 0:
|
|
print(f"{Colors.RED}{Colors.BOLD}✗ Failed: {failed}{Colors.RESET}")
|
|
else:
|
|
print(f"{Colors.GREEN}✗ Failed: {failed}{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
|
|
if failed == 0:
|
|
print(f"\n{Colors.GREEN}{Colors.BOLD}🎉 All tests passed!{Colors.RESET}")
|
|
else:
|
|
print(f"\n{Colors.RED}{Colors.BOLD}💥 Some tests failed!{Colors.RESET}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_all_tests()
|