c56601eaf5
CI / push-validation (pull_request) Successful in 30s
CI / lint (pull_request) Successful in 41s
CI / helm (pull_request) Successful in 46s
CI / build (pull_request) Successful in 51s
CI / quality (pull_request) Successful in 54s
CI / typecheck (pull_request) Successful in 1m16s
CI / security (pull_request) Successful in 1m25s
CI / unit_tests (pull_request) Successful in 5m36s
CI / integration_tests (pull_request) Successful in 10m20s
CI / docker (pull_request) Successful in 1m42s
CI / coverage (pull_request) Successful in 11m27s
CI / status-check (pull_request) Successful in 9s
274 lines
10 KiB
Python
274 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
|
|
from cleveragents.providers.llm.anthropic_provider import AnthropicChatProvider
|
|
|
|
# Shared step definitions (I have sample provider domain inputs, plan generation graph steps)
|
|
# are registered by features/steps/provider_shared_steps.py which Behave loads automatically.
|
|
# Helper functions are duplicated here to avoid cross-module import issues with Behave's exec_file.
|
|
|
|
|
|
def _register_cleanup(context, cleanup):
|
|
if hasattr(context, "add_cleanup"):
|
|
context.add_cleanup(cleanup)
|
|
else:
|
|
cleanup_handlers = getattr(context, "_cleanup_handlers", [])
|
|
cleanup_handlers.append(cleanup)
|
|
context._cleanup_handlers = cleanup_handlers
|
|
|
|
|
|
def _parse_kwargs_string(kwargs_string: str) -> dict[str, Any]:
|
|
if not kwargs_string:
|
|
return {}
|
|
result: dict[str, Any] = {}
|
|
for entry in kwargs_string.split(","):
|
|
entry = entry.strip()
|
|
if not entry:
|
|
continue
|
|
if "=" not in entry:
|
|
result[entry] = True
|
|
continue
|
|
key, value = entry.split("=", 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
try:
|
|
parsed_value = ast.literal_eval(value)
|
|
except Exception:
|
|
parsed_value = value
|
|
result[key] = parsed_value
|
|
return result
|
|
|
|
|
|
def _setup_plan_generation_graph(context) -> MagicMock:
|
|
patcher = patch(
|
|
"cleveragents.providers.llm.langchain_chat_provider.PlanGenerationGraph"
|
|
)
|
|
context.plan_generation_patcher = patcher
|
|
mock_graph_class = patcher.start()
|
|
_register_cleanup(context, patcher.stop)
|
|
mock_graph_instance = MagicMock(name="PlanGenerationGraphInstance")
|
|
|
|
default_state = {
|
|
"generated_changes": [],
|
|
"validation_result": {"status": "PASS"},
|
|
"error": None,
|
|
}
|
|
state_override = getattr(context, "plan_generation_state_override", None)
|
|
mock_graph_instance.invoke.return_value = state_override or default_state
|
|
|
|
invoke_side_effect = getattr(context, "plan_generation_invoke_side_effect", None)
|
|
if invoke_side_effect is not None:
|
|
mock_graph_instance.invoke.side_effect = invoke_side_effect
|
|
|
|
stream_events = getattr(context, "plan_generation_stream_events", None)
|
|
if stream_events is None:
|
|
mock_graph_instance.stream.return_value = iter(())
|
|
else:
|
|
events_copy = list(stream_events)
|
|
|
|
def _stream(*_args, **_kwargs):
|
|
yield from events_copy
|
|
|
|
mock_graph_instance.stream.side_effect = _stream
|
|
|
|
mock_graph_class.return_value = mock_graph_instance
|
|
context.plan_generation_graph = mock_graph_instance
|
|
context.plan_generation_graph_class = mock_graph_class
|
|
return mock_graph_instance
|
|
|
|
|
|
@given('I set the Anthropic provider extra kwargs "{kwargs_string}"')
|
|
def step_set_anthropic_provider_kwargs(context, kwargs_string):
|
|
context.anthropic_provider_kwargs = _parse_kwargs_string(kwargs_string)
|
|
|
|
|
|
@given(
|
|
'I create an Anthropic chat provider with API key "{api_key}" and model "{model_id}"'
|
|
)
|
|
@when(
|
|
'I create an Anthropic chat provider with API key "{api_key}" and model "{model_id}"'
|
|
)
|
|
def step_create_anthropic_provider(context, api_key, model_id):
|
|
patcher = patch("cleveragents.providers.llm.anthropic_provider.ChatAnthropic")
|
|
context.chat_anthropic_patcher = patcher
|
|
mock_chat_anthropic_class = patcher.start()
|
|
_register_cleanup(context, patcher.stop)
|
|
mock_chat_anthropic_instance = MagicMock(name="ChatAnthropicInstance")
|
|
mock_chat_anthropic_instance.get_num_tokens = MagicMock(return_value=0)
|
|
mock_chat_anthropic_class.return_value = mock_chat_anthropic_instance
|
|
|
|
extra_kwargs = dict(getattr(context, "anthropic_provider_kwargs", {}))
|
|
|
|
context.provider = AnthropicChatProvider(
|
|
api_key=api_key,
|
|
model=model_id,
|
|
**extra_kwargs,
|
|
)
|
|
context.chat_anthropic_class = mock_chat_anthropic_class
|
|
context.chat_anthropic_instance = mock_chat_anthropic_instance
|
|
|
|
|
|
@when("I attempt to create an Anthropic chat provider without an API key")
|
|
def step_anthropic_provider_without_api_key(context):
|
|
context.anthropic_provider_error = None
|
|
try:
|
|
AnthropicChatProvider(api_key="", model="claude-sonnet-4-20250514")
|
|
except Exception as exc: # pragma: no cover - defensive logging only
|
|
context.anthropic_provider_error = exc
|
|
|
|
|
|
@when("I request plan generation from the Anthropic provider")
|
|
def step_request_plan_generation(context):
|
|
_setup_plan_generation_graph(context)
|
|
|
|
context.response = context.provider.generate_changes(
|
|
context.project,
|
|
context.plan,
|
|
context.contexts,
|
|
)
|
|
context.chat_anthropic_call = context.chat_anthropic_class.call_args
|
|
context.plan_generation_graph_call = context.plan_generation_graph_class.call_args
|
|
|
|
|
|
@when("I stream plan generation from the Anthropic provider")
|
|
def step_stream_plan_generation(context):
|
|
_setup_plan_generation_graph(context)
|
|
context.streamed_events = list(
|
|
context.provider.stream_changes(
|
|
context.project,
|
|
context.plan,
|
|
context.contexts,
|
|
)
|
|
)
|
|
context.chat_anthropic_call = context.chat_anthropic_class.call_args
|
|
context.plan_generation_graph_call = context.plan_generation_graph_class.call_args
|
|
|
|
|
|
@then(
|
|
'the Anthropic provider should construct ChatAnthropic with api key "{api_key}" and model "{model_id}"'
|
|
)
|
|
def step_assert_chat_anthropic_constructor(context, api_key, model_id):
|
|
assert context.chat_anthropic_call is not None, (
|
|
"ChatAnthropic should have been called"
|
|
)
|
|
call_args, call_kwargs = context.chat_anthropic_call
|
|
assert call_args == (), "ChatAnthropic should be called with keyword arguments"
|
|
assert call_kwargs["api_key"] == api_key
|
|
assert call_kwargs["model"] == model_id
|
|
|
|
assert context.plan_generation_graph_call is not None, (
|
|
"PlanGenerationGraph should receive the ChatAnthropic instance"
|
|
)
|
|
_, graph_kwargs = context.plan_generation_graph_call
|
|
assert graph_kwargs["llm"] is context.chat_anthropic_instance
|
|
|
|
|
|
@then(
|
|
'the Anthropic provider should include kwargs "{kwargs_string}" in the ChatAnthropic call'
|
|
)
|
|
def step_assert_chat_anthropic_kwargs(context, kwargs_string):
|
|
assert context.chat_anthropic_call is not None, (
|
|
"ChatAnthropic should have been called"
|
|
)
|
|
_, call_kwargs = context.chat_anthropic_call
|
|
expected_kwargs = _parse_kwargs_string(kwargs_string)
|
|
for key, value in expected_kwargs.items():
|
|
assert key in call_kwargs, f"Expected {key} in ChatAnthropic kwargs"
|
|
assert call_kwargs[key] == value, (
|
|
f"Expected ChatAnthropic kwargs[{key!r}] to equal {value!r}"
|
|
)
|
|
|
|
|
|
@then(
|
|
'the Anthropic provider metadata should report name "{expected_name}" and model "{expected_model}"'
|
|
)
|
|
def step_assert_provider_metadata(context, expected_name, expected_model):
|
|
provider = getattr(context, "provider", None)
|
|
assert provider is not None, "Provider should exist"
|
|
assert provider.name == expected_name
|
|
assert provider.model_id == expected_model
|
|
response = getattr(context, "response", None)
|
|
if response is not None:
|
|
assert response.model_used == expected_model
|
|
|
|
|
|
@then(
|
|
"the Anthropic provider response should report the requested model without errors"
|
|
)
|
|
def step_assert_placeholder_metadata(context):
|
|
assert context.response is not None
|
|
assert context.response.model_used == context.provider.model_id
|
|
assert context.response.error_message in (None, "")
|
|
|
|
|
|
@then("the Anthropic provider response should contain no generated changes")
|
|
def step_assert_no_changes(context):
|
|
assert context.response is not None, "Provider response should exist"
|
|
assert len(context.response.changes) == 0, "Expected no generated changes"
|
|
|
|
|
|
@then(
|
|
'the Anthropic provider response should include {count:d} generated change for "{file_path}"'
|
|
)
|
|
def step_assert_generated_change(context, count, file_path):
|
|
assert context.response is not None, "Provider response should exist"
|
|
assert len(context.response.changes) == count, (
|
|
f"Expected {count} changes, got {len(context.response.changes)}"
|
|
)
|
|
assert any(
|
|
getattr(change, "file_path", None) == file_path
|
|
for change in context.response.changes
|
|
), f"Expected change for {file_path}"
|
|
|
|
|
|
@then('the Anthropic provider response should report error "{message}"')
|
|
def step_assert_response_error(context, message):
|
|
assert context.response is not None
|
|
assert context.response.error_message == message
|
|
|
|
|
|
@then('the Anthropic provider streaming events should include nodes "{node_list}"')
|
|
def step_assert_stream_events(context, node_list):
|
|
expected = [node.strip() for node in node_list.split(",") if node.strip()]
|
|
actual = [
|
|
next(iter(event.keys()))
|
|
for event in getattr(context, "streamed_events", [])
|
|
if "__end__" not in event
|
|
]
|
|
assert actual == expected
|
|
|
|
|
|
@then(
|
|
"the Anthropic provider streaming result should finish with a response containing {count:d} generated change"
|
|
)
|
|
def step_assert_stream_final_response(context, count):
|
|
events = getattr(context, "streamed_events", [])
|
|
assert events, "Expected streamed events"
|
|
final_event = events[-1]
|
|
assert "__end__" in final_event, "Expected final __end__ event"
|
|
response = final_event["__end__"].get("response")
|
|
assert response is not None, "Expected ProviderResponse in __end__ event"
|
|
assert len(response.changes) == count
|
|
|
|
|
|
@then('the Anthropic provider creation should fail with error "{message}"')
|
|
def step_assert_anthropic_provider_error(context, message):
|
|
error = getattr(context, "anthropic_provider_error", None)
|
|
assert error is not None, "Expected the provider to raise an error"
|
|
assert isinstance(error, ValueError)
|
|
assert str(error) == message
|
|
|
|
|
|
@then("the Anthropic provider should support streaming responses")
|
|
def step_assert_anthropic_supports_streaming(context):
|
|
provider = getattr(context, "provider", None)
|
|
assert provider is not None, "Provider should exist"
|
|
assert provider._supports_streaming is True, (
|
|
"Anthropic provider should support streaming"
|
|
)
|