c2743251e9
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>
155 lines
5.8 KiB
Python
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"
|