Files
temp/tests/features/steps/interactive_session_comprehensive_steps.py

266 lines
8.2 KiB
Python

import asyncio
import io
from pathlib import Path
from unittest.mock import AsyncMock
from unittest.mock import MagicMock
from unittest.mock import patch
from behave import given
from behave import then
from behave import when
from cleveragents.core.exceptions import InteractiveSessionError
from cleveragents.interactive.session import InteractiveSession
from cleveragents.routing.router import Router
# --- Givens ---
@given("an interactive session with a corrupted history file")
def step_impl(context):
context.temp_dir = Path("build/tests/temp_behave_test_files")
context.temp_dir.mkdir(exist_ok=True, parents=True)
if not hasattr(context, "cleanup_dirs"):
context.cleanup_dirs = []
context.cleanup_dirs.append(context.temp_dir)
history_file = context.temp_dir / "corrupted_history.json"
history_file.write_text("this is not valid json")
context.session = InteractiveSession(
routers={"main": MagicMock(spec=Router)},
initial_route_name="main",
history_file=history_file,
)
@given("an interactive session with a read-only history file")
def step_impl(context):
context.temp_dir = Path("build/tests/temp_behave_test_files")
context.temp_dir.mkdir(exist_ok=True, parents=True)
if not hasattr(context, "cleanup_dirs"):
context.cleanup_dirs = []
context.cleanup_dirs.append(context.temp_dir)
history_file = context.temp_dir / "readonly_history.json"
history_file.touch()
history_file.chmod(0o444) # Read-only permissions
context.session = InteractiveSession(
routers={"main": MagicMock(spec=Router)},
initial_route_name="main",
history_file=history_file,
)
@given("an interactive session where the router will fail")
def step_impl(context):
mock_router = MagicMock(spec=Router)
mock_router.process_message = AsyncMock(
side_effect=Exception("Router processing failed")
)
context.session = InteractiveSession(
routers={"main": mock_router}, initial_route_name="main"
)
@given("an interactive session with a mock router")
def step_impl(context):
context.mock_router = MagicMock(spec=Router)
context.mock_router.process_message = AsyncMock(return_value="Mock response")
context.session = InteractiveSession(
routers={"main": context.mock_router}, initial_route_name="main"
)
@given('a sequence of user inputs: "{input1}", "{input2}"')
def step_impl(context, input1, input2):
context.user_inputs = [input1, input2]
@given("an interactive session in verbose mode")
def step_impl(context):
context.mock_router = MagicMock(spec=Router)
context.mock_router.process_message = AsyncMock(return_value="Verbose response")
context.session = InteractiveSession(
routers={"main": context.mock_router},
initial_route_name="main",
verbose=True,
)
@given("an interactive session")
def step_impl(context):
context.mock_router = MagicMock(spec=Router)
context.session = InteractiveSession(
routers={"main": context.mock_router}, initial_route_name="main"
)
@given("the user input will raise a KeyboardInterrupt")
def step_impl(context):
context.user_inputs = [KeyboardInterrupt()]
@given("an interactive session where message processing will raise a generic error")
def step_impl(context):
context.mock_router = MagicMock(spec=Router)
context.mock_router.process_message = AsyncMock(
side_effect=Exception("Test processing error")
)
context.session = InteractiveSession(
routers={"main": context.mock_router}, initial_route_name="main"
)
@given("an interactive session that will fail to load history on startup")
def step_impl(context):
# Patch load_history to raise an error during the run method
context.load_history_patch = patch.object(
InteractiveSession, "load_history", side_effect=Exception("Failed to load")
)
context.load_history_patch.start()
context.session = InteractiveSession(
routers={"main": MagicMock(spec=Router)}, initial_route_name="main"
)
@given("an interactive session with some history entries")
def step_impl(context):
context.session = InteractiveSession(
routers={"main": MagicMock(spec=Router)}, initial_route_name="main"
)
context.session.add_to_history("user", "Hello")
context.session.add_to_history("assistant", "Hi there!")
@given("an interactive session without a history file")
def step_impl(context):
context.session = InteractiveSession(
routers={"main": MagicMock(spec=Router)},
initial_route_name="main",
history_file=None, # Explicitly None
)
# --- Whens ---
@when("I try to load the history")
def step_impl(context):
context.error = None
try:
context.session.load_history()
except InteractiveSessionError as e:
context.error = e
@when("I try to save the history")
def step_impl(context):
context.error = None
try:
context.session.save_history()
except InteractiveSessionError as e:
context.error = e
@when("I try to process a message through the session")
def step_impl(context):
context.error = None
try:
asyncio.run(context.session.process_message("test"))
except InteractiveSessionError as e:
context.error = e
@when("I run the session's main loop")
def step_impl(context):
# Mock input() and print() to control the loop and capture output
with patch("builtins.input", side_effect=context.user_inputs), patch(
"builtins.print"
) as mock_print:
context.mock_print = mock_print
# We don't need to catch errors here, the run method should handle them
asyncio.run(context.session.run())
@when("I try to run the session's main loop")
def step_impl(context):
context.error = None
try:
asyncio.run(context.session.run())
except InteractiveSessionError as e:
context.error = e
finally:
if hasattr(context, "load_history_patch"):
context.load_history_patch.stop()
@when("I display the history")
def step_impl(context):
with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
context.session.display_history()
context.output = mock_stdout.getvalue()
@when("I call load_history and save_history")
def step_impl(context):
context.error = None
try:
# Patch open to ensure it's not called
with patch("builtins.open") as mock_open:
context.session.load_history()
context.session.save_history()
mock_open.assert_not_called()
except Exception as e:
context.error = e
# --- Thens ---
@then("an InteractiveSessionError should be raised with a message about {action}")
def step_impl(context, action):
assert context.error is not None, "Expected an error but none was raised"
assert isinstance(context.error, InteractiveSessionError)
action_map = {
"loading history": "Failed to load history file",
"saving history": "Failed to save history file",
"processing a message": "Failed to process message",
"running the session": "Failed to run interactive session",
}
assert action_map[action] in str(context.error)
@then('the router should have processed the message "{message}"')
def step_impl(context, message):
context.mock_router.process_message.assert_called_once_with(
message, context.session.context
)
@then("the session should have stopped running")
def step_impl(context):
assert context.session.running is False
@then('the output should contain "{text}"')
def step_impl(context, text):
if hasattr(context, "output"):
# From display_history
assert text in context.output
else:
# From run loop
mock_print_calls = [call.args[0] for call in context.mock_print.call_args_list]
assert any(text in call for call in mock_print_calls)
@then("no errors should be raised")
def step_impl(context):
assert context.error is None, f"An unexpected error was raised: {context.error}"
@then('the output must contain "You: {user_message}"')
def step_impl(context, user_message):
assert f"You: {user_message}" in context.output
@then('the output must contain "Agent: {agent_message}"')
def step_impl(context, agent_message):
assert f"Agent: {agent_message}" in context.output