"""Step definitions for context_service_new_coverage.feature.""" from __future__ import annotations import tempfile from pathlib import Path from typing import Any from unittest.mock import MagicMock from behave import given, then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] from cleveragents.application.services.context_service import ContextService from cleveragents.core.exceptions import ConfigurationError from cleveragents.domain.models.core import ( Context as CtxModel, ) from cleveragents.domain.models.core import ( ContextType, Plan, Project, ) def _make_project( project_id: int | None = 1, ) -> Project: return Project( id=project_id, name="test-project", path=Path("/tmp/test-project"), ) def _make_plan(plan_id: int = 10) -> Plan: plan = MagicMock(spec=Plan) plan.id = plan_id plan.name = "test-plan" return plan def _make_uow( *, plan: Plan | None = None, contexts: list[CtxModel] | None = None ) -> MagicMock: uow = MagicMock() ctx_mgr = MagicMock() ctx_mgr.plans.get_current_for_project.return_value = plan ctx_mgr.contexts.get_for_plan.return_value = contexts or [] uow.transaction.return_value.__enter__ = MagicMock(return_value=ctx_mgr) uow.transaction.return_value.__exit__ = MagicMock(return_value=False) return uow def _make_settings(**overrides: Any) -> MagicMock: settings = MagicMock() settings.is_langsmith_enabled = overrides.get("langsmith", False) settings.build_langsmith_config.return_value = overrides.get("langsmith_config", {}) settings.cleveragents_max_context_size = 50 * 1024 * 1024 return settings @given("a context service with mocked dependencies") def step_context_service(context: Context) -> None: context.settings = _make_settings() context.uow = _make_uow() context.svc = ContextService(context.settings, context.uow) @given("a project without an id") def step_project_no_id(context: Context) -> None: context.project = _make_project(project_id=None) @when("I get the current plan") def step_get_plan(context: Context) -> None: context.result = context.svc._get_current_plan(context.project) @then("the context result should be None") def step_assert_none(context: Context) -> None: assert context.result is None @given("a project with id {pid:d}") def step_project_with_id(context: Context, pid: int) -> None: context.project = _make_project(project_id=pid) @when("I build langsmith config with langsmith disabled") def step_build_langsmith_disabled(context: Context) -> None: context.result = context.svc._build_langsmith_config( context.project, run_name="test", file_paths=["a.py"], mode="sync", ) @then("the config should be an empty dict") def step_assert_empty_config(context: Context) -> None: assert context.result == {} @given("a context service with langsmith enabled") def step_svc_langsmith(context: Context) -> None: context.settings = _make_settings( langsmith=True, langsmith_config={ "metadata": {"project_id": 1}, "tags": ["context-analysis"], }, ) context.uow = _make_uow(plan=_make_plan()) context.svc = ContextService(context.settings, context.uow) @when("I build langsmith config with langsmith enabled") def step_build_langsmith_enabled(context: Context) -> None: context.result = context.svc._build_langsmith_config( context.project, run_name="test", file_paths=["a.py"], mode="sync", ) @then("the config should contain metadata and tags") def step_assert_langsmith_config(context: Context) -> None: assert isinstance(context.result, dict) assert len(context.result) > 0 @when("I prepare analysis config") def step_prepare_config(context: Context) -> None: context.result = context.svc._prepare_analysis_config( context.project, run_name="test", file_paths=["a.py"], mode="sync", ) @then("the config should have a configurable thread_id") def step_assert_thread_id(context: Context) -> None: assert "configurable" in context.result assert "thread_id" in context.result["configurable"] @given("a context service with no vector store") def step_svc_no_vector(context: Context) -> None: context.settings = _make_settings() context.uow = _make_uow() context.svc = ContextService( context.settings, context.uow, vector_store_service=None ) @when("I check vector store availability") def step_check_vector(context: Context) -> None: context.result = context.svc._vector_store_available() @then("the vector store check should return False") def step_assert_false(context: Context) -> None: assert context.result is False @when("I refresh vector index for plan {pid:d}") def step_refresh_vector(context: Context, pid: int) -> None: context.error = None try: context.svc._refresh_vector_index(pid) except Exception as exc: context.error = exc @then("no exception should be raised") def step_assert_no_error(context: Context) -> None: assert context.error is None @given("a context service with a failing vector store") def step_svc_failing_vector(context: Context) -> None: context.settings = _make_settings() context.uow = _make_uow() vs = MagicMock() vs.is_enabled.return_value = True vs.refresh_for_plan.side_effect = ConfigurationError(message="vector store error") context.svc = ContextService(context.settings, context.uow, vector_store_service=vs) @when("I get the context agent") def step_get_agent(context: Context) -> None: from langchain_community.llms import FakeListLLM context.result = context.svc._get_context_agent( llm=FakeListLLM( responses=["Dependencies: ['os']", "Relevance: High", "Summary: test"] * 3 ), ) @then("the context agent should be a ContextAnalysisAgent instance") def step_assert_agent(context: Context) -> None: from cleveragents.agents.graphs.context_analysis import ContextAnalysisAgent assert isinstance(context.result, ContextAnalysisAgent) @given("a project that has no context files") def step_project_no_files(context: Context) -> None: context.project = _make_project() # list_files returns empty when no plan or empty contexts context.uow = _make_uow(plan=None) context.svc = ContextService(context.settings, context.uow) @when("I analyze context") def step_analyze_context(context: Context) -> None: from langchain_community.llms import FakeListLLM _fake_llm = FakeListLLM( responses=["Dependencies: ['os']", "Relevance: High", "Summary: test"] * 3 ) context.result = context.svc.analyze_context(context.project, llm=_fake_llm) @then("the result summary should indicate no files") def step_assert_no_files_summary(context: Context) -> None: assert ( "no context" in context.result["summary"].lower() or context.result["summary"] ) @given("a project with context files") def step_project_with_files(context: Context) -> None: context.project = _make_project() # Create a real temp file for the agent to load with tempfile.NamedTemporaryFile( mode="w", suffix=".py", delete=False, encoding="utf-8" ) as tmp: tmp.write("import os\n\ndef main():\n pass\n") tmp.flush() context.temp_file = tmp.name plan = _make_plan() ctx_entry = CtxModel( id=1, plan_id=10, path=tmp.name, type=ContextType.FILE, content="import os", file_hash="abc123", size=10, ) context.uow = _make_uow(plan=plan, contexts=[ctx_entry]) context.svc = ContextService(context.settings, context.uow) @then("the result should have documents and summary") def step_assert_docs_summary(context: Context) -> None: assert "summary" in context.result @when("I retrieve the context summary") def step_get_summary(context: Context) -> None: from langchain_community.llms import FakeListLLM _fake_llm = FakeListLLM( responses=["Dependencies: ['os']", "Relevance: High", "Summary: test"] * 3 ) context.result = context.svc.get_context_summary(context.project, llm=_fake_llm) @then("the context summary should be a nonempty string") def step_assert_nonempty_summary(context: Context) -> None: assert isinstance(context.result, str) assert len(context.result) > 0 @when("I get context dependencies") def step_get_deps(context: Context) -> None: from langchain_community.llms import FakeListLLM _fake_llm = FakeListLLM( responses=["Dependencies: ['os']", "Relevance: High", "Summary: test"] * 3 ) context.result = context.svc.get_context_dependencies( context.project, llm=_fake_llm ) @then("the context deps result should be a dict") def step_assert_dict(context: Context) -> None: assert isinstance(context.result, dict) @when("I fetch relevant files above threshold {t}") def step_get_relevant(context: Context, t: str) -> None: try: context.result = context.svc.get_relevant_files( context.project, threshold=float(t) ) except Exception: context.result = [] @then("the relevant files should be a list of tuples") def step_assert_tuples(context: Context) -> None: assert isinstance(context.result, list) @when("I search context with empty query") def step_search_empty(context: Context) -> None: context.result = context.svc.search_context(context.project, " ") @then("the context search result should be an empty list") def step_assert_empty_list(context: Context) -> None: assert context.result == [] @given("a project with id {pid:d} but no current plan") def step_project_no_plan(context: Context, pid: int) -> None: context.project = _make_project(project_id=pid) context.uow = _make_uow(plan=None) context.svc = ContextService(context.settings, context.uow) @when('I search context with query "{query}"') def step_search_query(context: Context, query: str) -> None: context.result = context.svc.search_context(context.project, query) @given("a project with id {pid:d} and a current plan") def step_project_with_plan(context: Context, pid: int) -> None: context.project = _make_project(project_id=pid) # Only create new svc if one doesn't already exist (preserve vector store setup) if not hasattr(context, "svc") or context.svc is None: plan = _make_plan() context.uow = _make_uow(plan=plan) context.svc = ContextService(context.settings, context.uow) @given("a context service with a working vector store") def step_svc_vector(context: Context) -> None: context.settings = _make_settings() plan = _make_plan() context.uow = _make_uow(plan=plan) vs = MagicMock() vs.is_enabled.return_value = True vs.search.return_value = [{"path": "a.py", "score": 0.9}] context.vs = vs context.svc = ContextService(context.settings, context.uow, vector_store_service=vs) @then("the vector store search should be called") def step_assert_vs_called(context: Context) -> None: context.vs.search.assert_called() @given("a context service with a vector store that raises ConfigurationError") def step_svc_vector_config_error(context: Context) -> None: context.settings = _make_settings() plan = _make_plan() context.uow = _make_uow(plan=plan) vs = MagicMock() vs.is_enabled.return_value = True vs.search.side_effect = ConfigurationError(message="config error") context.svc = ContextService(context.settings, context.uow, vector_store_service=vs) @when("I stream analyze context") def step_stream_analyze(context: Context) -> None: from langchain_community.llms import FakeListLLM _fake_llm = FakeListLLM( responses=["Dependencies: ['os']", "Relevance: High", "Summary: test"] * 3 ) context.events = list( context.svc.analyze_context_streaming(context.project, llm=_fake_llm) ) @then("the first event should indicate no files") def step_assert_first_event_no_files(context: Context) -> None: assert len(context.events) > 0 first = context.events[0] assert "complete" in str(first.get("type", "")) or "summary" in first @then("at least one streaming event should be yielded") def step_assert_events_yielded(context: Context) -> None: assert len(context.events) > 0