Files
EvoScientist/tests/test_progress_event.py
T
m4 c2743251e9 Initial commit of EvoScientist framework
Self-evolving AI scientist framework built on LangGraph/LangChain with
CLI/TUI core, FastAPI gateway, and Next.js frontend.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-07-13 08:07:45 +08:00

155 lines
5.8 KiB
Python

"""Tests for the progress event type.
Validates that:
1. StreamEventEmitter.progress() produces correct events
2. StreamState accumulates progress_text without polluting response_text
3. create_streaming_display renders a collapsed Progress panel
4. progress events do NOT appear in the channel consumer's final_content
"""
import pytest
from EvoScientist.stream.emitter import StreamEvent, StreamEventEmitter
from EvoScientist.stream.state import StreamState
class TestProgressEmitter:
"""StreamEventEmitter.progress() produces valid events."""
def test_progress_event_type(self):
emitter = StreamEventEmitter(thread_id="test")
ev = emitter.progress("Planning step 1...")
assert isinstance(ev, StreamEvent)
assert ev.type == "progress"
assert ev.data["type"] == "progress"
assert ev.data["content"] == "Planning step 1..."
def test_progress_event_has_thread_id(self):
emitter = StreamEventEmitter(thread_id="thread_abc")
ev = emitter.progress("hello")
assert ev.data["thread_id"] == "thread_abc"
def test_progress_event_has_timestamp(self):
emitter = StreamEventEmitter()
ev = emitter.progress("hello")
assert "timestamp" in ev.data
class TestProgressStreamState:
"""StreamState.handle_event correctly handles progress events."""
def test_progress_accumulates(self):
state = StreamState()
state.handle_event({"type": "progress", "content": "Step 1\n"})
state.handle_event({"type": "progress", "content": "Step 2\n"})
assert state.progress_text == "Step 1\nStep 2\n"
def test_progress_does_not_pollute_response_text(self):
state = StreamState()
state.handle_event({"type": "progress", "content": "SESSION INTENT\n"})
state.handle_event({"type": "progress", "content": "SUMMARY\n"})
assert state.response_text == ""
assert state.progress_text == "SESSION INTENT\nSUMMARY\n"
def test_progress_does_not_set_is_responding(self):
state = StreamState()
state.handle_event({"type": "progress", "content": "planning..."})
assert not state.is_responding
def test_progress_sets_is_processing(self):
state = StreamState()
state.handle_event({"type": "progress", "content": "planning..."})
assert state.is_processing
def test_text_after_progress_clears_processing(self):
state = StreamState()
state.handle_event({"type": "progress", "content": "planning..."})
assert state.is_processing
state.handle_event({"type": "text", "content": "Final answer"})
assert state.is_responding
assert not state.is_processing
assert state.response_text == "Final answer"
# progress_text is preserved
assert state.progress_text == "planning..."
def test_progress_in_display_args(self):
state = StreamState()
state.handle_event({"type": "progress", "content": "diag"})
args = state.get_display_args()
assert args["progress_text"] == "diag"
def test_get_display_args_default_empty(self):
state = StreamState()
args = state.get_display_args()
assert args["progress_text"] == ""
class TestProgressDisplay:
"""create_streaming_display renders a collapsed Progress panel."""
@staticmethod
def _render(**kwargs) -> str:
"""Render the display to a plain text string via Rich capture."""
from EvoScientist.stream.display import console, create_streaming_display
layout = create_streaming_display(**kwargs)
with console.capture() as cap:
console.print(layout)
return cap.get()
def test_no_progress_no_panel(self):
rendered = self._render(progress_text="")
assert "Progress" not in rendered
def test_progress_renders_collapsed_panel(self):
long_progress = (
"SESSION INTENT\n"
"Generate a document\n\n"
"SUMMARY\n"
"Content extracted successfully\n"
"Critical Blocker: pip unavailable\n\n"
"NEXT STEPS\n"
"Audit the environment\n"
"Find alternatives"
)
rendered = self._render(progress_text=long_progress)
# Should show collapsed title
assert "Progress" in rendered
# Should NOT show the full content (only preview of first 2 lines)
assert "pip unavailable" not in rendered
def test_progress_short_text_shows_all(self):
rendered = self._render(progress_text="Planning...")
assert "Planning..." in rendered
def test_progress_does_not_affect_response_text(self):
rendered = self._render(
progress_text="Internal planning...",
response_text="User-facing answer",
is_final=True,
)
# response_text should be visible
assert "User-facing answer" in rendered
# progress should also be visible but in separate panel
assert "Progress" in rendered
class TestProgressAndThinkingCoexist:
"""Progress and thinking are independent streams."""
def test_thinking_and_progress_both_accumulate(self):
state = StreamState()
state.handle_event({"type": "thinking", "content": "Let me think..."})
state.handle_event({"type": "progress", "content": "Planning..."})
assert state.thinking_text == "Let me think..."
assert state.progress_text == "Planning..."
assert state.response_text == ""
def test_done_clears_processing_but_preserves_progress(self):
state = StreamState()
state.handle_event({"type": "progress", "content": "diag text"})
state.handle_event({"type": "done", "response": "Final answer"})
assert not state.is_processing
assert state.progress_text == "diag text"
assert state.response_text == "Final answer"