forked from HAL9000/cleveragents-core
e801eb1ee8
- Rewrite .forgejo/workflows/ci.yml to route all jobs through nox sessions - Fix coverage_report nox session: serial behave mode replaces broken parallel mode (22% -> 97% accuracy), raise fail-under from 85% to 97% - Pass posargs through format nox session for CI --check support - Add 11 CI workflow validation scenarios (Behave) + Robot smoke test + ASV bench - Add 108 new Behave scenarios covering 6 largest coverage gaps to reach 97%: yaml_template_engine, actor/config, actor/registry, message_router, context_analysis, context_service - Update docs/development/ci-cd.md with nox-based CI docs and 97% threshold - Restore implementation_plan.md verbose style, check off completed CI tasks Verified: 1673 scenarios pass, 97% coverage, lint clean, typecheck clean
365 lines
11 KiB
Python
365 lines
11 KiB
Python
"""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:
|
|
context.result = context.svc._get_context_agent()
|
|
|
|
|
|
@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:
|
|
context.result = context.svc.analyze_context(context.project)
|
|
|
|
|
|
@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:
|
|
context.result = context.svc.get_context_summary(context.project)
|
|
|
|
|
|
@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:
|
|
context.result = context.svc.get_context_dependencies(context.project)
|
|
|
|
|
|
@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:
|
|
context.events = list(context.svc.analyze_context_streaming(context.project))
|
|
|
|
|
|
@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
|