"""Behave steps for PlanGenerationGraph LangGraph coverage.""" from __future__ import annotations import importlib import importlib.util import sys from pathlib import Path from typing import Any from behave import given, then, when from langchain_community.llms import FakeListLLM PLAN_GEN_MODULE_PATH = ( Path(__file__).resolve().parents[2] / "src" / "cleveragents" / "agents" / "plan_generation.py" ) def _default_test_llm() -> FakeListLLM: """Create a FakeListLLM for test purposes.""" return FakeListLLM( responses=[ "Requirements: Add error handling with try-except blocks", "Generated code with proper error handling implementation", "Validation passed: Code follows best practices", ] ) def _load_plan_generation_module(context: Any) -> None: """Load the plan_generation module dynamically.""" if hasattr(context, "plan_generation_module"): return spec = importlib.util.spec_from_file_location( "cleveragents.agents.plan_generation", PLAN_GEN_MODULE_PATH ) if spec and spec.loader: module = importlib.util.module_from_spec(spec) sys.modules["cleveragents.agents.plan_generation"] = module spec.loader.exec_module(module) context.plan_generation_module = module @given("the langgraph plan generation module is importable") def step_langgraph_module_importable(context: Any) -> None: """Ensure the plan generation module can be imported.""" _load_plan_generation_module(context) assert hasattr(context, "plan_generation_module") assert hasattr(context.plan_generation_module, "PlanGenerationGraph") @when("I create a langgraph PlanGenerationGraph with no LLM") def step_create_langgraph_graph_no_llm(context: Any) -> None: """Create graph with explicit test LLM.""" _load_plan_generation_module(context) PlanGenerationGraph = context.plan_generation_module.PlanGenerationGraph context.graph = PlanGenerationGraph(llm=_default_test_llm()) @when("I create a langgraph PlanGenerationGraph with max_retries of {retries:d}") def step_create_langgraph_graph_with_retries(context: Any, retries: int) -> None: """Create graph with custom max_retries.""" _load_plan_generation_module(context) PlanGenerationGraph = context.plan_generation_module.PlanGenerationGraph context.graph = PlanGenerationGraph(llm=_default_test_llm(), max_retries=retries) @then("the langgraph graph should be initialized successfully") def step_langgraph_graph_initialized(context: Any) -> None: """Verify graph is initialized.""" assert context.graph is not None assert hasattr(context.graph, "llm") assert hasattr(context.graph, "graph") assert hasattr(context.graph, "app") @then("the langgraph graph should have a default FakeListLLM configured") def step_langgraph_graph_has_fake_llm(context: Any) -> None: """Verify FakeListLLM is used when explicitly provided.""" assert isinstance(context.graph.llm, FakeListLLM) @then("the langgraph graph should have max_retries set to {retries:d}") def step_langgraph_graph_max_retries(context: Any, retries: int) -> None: """Verify max_retries value.""" assert context.graph.max_retries == retries @then("the langgraph graph max_retries should be {retries:d}") def step_verify_langgraph_max_retries(context: Any, retries: int) -> None: """Verify max_retries value.""" assert context.graph.max_retries == retries @then("the langgraph graph should have an analyze_prompt template") def step_has_langgraph_analyze_prompt(context: Any) -> None: """Verify analyze_prompt exists.""" assert hasattr(context.graph, "analyze_prompt") assert context.graph.analyze_prompt is not None @then("the langgraph graph should have a generate_prompt template") def step_has_langgraph_generate_prompt(context: Any) -> None: """Verify generate_prompt exists.""" assert hasattr(context.graph, "generate_prompt") assert context.graph.generate_prompt is not None @then("the langgraph graph should have a validate_prompt template") def step_has_langgraph_validate_prompt(context: Any) -> None: """Verify validate_prompt exists.""" assert hasattr(context.graph, "validate_prompt") assert context.graph.validate_prompt is not None @then('the langgraph workflow graph should contain node "{node_name}"') def step_langgraph_graph_has_node(context: Any, node_name: str) -> None: """Verify graph has specific node.""" nodes = context.graph.graph.nodes assert node_name in nodes @given("I have a langgraph PlanGenerationGraph instance") def step_have_langgraph_graph_instance(context: Any) -> None: """Create a PlanGenerationGraph instance.""" _load_plan_generation_module(context) PlanGenerationGraph = context.plan_generation_module.PlanGenerationGraph context.graph = PlanGenerationGraph(llm=_default_test_llm()) @when("I format the langgraph context summary with no contexts") def step_format_langgraph_summary_no_contexts(context: Any) -> None: """Format context summary with empty list.""" summary = context.graph._format_context_summary([]) context.summary = summary @when("I format the langgraph context summary with {count:d} contexts") def step_format_langgraph_summary_n_contexts(context: Any, count: int) -> None: """Format context summary with N contexts.""" from cleveragents.domain.models.core import Context contexts = [ Context( plan_id=1, path=f"file{i}.py", content=f"# File {i} content\n" * 30, ) for i in range(count) ] summary = context.graph._format_context_summary(contexts) context.summary = summary context.context_count = count @then('the langgraph summary should be "{expected}"') def step_langgraph_summary_is(context: Any, expected: str) -> None: """Verify exact summary text.""" assert context.summary == expected @then("the langgraph summary should include all {count:d} file paths") def step_langgraph_summary_includes_files(context: Any, count: int) -> None: """Verify summary includes all files.""" expected = min(count, 5) # Max 5 files shown for i in range(expected): assert f"file{i}.py" in context.summary @then('the langgraph summary should indicate "and {count:d} more files"') def step_langgraph_summary_more_files(context: Any, count: int) -> None: """Verify 'more files' indicator.""" assert f"{count} more files" in context.summary @when("I execute the langgraph load_context node") def step_execute_langgraph_load_context(context: Any) -> None: """Execute load_context node.""" state: dict[str, Any] = {} result = context.graph._load_context(state) context.node_result = result @when("I execute the langgraph load_context node with sample contexts") def step_execute_langgraph_load_context_with_samples(context: Any) -> None: """Execute load_context node with example contexts.""" from cleveragents.domain.models.core import Context contexts = [ Context( plan_id=1, path="src/app.py", content="def app():\n return 1", ), Context(plan_id=1, path="src/utils.py", content="VALUE = 42"), ] state: dict[str, Any] = {"contexts": contexts} context.node_result = context.graph._load_context(state) @then("the langgraph node result should have retry_count set to {count:d}") def step_langgraph_node_retry_count(context: Any, count: int) -> None: """Verify retry_count in result.""" assert context.node_result.get("retry_count") == count @then("the langgraph node result should have error set to None") def step_langgraph_node_error_none(context: Any) -> None: """Verify error is None.""" assert context.node_result.get("error") is None @then("the langgraph node result should include context metadata defaults") def step_langgraph_node_defaults(context: Any) -> None: """Verify context metadata defaults are present.""" assert context.node_result.get("context_summary") == "No context files provided" assert context.node_result.get("context_dependencies") == {} assert context.node_result.get("context_relevance") == {} assert context.node_result.get("context_analysis_error") is None @then("the langgraph node result should include an analyzed context summary") def step_langgraph_node_has_summary(context: Any) -> None: """Ensure context summary is populated from analysis.""" summary = context.node_result.get("context_summary", "") assert summary assert summary != "No context files provided" @then("the langgraph node result should include context dependencies") def step_langgraph_node_has_dependencies(context: Any) -> None: """Ensure dependency metadata exists.""" deps = context.node_result.get("context_dependencies") assert isinstance(deps, dict) assert deps @given("I have a langgraph PlanGenerationGraph instance with max_retries {retries:d}") def step_langgraph_graph_with_max_retries(context: Any, retries: int) -> None: """Create graph with specific max_retries.""" _load_plan_generation_module(context) PlanGenerationGraph = context.plan_generation_module.PlanGenerationGraph context.graph = PlanGenerationGraph(llm=_default_test_llm(), max_retries=retries) @when( "I check langgraph should_retry with {status} validation and retry_count {count:d}" ) def step_check_langgraph_should_retry(context: Any, status: str, count: int) -> None: """Check should_retry decision.""" state: dict[str, Any] = { "validation_result": {"status": status}, "retry_count": count, } decision = context.graph._should_retry(state) context.retry_decision = decision context.final_retry_count = state.get("retry_count", count) @then('the langgraph retry decision should be "{decision}"') def step_langgraph_decision_is(context: Any, decision: str) -> None: """Verify retry decision.""" assert context.retry_decision == decision @when("I execute the langgraph validate node with no changes") def step_execute_langgraph_validate_no_changes(context: Any) -> None: """Execute validate with no changes.""" state: dict[str, Any] = {"generated_changes": []} result = context.graph._validate(state) context.node_result = result @then('the langgraph validation status should be "{status}"') def step_langgraph_validation_status(context: Any, status: str) -> None: """Verify validation status.""" validation = context.node_result.get("validation_result", {}) assert validation.get("status") == status @then('the langgraph validation message should contain "{text}"') def step_langgraph_validation_message_contains(context: Any, text: str) -> None: """Verify validation message contains text.""" validation = context.node_result.get("validation_result", {}) message = validation.get("message", "") assert text in message @when("I execute the langgraph generate_plan node with no requirements") def step_execute_langgraph_generate_no_requirements(context: Any) -> None: """Execute generate_plan with no requirements.""" state: dict[str, Any] = {"analyzed_requirements": {}} result = context.graph._generate_plan(state) context.node_result = result @then("the langgraph generated_changes should be empty") def step_langgraph_changes_empty(context: Any) -> None: """Verify changes list is empty.""" changes = context.node_result.get("generated_changes", []) assert len(changes) == 0 @when( "I execute the langgraph analyze_requirements node with a flaky LLM that fails once" ) def step_langgraph_analyze_with_flaky_llm(context: Any) -> None: """Execute analyze_requirements with a flaky LLM.""" from cleveragents.domain.models.core import Context as PlanContext class FlakyLLM(FakeListLLM): def __init__(self) -> None: super().__init__(responses=["Requirements succeeded after retry"]) object.__setattr__(self, "_call_count", 0) @property def call_count(self) -> int: return getattr(self, "_call_count", 0) def _call(self, prompt: str, stop: list[str] | None = None) -> str: object.__setattr__(self, "_call_count", self.call_count + 1) if self.call_count == 1: raise RuntimeError("transient failure") return super()._call(prompt, stop=stop) original_llm = context.graph.llm flaky_llm = FlakyLLM() context.graph.llm = flaky_llm contexts = [ PlanContext(plan_id=1, path="retry.py", content="print('retry')"), ] state: dict[str, Any] = { "prompt": "Add retry support", "contexts": contexts, "context_summary": "", } try: context.node_result = context.graph._analyze_requirements(state) finally: context.graph.llm = original_llm context.flaky_llm_calls = flaky_llm.call_count @then("the langgraph analyze node should succeed after retry") def step_langgraph_analyze_retry_success(context: Any) -> None: """Verify analyze_requirements succeeded after retry.""" result = context.node_result assert result.get("analyzed_requirements") assert not result.get("error") assert context.flaky_llm_calls >= 2 @then('the langgraph error should contain "{text}"') def step_langgraph_error_contains(context: Any, text: str) -> None: """Verify error message contains text.""" error = context.node_result.get("error") assert error is not None assert text in error @given("I have langgraph workflow inputs with project plan and contexts") def step_have_langgraph_workflow_inputs(context: Any) -> None: """Create workflow inputs.""" from cleveragents.domain.models.core import Context, Plan, Project context.project = Project(id=1, name="test_project", path=Path("/tmp/test")) context.plan = Plan(id=1, project_id=1, name="test_plan", prompt="Test") context.contexts = [Context(plan_id=1, path="test.py", content="# test")] @when("I invoke the langgraph workflow synchronously") def step_invoke_langgraph_workflow_sync(context: Any) -> None: """Invoke workflow synchronously.""" result = context.graph.invoke(context.project, context.plan, context.contexts) context.result = result @then("the langgraph workflow result should contain all expected fields") def step_langgraph_result_has_all_fields(context: Any) -> None: """Verify all expected fields.""" expected_fields = [ "project", "plan", "contexts", "context_summary", "context_dependencies", "context_relevance", "context_analysis_error", "prompt", "analyzed_requirements", "generated_changes", "validation_result", "retry_count", "error", ] for field in expected_fields: assert field in context.result @when("I stream the langgraph workflow execution") def step_stream_langgraph_workflow(context: Any) -> None: """Stream workflow execution.""" events = list(context.graph.stream(context.project, context.plan, context.contexts)) context.stream_events = events @then("the langgraph stream should yield multiple events") def step_langgraph_stream_yields_events(context: Any) -> None: """Verify stream yields events.""" assert len(context.stream_events) > 0 @given("the langgraph graphs package is importable") def step_langgraph_graphs_package_importable(context: Any) -> None: """Import the graphs package for LangGraph workflows.""" context.langgraph_graphs_package = importlib.import_module( "cleveragents.agents.graphs" ) @then('the langgraph graphs exports should include "{symbol}"') def step_langgraph_graphs_exports_include(context: Any, symbol: str) -> None: """Verify the graphs package exports include the symbol.""" package = getattr(context, "langgraph_graphs_package", None) assert package is not None, "LangGraph graphs package not imported" exports = getattr(package, "__all__", []) assert symbol in exports, f"{symbol} not listed in __all__" assert getattr(package, symbol, None) is not None, ( f"Package missing attribute {symbol}" ) @given("the agents package is importable") def step_agents_package_importable(context: Any) -> None: """Import the top-level agents package.""" context.agents_package = importlib.import_module("cleveragents.agents") @then('the agents package exports should include "{symbol}"') def step_agents_package_exports_include(context: Any, symbol: str) -> None: """Verify the agents package exports include the symbol.""" package = getattr(context, "agents_package", None) assert package is not None, "Agents package was not imported" exports = getattr(package, "__all__", []) assert symbol in exports, f"{symbol} not in agents __all__" assert getattr(package, symbol, None) is not None, ( f"Agents package missing attribute {symbol}" )