forked from cleveragents/cleveragents-core
522 lines
19 KiB
Python
522 lines
19 KiB
Python
"""Step definitions for CLI streaming tests."""
|
|
|
|
import os
|
|
import re
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from behave import given, then, when
|
|
|
|
|
|
@given("a temporary directory")
|
|
def step_create_temp_dir(context):
|
|
"""Create a temporary directory for testing."""
|
|
context.temp_dir = tempfile.mkdtemp()
|
|
context.original_dir = os.getcwd()
|
|
os.chdir(context.temp_dir)
|
|
|
|
# Reset container for clean state
|
|
from cleveragents.application.container import reset_container
|
|
|
|
reset_container()
|
|
|
|
# Set up mock AI provider for testing
|
|
os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = "true"
|
|
|
|
|
|
@given('I initialize a new project named "{name}"')
|
|
def step_initialize_project(context, name):
|
|
"""Initialize a new CleverAgents project."""
|
|
from cleveragents.application.container import get_container
|
|
from cleveragents.application.services.project_service import ProjectService
|
|
from cleveragents.cli.commands.project import init_command
|
|
|
|
# Use the CLI command directly to properly initialize the database
|
|
init_command(name, Path.cwd())
|
|
|
|
# Store the project for later use
|
|
container = get_container()
|
|
project_service: ProjectService = container.project_service()
|
|
context.project = project_service.get_current_project()
|
|
|
|
# Verify project was initialized
|
|
assert (Path.cwd() / ".cleveragents").exists()
|
|
|
|
|
|
@given("I add a test file to the project")
|
|
def step_add_test_file(context):
|
|
"""Add a test file to the project."""
|
|
test_file = Path.cwd() / "test.py"
|
|
test_file.write_text("def hello():\n print('Hello, world!')\n")
|
|
|
|
# Ensure test_file exists
|
|
assert test_file.exists()
|
|
|
|
# Add to context using the CLI pattern
|
|
from cleveragents.cli.commands.context import add_command
|
|
|
|
add_command([str(test_file)])
|
|
|
|
|
|
@given("I add {count:d} files to the project")
|
|
def step_add_multiple_files(context, count):
|
|
"""Add multiple test files to the project."""
|
|
from cleveragents.cli.commands.context import add_command
|
|
|
|
files = []
|
|
for i in range(count):
|
|
test_file = Path.cwd() / f"test_{i}.py"
|
|
test_file.write_text(f"def function_{i}():\n pass\n")
|
|
files.append(str(test_file))
|
|
|
|
# Add all files to context using the CLI command
|
|
add_command(files)
|
|
|
|
|
|
@given("the AI provider is configured to fail")
|
|
def step_configure_provider_to_fail(context):
|
|
"""Configure the AI provider to fail."""
|
|
# Set environment variable to make mock provider fail
|
|
os.environ["CLEVERAGENTS_MOCK_SHOULD_FAIL"] = "true"
|
|
context.provider_should_fail = True
|
|
|
|
|
|
@given("the AI provider generates invalid code")
|
|
def step_configure_provider_invalid_code(context):
|
|
"""Configure the AI provider to generate invalid code."""
|
|
# Set environment variable to make mock provider generate invalid code
|
|
os.environ["CLEVERAGENTS_MOCK_INVALID_CODE"] = "true"
|
|
context.provider_generates_invalid = True
|
|
|
|
|
|
@when('I run tell command "{prompt}" without streaming')
|
|
def step_run_tell_without_streaming(context, prompt):
|
|
"""Run a tell command without streaming."""
|
|
from cleveragents.cli.commands.plan import tell_command
|
|
|
|
try:
|
|
tell_command(prompt=prompt, name=None)
|
|
context.command_output = "Plan created"
|
|
context.command_success = True
|
|
except Exception as e:
|
|
context.command_output = str(e)
|
|
context.command_success = False
|
|
|
|
|
|
def _strip_rich_markup(text: str) -> str:
|
|
"""Strip Rich markup codes from text for testing."""
|
|
import re
|
|
|
|
# Remove Rich color/style markup like [green], [/green], [cyan], [dim], etc.
|
|
return re.sub(r"\[/?[a-z]+\]", "", text)
|
|
|
|
|
|
@when('I run tell command "{prompt}" with streaming')
|
|
def step_run_tell_with_streaming(context, prompt):
|
|
"""Run a tell command with streaming."""
|
|
import asyncio
|
|
from io import StringIO
|
|
from unittest.mock import patch
|
|
|
|
from rich.console import Console
|
|
|
|
from cleveragents.application.container import get_container
|
|
from cleveragents.application.services.plan_service import PlanService
|
|
from cleveragents.cli.commands.plan import _tell_streaming
|
|
|
|
container = get_container()
|
|
plan_service: PlanService = container.plan_service()
|
|
|
|
# Capture output using Rich Console
|
|
output = StringIO()
|
|
test_console = Console(file=output, force_terminal=False, width=120)
|
|
|
|
try:
|
|
# Patch the console in plan.py to use our test console
|
|
with patch("cleveragents.cli.commands.plan.console", test_console):
|
|
asyncio.run(_tell_streaming(context.project, prompt, None, plan_service))
|
|
# Strip Rich markup codes for easier testing
|
|
context.command_output = _strip_rich_markup(output.getvalue())
|
|
context.command_success = True
|
|
# Track all command successes for sequential tests
|
|
if not hasattr(context, "all_commands_success"):
|
|
context.all_commands_success = []
|
|
context.all_commands_success.append(True)
|
|
except Exception as e:
|
|
context.command_output = _strip_rich_markup(output.getvalue()) + str(e)
|
|
context.command_success = False
|
|
# Track all command successes for sequential tests
|
|
if not hasattr(context, "all_commands_success"):
|
|
context.all_commands_success = []
|
|
context.all_commands_success.append(False)
|
|
|
|
|
|
@when('I run tell command "{prompt}" with name "{name}" and streaming')
|
|
def step_run_tell_with_name_and_streaming(context, prompt, name):
|
|
"""Run a tell command with custom name and streaming."""
|
|
import asyncio
|
|
from io import StringIO
|
|
from unittest.mock import patch
|
|
|
|
from rich.console import Console
|
|
|
|
from cleveragents.application.container import get_container
|
|
from cleveragents.application.services.plan_service import PlanService
|
|
from cleveragents.cli.commands.plan import _tell_streaming
|
|
|
|
container = get_container()
|
|
plan_service: PlanService = container.plan_service()
|
|
|
|
# Capture output using Rich Console
|
|
output = StringIO()
|
|
test_console = Console(file=output, force_terminal=False, width=120)
|
|
|
|
# Store the plan name for later verification
|
|
context.custom_plan_name = name
|
|
|
|
try:
|
|
# Patch the console in plan.py to use our test console
|
|
with patch("cleveragents.cli.commands.plan.console", test_console):
|
|
asyncio.run(_tell_streaming(context.project, prompt, name, plan_service))
|
|
# Strip Rich markup codes for easier testing
|
|
context.command_output = _strip_rich_markup(output.getvalue())
|
|
context.command_success = True
|
|
except Exception as e:
|
|
context.command_output = _strip_rich_markup(output.getvalue()) + str(e)
|
|
context.command_success = False
|
|
|
|
|
|
@when('I measure time for tell command "{prompt}" without streaming')
|
|
def step_measure_time_without_streaming(context, prompt):
|
|
"""Measure execution time without streaming."""
|
|
start_time = time.time()
|
|
step_run_tell_without_streaming(context, prompt)
|
|
context.time_without_streaming = time.time() - start_time
|
|
context.without_streaming_success = context.command_success
|
|
|
|
|
|
@when('I measure time for tell command "{prompt}" with streaming')
|
|
def step_measure_time_with_streaming(context, prompt):
|
|
"""Measure execution time with streaming."""
|
|
start_time = time.time()
|
|
step_run_tell_with_streaming(context, prompt)
|
|
context.time_with_streaming = time.time() - start_time
|
|
context.with_streaming_success = context.command_success
|
|
|
|
|
|
@when("I wait for completion")
|
|
def step_wait_for_completion(context):
|
|
"""Wait for command completion."""
|
|
time.sleep(0.5)
|
|
|
|
|
|
@when("I send interrupt signal after {seconds:d} seconds")
|
|
def step_send_interrupt_signal(context, seconds):
|
|
"""Send interrupt signal (simulated)."""
|
|
# In actual tests, this would send SIGINT
|
|
context.interrupt_sent = True
|
|
|
|
|
|
@then("the command should complete successfully")
|
|
def step_command_success(context):
|
|
"""Verify command completed successfully."""
|
|
assert context.command_success, f"Command failed: {context.command_output}"
|
|
|
|
|
|
@then("the command should fail with an error message")
|
|
def step_command_should_fail(context):
|
|
"""Verify command failed as expected."""
|
|
assert not context.command_success, "Command should have failed but succeeded"
|
|
assert context.command_output, "No error message provided"
|
|
|
|
|
|
@then('the output should not contain "{text}"')
|
|
def step_output_not_contains(context, text):
|
|
"""Verify output does not contain specific text."""
|
|
assert text not in context.command_output, (
|
|
f"Did not expect '{text}' in output, got: {context.command_output}"
|
|
)
|
|
|
|
|
|
@then('the output should match the pattern "{pattern}"')
|
|
def step_output_matches_pattern(context, pattern):
|
|
"""Verify output matches regex pattern."""
|
|
assert re.search(pattern, context.command_output), (
|
|
f"Expected pattern '{pattern}' in output, got: {context.command_output}"
|
|
)
|
|
|
|
|
|
@then("each node should show progress before completion")
|
|
def step_nodes_show_progress(context):
|
|
"""Verify each node shows progress."""
|
|
nodes = [
|
|
"Loading context",
|
|
"Analyzing requirements",
|
|
"Generating plan",
|
|
"Validating",
|
|
]
|
|
for node in nodes:
|
|
assert node in context.command_output, f"Node '{node}' not found in output"
|
|
|
|
|
|
@then("timing should be sequential")
|
|
def step_timing_sequential(context):
|
|
"""Verify timing is sequential."""
|
|
# Extract all timing values
|
|
times = re.findall(r"\((\d+\.\d+)s\)", context.command_output)
|
|
assert len(times) > 0, "No timing information found"
|
|
|
|
|
|
@then("both commands should complete successfully")
|
|
def step_both_commands_success(context):
|
|
"""Verify both timed commands succeeded."""
|
|
# Check if this is a timing comparison test (with/without streaming)
|
|
if hasattr(context, "without_streaming_success") and hasattr(
|
|
context, "with_streaming_success"
|
|
):
|
|
assert context.without_streaming_success, "Non-streaming command failed"
|
|
assert context.with_streaming_success, "Streaming command failed"
|
|
# Check if this is a sequential commands test
|
|
elif hasattr(context, "all_commands_success"):
|
|
assert len(context.all_commands_success) >= 2, (
|
|
f"Expected at least 2 commands, got {len(context.all_commands_success)}"
|
|
)
|
|
assert all(context.all_commands_success), (
|
|
f"Not all commands succeeded: {context.all_commands_success}"
|
|
)
|
|
else:
|
|
# Fallback: check if the last command succeeded
|
|
assert hasattr(context, "command_success") and context.command_success, (
|
|
"Command failed"
|
|
)
|
|
|
|
|
|
@then("both should create valid plans")
|
|
def step_both_create_plans(context):
|
|
"""Verify both commands created valid plans."""
|
|
# Check that plans were created
|
|
from cleveragents.application.container import get_container
|
|
from cleveragents.application.services.plan_service import PlanService
|
|
|
|
container = get_container()
|
|
plan_service: PlanService = container.plan_service()
|
|
plans = plan_service.list_plans(context.project)
|
|
assert len(plans) > 0, "No plans were created"
|
|
|
|
|
|
@then("the output should show which node failed")
|
|
def step_output_shows_failed_node(context):
|
|
"""Verify output shows which node failed."""
|
|
# Should contain node name and error indication
|
|
assert "Error" in context.command_output or "Failed" in context.command_output
|
|
|
|
|
|
@then("the error message should be user-friendly")
|
|
def step_error_is_user_friendly(context):
|
|
"""Verify error message is user-friendly."""
|
|
# Should not contain stack traces or technical jargon
|
|
assert "Traceback" not in context.command_output
|
|
assert "Exception" not in context.command_output
|
|
|
|
|
|
@then('a plan named "{name}" should exist')
|
|
def step_plan_exists(context, name):
|
|
"""Verify a plan with the given name exists."""
|
|
from cleveragents.application.container import get_container
|
|
from cleveragents.application.services.plan_service import PlanService
|
|
|
|
container = get_container()
|
|
plan_service: PlanService = container.plan_service()
|
|
plans = plan_service.list_plans(context.project)
|
|
|
|
plan_names = [p.name for p in plans]
|
|
|
|
# Check both exact name and normalized name (lowercase with hyphens)
|
|
normalized_name = name.lower().replace(" ", "-").replace("_", "-")
|
|
assert name in plan_names or normalized_name in plan_names, (
|
|
f"Plan '{name}' (or normalized '{normalized_name}') not found. Available: {plan_names}"
|
|
)
|
|
|
|
|
|
@then("the output should show streaming progress")
|
|
def step_output_shows_streaming(context):
|
|
"""Verify output shows streaming progress indicators."""
|
|
indicators = ["⏳", "✓"]
|
|
assert any(ind in context.command_output for ind in indicators), (
|
|
"No streaming indicators found in output"
|
|
)
|
|
|
|
|
|
@then("the output should use colored text")
|
|
def step_output_uses_colors(context):
|
|
"""Verify output uses colored text (Rich formatting)."""
|
|
# Rich uses ANSI codes or unicode for formatting
|
|
# For simplicity, check for checkmarks and spinners
|
|
assert "✓" in context.command_output or "⏳" in context.command_output
|
|
|
|
|
|
@then("the output should have proper indentation")
|
|
def step_output_properly_indented(context):
|
|
"""Verify output has proper indentation."""
|
|
lines = context.command_output.split("\n")
|
|
indented_lines = [line for line in lines if line.startswith(" ")]
|
|
assert len(indented_lines) > 0, "No indented lines found"
|
|
|
|
|
|
@then("checkmarks and spinners should be properly rendered")
|
|
def step_symbols_rendered(context):
|
|
"""Verify symbols are properly rendered."""
|
|
assert "✓" in context.command_output, "Checkmark not found"
|
|
# Spinner might not be visible in captured output
|
|
|
|
|
|
@then("each should show independent progress")
|
|
def step_each_shows_independent_progress(context):
|
|
"""Verify each command shows independent progress."""
|
|
# This is implicitly tested by the command succeeding
|
|
assert context.command_success
|
|
|
|
|
|
@then("the loading context step should take longer")
|
|
def step_loading_takes_longer(context):
|
|
"""Verify loading context takes longer with more files."""
|
|
# Extract timing for loading context - match both "Loading context" variations
|
|
match = re.search(r"Loading context[^(]*\((\d+\.\d+)s\)", context.command_output)
|
|
assert match, (
|
|
f"Could not find loading context timing in output: {context.command_output[:500]}"
|
|
)
|
|
loading_time = float(match.group(1))
|
|
# With 10 files, should take measurable time (even 0.0 is valid for fast mocks)
|
|
assert loading_time >= 0
|
|
|
|
|
|
@then("all workflow stages should complete")
|
|
def step_all_stages_complete(context):
|
|
"""Verify all workflow stages completed."""
|
|
stages = ["Loading context", "Analyzing", "Generating", "Validating"]
|
|
for stage in stages:
|
|
assert stage in context.command_output, f"Stage '{stage}' did not complete"
|
|
|
|
|
|
@then("the command should exit gracefully")
|
|
def step_command_exits_gracefully(context):
|
|
"""Verify command exited gracefully after interrupt."""
|
|
# Command should have been interrupted
|
|
assert context.interrupt_sent
|
|
|
|
|
|
@then("partial progress should be shown")
|
|
def step_partial_progress_shown(context):
|
|
"""Verify partial progress was shown."""
|
|
# At least one stage should be visible
|
|
assert "✓" in context.command_output or "⏳" in context.command_output
|
|
|
|
|
|
@then("no corrupted state should remain")
|
|
def step_no_corrupted_state(context):
|
|
"""Verify no corrupted state remains."""
|
|
# Check that project state is still valid
|
|
from cleveragents.application.container import get_container
|
|
from cleveragents.application.services.project_service import ProjectService
|
|
|
|
container = get_container()
|
|
project_service: ProjectService = container.project_service()
|
|
project = project_service.get_current_project()
|
|
assert project is not None
|
|
|
|
|
|
@then("checkpoint data should be saved")
|
|
def step_checkpoint_saved(context):
|
|
"""Verify checkpoint data was saved."""
|
|
# LangGraph checkpoints are saved automatically
|
|
# This is more of a conceptual check
|
|
assert context.command_success
|
|
|
|
|
|
@then("the workflow should be resumable if interrupted")
|
|
def step_workflow_resumable(context):
|
|
"""Verify workflow is resumable."""
|
|
# With LangGraph checkpointing, workflows are resumable
|
|
assert context.command_success
|
|
|
|
|
|
@then("the validation node should show an error")
|
|
def step_validation_shows_error(context):
|
|
"""Verify validation node shows error."""
|
|
assert (
|
|
"Validating" in context.command_output
|
|
or "validation" in context.command_output.lower()
|
|
)
|
|
|
|
|
|
@then("the output should explain the validation failure")
|
|
def step_output_explains_failure(context):
|
|
"""Verify output explains the validation failure."""
|
|
# Should contain some explanation
|
|
assert (
|
|
"error" in context.command_output.lower()
|
|
or "invalid" in context.command_output.lower()
|
|
)
|
|
|
|
|
|
@then("suggestions for fixing should be provided")
|
|
def step_suggestions_provided(context):
|
|
"""Verify suggestions are provided."""
|
|
# This would require the actual implementation to provide suggestions
|
|
# For now, just check that there's some helpful output
|
|
assert len(context.command_output) > 100
|
|
|
|
|
|
@then("the output should be plain text")
|
|
def step_output_is_plain_text(context):
|
|
"""Verify output is plain text in non-TTY."""
|
|
# In non-TTY, Rich should output plain text
|
|
assert context.command_output
|
|
|
|
|
|
@then("progress information should still be visible")
|
|
def step_progress_still_visible(context):
|
|
"""Verify progress information is still visible."""
|
|
# Even in plain text, progress should be visible
|
|
nodes = ["Loading", "Analyzing", "Generating", "Validating"]
|
|
assert any(node in context.command_output for node in nodes)
|
|
|
|
|
|
@then('the description should mention "{text}"')
|
|
def step_description_mentions(context, text):
|
|
"""Verify description mentions specific text."""
|
|
# Check both stdout and stderr
|
|
combined_output = context.command_output + (
|
|
context.command_error if hasattr(context, "command_error") else ""
|
|
)
|
|
assert text in combined_output, (
|
|
f"Expected '{text}' not found in output: {combined_output}"
|
|
)
|
|
|
|
|
|
# Cleanup
|
|
def after_scenario(context, scenario):
|
|
"""Clean up after each scenario."""
|
|
import shutil
|
|
|
|
from cleveragents.application.container import reset_container
|
|
|
|
if hasattr(context, "temp_dir"):
|
|
# Change back to original directory
|
|
if hasattr(context, "original_dir"):
|
|
os.chdir(context.original_dir)
|
|
# Remove test directory
|
|
shutil.rmtree(context.temp_dir, ignore_errors=True)
|
|
|
|
# Clean up environment
|
|
if "CLEVERAGENTS_TESTING_USE_MOCK_AI" in os.environ:
|
|
del os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"]
|
|
if "CLEVERAGENTS_MOCK_SHOULD_FAIL" in os.environ:
|
|
del os.environ["CLEVERAGENTS_MOCK_SHOULD_FAIL"]
|
|
if "CLEVERAGENTS_MOCK_INVALID_CODE" in os.environ:
|
|
del os.environ["CLEVERAGENTS_MOCK_INVALID_CODE"]
|
|
|
|
# Reset container
|
|
reset_container()
|