Files
EvoScientist-Multi/tests/test_summarization.py
T
Xi Zhang 65db3a4fcd Add status bar and compact summary widgets with context window resolu… (#152)
* Add status bar and compact summary widgets with context window resolution

- Implemented a shared status bar for CLI and TUI frontends, including helpers for managing session metrics and context windows.
- Created a `CompactSummaryWidget` for displaying manual summaries in a collapsible format.
- Introduced a `CompactingWidget` to indicate ongoing compacting processes.
- Added a base class `TimedStatusWidget` for widgets that require a timer.
- Developed context window resolution helpers to retrieve context window sizes from various model attributes.
- Enhanced tests for context window resolution and status bar functionalities, ensuring accurate behavior across different scenarios.
- Updated existing tests to cover new features and maintain code quality.

* refactor(Channel): simplify lambda function in _send_with_retry method

* feat: enhance context editing logic and improve error handling in StreamState

* refactor(Channel): streamline lambda function in _send_with_retry method

* feat: rename auto-approve option to auto-mode for unattended execution; update checkpoint queries to filter by agent name; improve compatibility validation logic

* feat: rename auto-approve option to auto-mode; update related logic and tests for improved unattended execution

* fix: correct formatting of console message for MCP server configuration status

* feat: add check for None summary_message in _apply_summarization_event to prevent errors

* feat: enhance _load_checkpoint_messages to validate message format and apply summarization event
2026-04-12 17:47:37 +01:00

333 lines
11 KiB
Python

"""Tests for the summarization event pipeline and display widgets."""
from EvoScientist.stream.emitter import StreamEventEmitter
from EvoScientist.stream.events import _extract_summarization_text
from EvoScientist.stream.state import StreamState
# ---------------------------------------------------------------------------
# Emitter
# ---------------------------------------------------------------------------
class TestSummarizationEmitter:
"""StreamEventEmitter.summarization()."""
def test_event_type(self):
ev = StreamEventEmitter.summarization("hello")
assert ev.type == "summarization"
def test_event_data(self):
ev = StreamEventEmitter.summarization("ctx compressed")
assert ev.data["type"] == "summarization"
assert ev.data["content"] == "ctx compressed"
def test_empty_content(self):
ev = StreamEventEmitter.summarization("")
assert ev.data["content"] == ""
def test_start_event(self):
ev = StreamEventEmitter.summarization_start()
assert ev.type == "summarization_start"
assert ev.data["type"] == "summarization_start"
# ---------------------------------------------------------------------------
# StreamState
# ---------------------------------------------------------------------------
class TestSummarizationState:
"""StreamState handling of summarization events."""
def test_initial_state(self):
state = StreamState()
assert state.summarization_text == ""
assert state.is_summarizing is False
def test_handle_summarization_start(self):
state = StreamState()
etype = state.handle_event({"type": "summarization_start"})
assert etype == "summarization_start"
assert state.is_summarizing is True
def test_handle_summarization(self):
state = StreamState()
etype = state.handle_event({"type": "summarization", "content": "summary"})
assert etype == "summarization"
assert state.summarization_text == "summary"
assert state.is_summarizing is True
def test_accumulates_chunks(self):
"""Summarization chunks are accumulated (streaming)."""
state = StreamState()
state.handle_event({"type": "summarization", "content": "first"})
state.handle_event({"type": "summarization", "content": "second"})
assert state.summarization_text == "firstsecond"
def test_get_display_args_includes_field(self):
state = StreamState()
state.handle_event({"type": "summarization", "content": "ctx"})
args = state.get_display_args()
assert "summarization_text" in args
assert args["summarization_text"] == "ctx"
assert args["is_summarizing"] is True
def test_does_not_affect_thinking(self):
state = StreamState()
state.handle_event({"type": "thinking", "content": "think"})
state.handle_event({"type": "summarization", "content": "sum"})
assert state.thinking_text == "think"
assert state.summarization_text == "sum"
def test_does_not_affect_response(self):
state = StreamState()
state.handle_event({"type": "text", "content": "hello"})
state.handle_event({"type": "summarization", "content": "sum"})
assert state.response_text == "hello"
assert state.summarization_text == "sum"
def test_text_ends_summarizing_state(self):
state = StreamState()
state.handle_event({"type": "summarization_start"})
state.handle_event({"type": "summarization", "content": "sum"})
state.handle_event({"type": "text", "content": "hello"})
assert state.is_summarizing is False
# ---------------------------------------------------------------------------
# Rich CLI display
# ---------------------------------------------------------------------------
def _render_group(group) -> str:
"""Render a Rich Group to plain text for assertion checks."""
from io import StringIO
from rich.console import Console
buf = StringIO()
console = Console(file=buf, width=120, force_terminal=True)
console.print(group)
return buf.getvalue()
class TestSummarizationRichDisplay:
"""create_streaming_display() with summarization_text."""
def test_no_panel_when_empty(self):
from EvoScientist.stream.display import create_streaming_display
group = create_streaming_display(summarization_text="")
rendered = _render_group(group)
assert "Context Summarized" not in rendered
def test_panel_rendered(self):
from EvoScientist.stream.display import create_streaming_display
group = create_streaming_display(
summarization_text="The conversation was about ML.",
response_text="ok",
)
rendered = _render_group(group)
assert "Context Summarized" in rendered
def test_panel_rendered_while_summarizing(self):
from EvoScientist.stream.display import create_streaming_display
group = create_streaming_display(
summarization_text="The conversation was about ML.",
is_summarizing=True,
response_text="ok",
)
rendered = _render_group(group)
assert "Context Summarizing..." in rendered
def test_panel_rendered_start_placeholder(self):
from EvoScientist.stream.display import create_streaming_display
group = create_streaming_display(
summarization_text="",
is_summarizing=True,
response_text="ok",
)
rendered = _render_group(group)
assert "Context Summarizing..." in rendered
def test_long_text_truncated(self):
from EvoScientist.stream.display import create_streaming_display
long_text = "x" * 500
group = create_streaming_display(
summarization_text=long_text,
response_text="ok",
)
rendered = _render_group(group)
assert "..." in rendered
# ---------------------------------------------------------------------------
# TUI SummarizationWidget
# ---------------------------------------------------------------------------
class TestSummarizationWidget:
"""SummarizationWidget (Textual TUI)."""
def test_init_collapsed(self):
from EvoScientist.cli.widgets.summarization_widget import SummarizationWidget
w = SummarizationWidget()
assert w._collapsed is True
assert w._content == ""
assert w._elapsed == 0.0
assert w._timer_handle is None
def test_set_content(self):
from EvoScientist.cli.widgets.summarization_widget import SummarizationWidget
w = SummarizationWidget()
w._content = "test content"
assert w._content == "test content"
def test_char_count_label_small(self):
from EvoScientist.cli.widgets.summarization_widget import SummarizationWidget
w = SummarizationWidget()
w._content = "hello"
assert w._char_count_label() == "5 chars"
def test_char_count_label_large(self):
from EvoScientist.cli.widgets.summarization_widget import SummarizationWidget
w = SummarizationWidget()
w._content = "x" * 2500
assert w._char_count_label() == "2.5k chars"
def test_append_text(self):
from EvoScientist.cli.widgets.summarization_widget import SummarizationWidget
w = SummarizationWidget()
w.append_text("hello ")
w.append_text("world")
assert w._content == "hello world"
assert w._is_active is True
def test_finalize(self):
from EvoScientist.cli.widgets.summarization_widget import SummarizationWidget
class _Timer:
def __init__(self) -> None:
self.stopped = False
def stop(self) -> None:
self.stopped = True
w = SummarizationWidget()
w.append_text("some summary")
timer = _Timer()
w._timer_handle = timer
w.finalize()
assert w._is_active is False
assert w._collapsed is True
assert timer.stopped is True
assert w._timer_handle is None
def test_toggle_collapsed(self):
from EvoScientist.cli.widgets.summarization_widget import SummarizationWidget
w = SummarizationWidget()
assert w._collapsed is True
w._collapsed = not w._collapsed
assert w._collapsed is False
w._collapsed = not w._collapsed
assert w._collapsed is True
class TestCompactSummaryWidget:
"""CompactSummaryWidget (manual /compact result panel)."""
def test_init_collapsed(self):
from EvoScientist.cli.widgets.compact_summary_widget import CompactSummaryWidget
w = CompactSummaryWidget("summary content")
assert w._collapsed is True
assert w._content == "summary content"
def test_char_count_label(self):
from EvoScientist.cli.widgets.compact_summary_widget import CompactSummaryWidget
w = CompactSummaryWidget("hello")
assert w._char_count_label() == "5 chars"
def test_toggle_collapsed(self):
from EvoScientist.cli.widgets.compact_summary_widget import CompactSummaryWidget
w = CompactSummaryWidget("hello world")
assert w._collapsed is True
w._collapsed = not w._collapsed
assert w._collapsed is False
# ---------------------------------------------------------------------------
# _extract_summarization_text helper
# ---------------------------------------------------------------------------
class TestExtractSummarizationText:
"""Content extraction from summarization chunks."""
def test_string_content(self):
class Msg:
content = "hello world"
assert _extract_summarization_text(Msg()) == "hello world"
def test_content_blocks(self):
class Msg:
def __init__(self):
self.content = [
{"type": "text", "text": "part1"},
{"type": "text", "text": "part2"},
]
assert _extract_summarization_text(Msg()) == "part1part2"
def test_content_blocks_with_index(self):
"""Content blocks may include 'index' field — should still extract text."""
class Msg:
def __init__(self):
self.content = [{"type": "text", "text": " vs", "index": 1}]
assert _extract_summarization_text(Msg()) == " vs"
def test_empty_list(self):
class Msg:
def __init__(self):
self.content = []
assert _extract_summarization_text(Msg()) == ""
def test_no_content_attr(self):
class Msg:
pass
assert _extract_summarization_text(Msg()) == ""
def test_mixed_block_types(self):
class Msg:
def __init__(self):
self.content = [
{"type": "text", "text": "hello"},
{"type": "image", "url": "..."},
]
assert _extract_summarization_text(Msg()) == "hello"
def test_string_blocks_in_list(self):
class Msg:
def __init__(self):
self.content = ["hello", "world"]
assert _extract_summarization_text(Msg()) == "helloworld"