a8f7ed57cb
Created scripts/create_template_db.py that builds a pre-migrated SQLite template database using Base.metadata.create_all() + alembic stamp (~5ms for 34 tables, vs ~0.5-3s x 25 Alembic migrations per scenario). Nox unit_tests and coverage_report sessions generate the template before test execution and propagate CLEVERAGENTS_TEMPLATE_DB env var to all workers. features/environment.py before_all() installs a monkey-patch on MigrationRunner.init_or_upgrade that copies the template for fresh scenario temp DBs, falling through to real migrations for :memory:, existing files, and migration-runner unit tests. Quick wins: sleep(0.5) -> sleep(0.05) in cli_streaming wait step; removed redundant Background re-declaration in cli_streaming.feature scenario 7. ISSUES CLOSED: #483
656 lines
23 KiB
Python
656 lines
23 KiB
Python
"""Step definitions for CLI streaming tests."""
|
|
|
|
import ast
|
|
import os
|
|
import re
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import parse
|
|
from behave import given, register_type, then, when
|
|
|
|
from features.steps.provider_stub_utils import (
|
|
RecordingProviderStub,
|
|
install_provider_resolver_patch,
|
|
)
|
|
|
|
|
|
@parse.with_pattern(r"[^\"]*")
|
|
def _parse_optional_quoted(text: str) -> str:
|
|
return text
|
|
|
|
|
|
register_type(OptionalQuoted=_parse_optional_quoted)
|
|
|
|
|
|
@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)
|
|
|
|
# Give this invocation a unique database so repeated calls within the
|
|
# same scenario (e.g. Background + explicit Given) never collide.
|
|
db_path = tempfile.mktemp(suffix=".db", prefix="cleveragents_")
|
|
os.environ["CLEVERAGENTS_DATABASE_URL"] = f"sqlite:///{db_path}"
|
|
context._scenario_db_paths = getattr(context, "_scenario_db_paths", [])
|
|
context._scenario_db_paths.append(db_path)
|
|
|
|
# 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("I have a context file with {count:d} python files")
|
|
def step_add_python_context_files(context, count):
|
|
"""Create and add python context files to the project."""
|
|
from cleveragents.cli.commands.context import add_command
|
|
|
|
files = []
|
|
for i in range(count):
|
|
context_file = Path.cwd() / f"context_{i}.py"
|
|
context_file.write_text("print('context file')\n")
|
|
files.append(str(context_file))
|
|
|
|
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
|
|
|
|
|
|
@given(
|
|
'actor overrides are stubbed with default "{default_actor}" and alternates "{alternates:OptionalQuoted}"'
|
|
)
|
|
def step_stub_streaming_actor_overrides(context, default_actor, alternates):
|
|
"""Install deterministic provider stubs keyed by actor selection."""
|
|
default_key = default_actor.lower()
|
|
actor_names = [
|
|
name.strip().lower() for name in alternates.split(",") if name.strip()
|
|
]
|
|
if default_key not in actor_names:
|
|
actor_names.insert(0, default_key)
|
|
|
|
overrides: dict[str, RecordingProviderStub] = {
|
|
name: RecordingProviderStub(name=name) for name in actor_names
|
|
}
|
|
install_provider_resolver_patch(
|
|
context,
|
|
override_map=overrides,
|
|
default_provider=default_key,
|
|
)
|
|
context.provider_stub_overrides = overrides
|
|
|
|
|
|
@given('actor stub "{actor}" will fail {count:d} time(s) before succeeding')
|
|
def step_stub_failures_before_success(context, actor, count):
|
|
"""Configure a stub to raise exceptions a fixed number of times."""
|
|
stub = _get_actor_stub(context, actor)
|
|
stub.should_fail = False
|
|
stub.failures_before_success = count
|
|
stub._failure_count = 0
|
|
|
|
|
|
@given('actor stub "{actor}" will always fail')
|
|
def step_stub_always_fail(context, actor):
|
|
"""Force a stub to fail every invocation."""
|
|
stub = _get_actor_stub(context, actor)
|
|
stub.should_fail = 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)
|
|
|
|
|
|
def _run_streaming_tell(
|
|
context,
|
|
prompt: str,
|
|
*,
|
|
name: str | None = None,
|
|
actor: str | None = None,
|
|
) -> bool:
|
|
"""Execute the streaming tell helper and capture its output."""
|
|
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()
|
|
|
|
output = StringIO()
|
|
test_console = Console(file=output, force_terminal=False, width=120)
|
|
|
|
success = False
|
|
error_message = ""
|
|
try:
|
|
with patch("cleveragents.cli.commands.plan.console", test_console):
|
|
asyncio.run(
|
|
_tell_streaming(
|
|
context.project,
|
|
prompt,
|
|
name,
|
|
plan_service,
|
|
actor=actor,
|
|
)
|
|
)
|
|
success = True
|
|
except Exception as exc: # pragma: no cover - diagnostics surface via assertions
|
|
error_message = str(exc)
|
|
finally:
|
|
rendered_output = _strip_rich_markup(output.getvalue())
|
|
context.command_output = (
|
|
rendered_output if success else f"{rendered_output}{error_message}"
|
|
)
|
|
context.command_success = success
|
|
if not hasattr(context, "all_commands_success"):
|
|
context.all_commands_success = []
|
|
context.all_commands_success.append(success)
|
|
return success
|
|
|
|
|
|
def _get_actor_stub(context, actor: str) -> RecordingProviderStub:
|
|
overrides = getattr(context, "provider_stub_overrides", None)
|
|
assert overrides, "Provider stubs were not configured for this scenario"
|
|
stub = overrides.get(actor.lower())
|
|
assert stub is not None, f"No provider stub named '{actor}' was registered"
|
|
return stub
|
|
|
|
|
|
@when('I run tell command "{prompt}" with streaming')
|
|
def step_run_tell_with_streaming(context, prompt):
|
|
"""Run a tell command with streaming."""
|
|
_run_streaming_tell(context, prompt)
|
|
|
|
|
|
@given('actor stub "{actor}" uses config blob {config_blob}')
|
|
def step_configure_actor_stub_blob(context, actor, config_blob):
|
|
"""Attach a configuration blob to the stubbed actor."""
|
|
stub = _get_actor_stub(context, actor)
|
|
stub.config_blob = ast.literal_eval(config_blob)
|
|
|
|
|
|
@then(
|
|
'provider stub "{actor}" should receive actor context with options {options_blob} graph {graph_descriptor} initial context {initial_context}'
|
|
)
|
|
def step_assert_actor_context(
|
|
context, actor, options_blob, graph_descriptor, initial_context
|
|
):
|
|
stub = _get_actor_stub(context, actor)
|
|
ctx = stub.last_actor_context
|
|
assert ctx is not None, "Actor context was not captured for the provider stub"
|
|
expected_options = ast.literal_eval(options_blob)
|
|
expected_graph = ast.literal_eval(graph_descriptor)
|
|
expected_initial = ast.literal_eval(initial_context)
|
|
assert ctx.options == expected_options, ctx.options
|
|
assert ctx.graph_descriptor == expected_graph, ctx.graph_descriptor
|
|
assert ctx.initial_context == expected_initial, ctx.initial_context
|
|
|
|
|
|
@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."""
|
|
context.custom_plan_name = name
|
|
_run_streaming_tell(context, prompt, name=name)
|
|
|
|
|
|
@when('I run tell command "{prompt}" with streaming and actor "{actor}"')
|
|
def step_run_tell_with_streaming_actor(context, prompt, actor):
|
|
"""Run a streaming tell command that overrides the actor."""
|
|
_run_streaming_tell(context, prompt, actor=actor)
|
|
|
|
|
|
@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.05)
|
|
|
|
|
|
@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('the actor resolver should resolve actors "{actors}" in order')
|
|
def step_actor_resolver_sequence(context, actors):
|
|
"""Assert the patched resolver returned actors in the expected order."""
|
|
calls = getattr(context, "provider_resolver_calls", None)
|
|
assert calls is not None, "Provider resolver patch was not installed"
|
|
expected = [name.strip().lower() for name in actors.split(",") if name.strip()]
|
|
assert expected, "Expected actor list cannot be empty"
|
|
resolved = [str(call.get("resolved", "")).lower() for call in calls]
|
|
assert resolved[-len(expected) :] == expected, (
|
|
f"Expected resolver sequence {expected}, got {resolved}"
|
|
)
|
|
|
|
|
|
@then('actor stub "{actor}" should record {count:d} streaming call(s)')
|
|
def step_assert_stub_stream_calls(context, actor, count):
|
|
"""Verify a provider stub handled the expected number of streaming requests."""
|
|
stub = _get_actor_stub(context, actor)
|
|
assert stub.stream_calls == count, (
|
|
f"Expected {count} stream calls for {actor}, got {stub.stream_calls}"
|
|
)
|
|
|
|
|
|
@then('actor stub "{actor}" should record {count:d} generate call(s)')
|
|
def step_assert_stub_generate_calls(context, actor, count):
|
|
"""Verify a provider stub handled the expected number of build requests."""
|
|
stub = _get_actor_stub(context, actor)
|
|
assert stub.generate_calls == count, (
|
|
f"Expected {count} generate calls for {actor}, got {stub.generate_calls}"
|
|
)
|
|
|
|
|
|
@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()
|