forked from HAL9000/cleveragents-core
266 lines
8.2 KiB
Python
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
|