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
279 lines
9.0 KiB
Python
279 lines
9.0 KiB
Python
"""Step definitions for context_analysis_new_coverage.feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
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 langchain_core.documents import Document
|
|
|
|
from cleveragents.agents.graphs.context_analysis import (
|
|
ContextAnalysisAgent,
|
|
ContextAnalysisState,
|
|
)
|
|
|
|
|
|
def _empty_state(**overrides: Any) -> ContextAnalysisState:
|
|
base: ContextAnalysisState = {
|
|
"file_paths": [],
|
|
"documents": [],
|
|
"dependencies": {},
|
|
"summary": "",
|
|
"relevance_scores": {},
|
|
"chunks": [],
|
|
"error": None,
|
|
}
|
|
base.update(overrides) # type: ignore[typeddict-item]
|
|
return base
|
|
|
|
|
|
@when("I create a ContextAnalysisAgent without an LLM")
|
|
def step_create_default(context: Context) -> None:
|
|
context.agent = ContextAnalysisAgent()
|
|
|
|
|
|
@then("the agent should use FakeListLLM")
|
|
def step_assert_fake_llm(context: Context) -> None:
|
|
from langchain_community.llms import FakeListLLM
|
|
|
|
assert isinstance(context.agent.llm, FakeListLLM)
|
|
|
|
|
|
@then("the agent should have a compiled graph")
|
|
def step_assert_compiled(context: Context) -> None:
|
|
assert context.agent.app is not None
|
|
|
|
|
|
@when("I create a ContextAnalysisAgent with a mock LLM")
|
|
def step_create_with_llm(context: Context) -> None:
|
|
from langchain_community.llms import FakeListLLM
|
|
|
|
custom_llm = FakeListLLM(responses=["custom response"])
|
|
context.agent = ContextAnalysisAgent(llm=custom_llm)
|
|
|
|
|
|
@then("the agent should use the provided LLM")
|
|
def step_assert_custom_llm(context: Context) -> None:
|
|
from langchain_community.llms import FakeListLLM
|
|
|
|
assert isinstance(context.agent.llm, FakeListLLM)
|
|
|
|
|
|
@given("a ContextAnalysisAgent instance")
|
|
def step_agent_instance(context: Context) -> None:
|
|
context.agent = ContextAnalysisAgent()
|
|
|
|
|
|
@when("I invoke _load_files with preloaded documents")
|
|
def step_load_preloaded(context: Context) -> None:
|
|
doc = Document(page_content="hello world", metadata={"source": "test.py"})
|
|
state = _empty_state(documents=[doc])
|
|
context.result = context.agent._load_files(state)
|
|
|
|
|
|
@then("the result should return those preloaded documents")
|
|
def step_assert_preloaded(context: Context) -> None:
|
|
assert len(context.result["documents"]) == 1
|
|
|
|
|
|
@given("a temporary Python file on disk")
|
|
def step_temp_py_file(context: Context) -> None:
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".py", delete=False, encoding="utf-8"
|
|
) as tmp:
|
|
tmp.write("import os\nimport sys\n\ndef main():\n pass\n")
|
|
tmp.flush()
|
|
context.temp_file = tmp.name
|
|
|
|
|
|
@when("I invoke _load_files with the temp file path")
|
|
def step_load_temp(context: Context) -> None:
|
|
state = _empty_state(file_paths=[context.temp_file])
|
|
context.result = context.agent._load_files(state)
|
|
|
|
|
|
@then("the result should contain loaded documents")
|
|
def step_assert_loaded(context: Context) -> None:
|
|
assert len(context.result["documents"]) > 0
|
|
|
|
|
|
@when("I invoke _load_files with a nonexistent file path")
|
|
def step_load_missing(context: Context) -> None:
|
|
state = _empty_state(file_paths=["/tmp/nonexistent_file_xyz.py"])
|
|
context.result = context.agent._load_files(state)
|
|
|
|
|
|
@then("the result error should mention file not found")
|
|
def step_assert_file_not_found(context: Context) -> None:
|
|
assert context.result["error"] is not None
|
|
assert "not found" in context.result["error"].lower()
|
|
|
|
|
|
@when("I invoke _load_files with a directory path")
|
|
def step_load_directory(context: Context) -> None:
|
|
state = _empty_state(file_paths=["/tmp"])
|
|
context.result = context.agent._load_files(state)
|
|
|
|
|
|
@then("the result error should mention not a file")
|
|
def step_assert_not_file(context: Context) -> None:
|
|
assert context.result["error"] is not None
|
|
assert "not a file" in context.result["error"].lower()
|
|
|
|
|
|
@given("a state with one loaded document")
|
|
def step_one_doc_state(context: Context) -> None:
|
|
doc = Document(
|
|
page_content="import os\nimport sys\n", metadata={"source": "test.py"}
|
|
)
|
|
context.state = _empty_state(documents=[doc])
|
|
|
|
|
|
@when("I invoke _analyze_dependencies")
|
|
def step_analyze_deps(context: Context) -> None:
|
|
context.result = context.agent._analyze_dependencies(context.state)
|
|
|
|
|
|
@then("the dependencies dict should have entries")
|
|
def step_assert_deps(context: Context) -> None:
|
|
assert len(context.result["dependencies"]) > 0
|
|
|
|
|
|
@when('I parse dependencies from "{text}"')
|
|
def step_parse_deps(context: Context, text: str) -> None:
|
|
context.result = context.agent._parse_dependencies(text)
|
|
|
|
|
|
@then("the parsed list should contain os sys pathlib")
|
|
def step_assert_parsed_deps(context: Context) -> None:
|
|
deps_lower = [d.lower() for d in context.result]
|
|
for name in ("os", "sys", "pathlib"):
|
|
assert any(name in d for d in deps_lower), (
|
|
f"'{name}' not found in {context.result}"
|
|
)
|
|
|
|
|
|
@given("a state with a small document")
|
|
def step_small_doc(context: Context) -> None:
|
|
doc = Document(page_content="short content", metadata={"source": "small.py"})
|
|
context.state = _empty_state(documents=[doc])
|
|
|
|
|
|
@when("I invoke _chunk_documents")
|
|
def step_chunk_docs(context: Context) -> None:
|
|
context.result = context.agent._chunk_documents(context.state)
|
|
|
|
|
|
@then("the chunks should contain the original document")
|
|
def step_assert_original_chunk(context: Context) -> None:
|
|
assert len(context.result["chunks"]) == 1
|
|
|
|
|
|
@given("a ContextAnalysisAgent instance with chunk_size {size:d}")
|
|
def step_agent_custom_chunk(context: Context, size: int) -> None:
|
|
context.agent = ContextAnalysisAgent(chunk_size=size, chunk_overlap=20)
|
|
|
|
|
|
@given("a state with a large document of {n:d} characters")
|
|
def step_large_doc(context: Context, n: int) -> None:
|
|
doc = Document(page_content="x" * n, metadata={"source": "large.py"})
|
|
context.state = _empty_state(documents=[doc])
|
|
|
|
|
|
@then("the chunks should contain more than one document")
|
|
def step_assert_multiple_chunks(context: Context) -> None:
|
|
assert len(context.result["chunks"]) > 1
|
|
|
|
|
|
@given("a state with chunks from two files")
|
|
def step_chunks_two_files(context: Context) -> None:
|
|
chunks = [
|
|
Document(page_content="code a", metadata={"source": "a.py"}),
|
|
Document(page_content="code b", metadata={"source": "b.py"}),
|
|
]
|
|
context.state = _empty_state(chunks=chunks)
|
|
|
|
|
|
@when("I invoke _score_relevance")
|
|
def step_score_relevance(context: Context) -> None:
|
|
context.result = context.agent._score_relevance(context.state)
|
|
|
|
|
|
@then("relevance scores should exist for both files")
|
|
def step_assert_both_scores(context: Context) -> None:
|
|
scores = context.result["relevance_scores"]
|
|
assert len(scores) == 2
|
|
|
|
|
|
@when('I parse relevance score from "{text}"')
|
|
def step_parse_score(context: Context, text: str) -> None:
|
|
context.parsed_score = context.agent._parse_relevance_score(text)
|
|
|
|
|
|
@then("the parsed score should be approximately {expected}")
|
|
def step_assert_approx_score(context: Context, expected: str) -> None:
|
|
assert abs(context.parsed_score - float(expected)) < 0.1, (
|
|
f"Expected ~{expected}, got {context.parsed_score}"
|
|
)
|
|
|
|
|
|
@given("a full analysis state with documents and scores")
|
|
def step_full_state(context: Context) -> None:
|
|
doc = Document(page_content="import os\n", metadata={"source": "main.py"})
|
|
context.state = _empty_state(
|
|
documents=[doc],
|
|
dependencies={"main.py": ["os"]},
|
|
relevance_scores={"main.py": 0.9},
|
|
chunks=[doc],
|
|
)
|
|
|
|
|
|
@when("I invoke _summarize_context")
|
|
def step_summarize(context: Context) -> None:
|
|
context.result = context.agent._summarize_context(context.state)
|
|
|
|
|
|
@then("the direct summary should be a non-empty string")
|
|
def step_assert_summary(context: Context) -> None:
|
|
assert context.result["summary"]
|
|
|
|
|
|
@when("I invoke the full workflow with the temp file")
|
|
def step_invoke_full(context: Context) -> None:
|
|
state = _empty_state(file_paths=[context.temp_file])
|
|
config = {"configurable": {"thread_id": "test-invoke"}}
|
|
context.result = context.agent.invoke(state, config)
|
|
|
|
|
|
@then("the result should contain summary and scores")
|
|
def step_assert_full_result(context: Context) -> None:
|
|
assert "summary" in context.result
|
|
assert "relevance_scores" in context.result
|
|
|
|
|
|
@when("I stream the workflow with the temp file")
|
|
def step_stream(context: Context) -> None:
|
|
state = _empty_state(file_paths=[context.temp_file])
|
|
config = {"configurable": {"thread_id": "test-stream"}}
|
|
context.events = list(context.agent.stream(state, config))
|
|
|
|
|
|
@then("at least one analysis event should be yielded")
|
|
def step_assert_events(context: Context) -> None:
|
|
assert len(context.events) > 0
|
|
|
|
|
|
@when("I wrap a chain with retry support")
|
|
def step_with_retry(context: Context) -> None:
|
|
mock_chain = MagicMock()
|
|
mock_chain.with_retry.return_value = mock_chain
|
|
context.result = context.agent._with_retry(mock_chain)
|
|
|
|
|
|
@then("the wrapped chain should be returned")
|
|
def step_assert_wrapped(context: Context) -> None:
|
|
assert context.result is not None
|