forked from HAL9000/cleveragents-core
31472b5413
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate. ISSUES CLOSED: #1232
276 lines
9.4 KiB
Python
276 lines
9.4 KiB
Python
"""Step definitions for a2a_events_coverage_r3.feature.
|
|
|
|
Exercises previously-uncovered lines in src/cleveragents/a2a/events.py:
|
|
63, 73-76, 82, 91, 100, 126, 234-238, 244, 250, 258
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when
|
|
|
|
try:
|
|
from cleveragents.a2a.events import (
|
|
TASK_STATUS_UPDATE,
|
|
A2aEventQueue,
|
|
EventBusBridge,
|
|
)
|
|
from cleveragents.a2a.models import A2aEvent
|
|
except ImportError:
|
|
pass # module may be unavailable in isolated environments
|
|
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Givens
|
|
# -----------------------------------------------------------------------
|
|
|
|
|
|
@given("aecov3 a fresh A2aEventQueue")
|
|
def step_aecov3_fresh_queue(context: Any) -> None:
|
|
context.aecov3_queue = A2aEventQueue()
|
|
context.aecov3_error = None
|
|
|
|
|
|
@given("aecov3 a subscriber callback that raises RuntimeError")
|
|
def step_aecov3_failing_subscriber(context: Any) -> None:
|
|
def _boom(event: Any) -> None:
|
|
raise RuntimeError("subscriber exploded")
|
|
|
|
context.aecov3_sub_id = context.aecov3_queue.subscribe_local(_boom)
|
|
|
|
|
|
@given("aecov3 a mock EventBus that returns a disposable subscription")
|
|
def step_aecov3_mock_bus_disposable(context: Any) -> None:
|
|
context.aecov3_bus = MagicMock()
|
|
context.aecov3_subscription = MagicMock()
|
|
# The subscription has a .dispose() method
|
|
context.aecov3_subscription.dispose = MagicMock()
|
|
context.aecov3_bus_callback = None
|
|
|
|
def _fake_subscribe(callback: Any) -> MagicMock:
|
|
context.aecov3_bus_callback = callback
|
|
return context.aecov3_subscription
|
|
|
|
context.aecov3_bus.subscribe.side_effect = _fake_subscribe
|
|
|
|
|
|
@given("aecov3 a mock EventBus that returns a non-disposable subscription")
|
|
def step_aecov3_mock_bus_non_disposable(context: Any) -> None:
|
|
context.aecov3_bus = MagicMock(spec=[])
|
|
context.aecov3_bus.subscribe = MagicMock()
|
|
context.aecov3_subscription = "simple-token" # no .dispose attribute
|
|
context.aecov3_bus_callback = None
|
|
|
|
def _fake_subscribe(callback: Any) -> str:
|
|
context.aecov3_bus_callback = callback
|
|
return context.aecov3_subscription
|
|
|
|
context.aecov3_bus.subscribe.side_effect = _fake_subscribe
|
|
|
|
|
|
@given("aecov3 a mock EventBus that captures the callback")
|
|
def step_aecov3_mock_bus_capture(context: Any) -> None:
|
|
context.aecov3_bus = MagicMock()
|
|
context.aecov3_bus_callback = None
|
|
|
|
def _fake_subscribe(callback: Any) -> MagicMock:
|
|
context.aecov3_bus_callback = callback
|
|
return MagicMock()
|
|
|
|
context.aecov3_bus.subscribe.side_effect = _fake_subscribe
|
|
|
|
|
|
@given("aecov3 an EventBusBridge connecting the bus and queue")
|
|
def step_aecov3_bridge(context: Any) -> None:
|
|
context.aecov3_bridge = EventBusBridge(context.aecov3_bus, context.aecov3_queue)
|
|
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Whens
|
|
# -----------------------------------------------------------------------
|
|
|
|
|
|
@when("aecov3 I publish a plain dict instead of an A2aEvent")
|
|
def step_aecov3_publish_dict(context: Any) -> None:
|
|
try:
|
|
context.aecov3_queue.publish({"not": "an event"}) # type: ignore[arg-type]
|
|
except Exception as exc:
|
|
context.aecov3_error = exc
|
|
|
|
|
|
@when("aecov3 I publish a valid A2aEvent")
|
|
def step_aecov3_publish_valid(context: Any) -> None:
|
|
context.aecov3_event = A2aEvent(
|
|
event_type="TestEvent",
|
|
plan_id="plan-test",
|
|
data={"key": "value"},
|
|
)
|
|
try:
|
|
context.aecov3_queue.publish(context.aecov3_event)
|
|
except Exception as exc:
|
|
context.aecov3_error = exc
|
|
|
|
|
|
@when("aecov3 I subscribe with a non-callable value")
|
|
def step_aecov3_subscribe_non_callable(context: Any) -> None:
|
|
try:
|
|
context.aecov3_queue.subscribe_local("not-a-callable") # type: ignore[arg-type]
|
|
except Exception as exc:
|
|
context.aecov3_error = exc
|
|
|
|
|
|
@when("aecov3 I unsubscribe with an empty string")
|
|
def step_aecov3_unsubscribe_empty(context: Any) -> None:
|
|
try:
|
|
context.aecov3_queue.unsubscribe("")
|
|
except Exception as exc:
|
|
context.aecov3_error = exc
|
|
|
|
|
|
@when("aecov3 I unsubscribe with a non-string value")
|
|
def step_aecov3_unsubscribe_non_string(context: Any) -> None:
|
|
try:
|
|
context.aecov3_queue.unsubscribe(12345) # type: ignore[arg-type]
|
|
except Exception as exc:
|
|
context.aecov3_error = exc
|
|
|
|
|
|
@when("aecov3 I call get_events with limit 0")
|
|
def step_aecov3_get_events_zero(context: Any) -> None:
|
|
try:
|
|
context.aecov3_queue.get_events(limit=0)
|
|
except Exception as exc:
|
|
context.aecov3_error = exc
|
|
|
|
|
|
@when("aecov3 I call get_events with a string limit")
|
|
def step_aecov3_get_events_string(context: Any) -> None:
|
|
try:
|
|
context.aecov3_queue.get_events(limit="ten") # type: ignore[arg-type]
|
|
except Exception as exc:
|
|
context.aecov3_error = exc
|
|
|
|
|
|
@when("aecov3 I call subscribe_remote with an empty endpoint")
|
|
def step_aecov3_remote_empty(context: Any) -> None:
|
|
try:
|
|
context.aecov3_queue.subscribe_remote("")
|
|
except Exception as exc:
|
|
context.aecov3_error = exc
|
|
|
|
|
|
@when("aecov3 I call subscribe_remote with a non-string endpoint")
|
|
def step_aecov3_remote_non_string(context: Any) -> None:
|
|
try:
|
|
context.aecov3_queue.subscribe_remote(42) # type: ignore[arg-type]
|
|
except Exception as exc:
|
|
context.aecov3_error = exc
|
|
|
|
|
|
@when("aecov3 the bridge is started")
|
|
def step_aecov3_bridge_start(context: Any) -> None:
|
|
context.aecov3_bridge.start()
|
|
|
|
|
|
@when("aecov3 the bridge is stopped")
|
|
def step_aecov3_bridge_stop(context: Any) -> None:
|
|
context.aecov3_bridge.stop()
|
|
|
|
|
|
@when("aecov3 the bridge receives a domain event without event_type")
|
|
def step_aecov3_domain_no_type(context: Any) -> None:
|
|
domain_event = MagicMock(spec=[])
|
|
# No event_type attribute at all → getattr returns None → early return
|
|
context.aecov3_bus_callback(domain_event)
|
|
|
|
|
|
@when('aecov3 the bridge receives a domain event with string event_type "{type_str}"')
|
|
def step_aecov3_domain_string_type(context: Any, type_str: str) -> None:
|
|
domain_event = MagicMock()
|
|
# Use a plain string (no .value) so the `else str(event_type_name)` branch runs
|
|
domain_event.event_type = type_str
|
|
# Remove .value so hasattr(event_type_name, "value") is False on the str
|
|
# Python str does not have a `.value` attribute, so this exercises line 250
|
|
domain_event.plan_id = "plan-bridge-test"
|
|
domain_event.details = {"info": "testing"}
|
|
context.aecov3_bus_callback(domain_event)
|
|
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Thens
|
|
# -----------------------------------------------------------------------
|
|
|
|
|
|
@then('aecov3 a TypeError should be stored with message "{msg}"')
|
|
def step_aecov3_check_type_error(context: Any, msg: str) -> None:
|
|
assert context.aecov3_error is not None, "Expected an error but none was stored"
|
|
assert isinstance(context.aecov3_error, TypeError), (
|
|
f"Expected TypeError, got {type(context.aecov3_error).__name__}"
|
|
)
|
|
assert msg in str(context.aecov3_error), (
|
|
f"Expected message containing '{msg}', got: {context.aecov3_error}"
|
|
)
|
|
|
|
|
|
@then('aecov3 a ValueError should be stored with message "{msg}"')
|
|
def step_aecov3_check_value_error(context: Any, msg: str) -> None:
|
|
assert context.aecov3_error is not None, "Expected an error but none was stored"
|
|
assert isinstance(context.aecov3_error, ValueError), (
|
|
f"Expected ValueError, got {type(context.aecov3_error).__name__}"
|
|
)
|
|
assert msg in str(context.aecov3_error), (
|
|
f"Expected message containing '{msg}', got: {context.aecov3_error}"
|
|
)
|
|
|
|
|
|
@then("aecov3 no error should be stored")
|
|
def step_aecov3_no_error(context: Any) -> None:
|
|
assert context.aecov3_error is None, (
|
|
f"Expected no error, got: {context.aecov3_error}"
|
|
)
|
|
|
|
|
|
@then("aecov3 the event should be in the queue")
|
|
def step_aecov3_event_in_queue(context: Any) -> None:
|
|
events = context.aecov3_queue.get_events()
|
|
ids = [e.event_id for e in events]
|
|
assert context.aecov3_event.event_id in ids, (
|
|
f"Event {context.aecov3_event.event_id} not found in queue: {ids}"
|
|
)
|
|
|
|
|
|
@then("aecov3 the subscription dispose should have been called")
|
|
def step_aecov3_dispose_called(context: Any) -> None:
|
|
context.aecov3_subscription.dispose.assert_called_once()
|
|
|
|
|
|
@then("aecov3 the bridge subscription should be None")
|
|
def step_aecov3_subscription_none(context: Any) -> None:
|
|
assert context.aecov3_bridge._subscription is None, (
|
|
f"Expected None subscription, got: {context.aecov3_bridge._subscription}"
|
|
)
|
|
|
|
|
|
@then("aecov3 the queue should be empty")
|
|
def step_aecov3_queue_empty(context: Any) -> None:
|
|
events = context.aecov3_queue.get_events()
|
|
assert len(events) == 0, f"Expected empty queue, got {len(events)} events"
|
|
|
|
|
|
@then("aecov3 the queue should contain a TaskStatusUpdateEvent")
|
|
def step_aecov3_queue_has_status(context: Any) -> None:
|
|
events = context.aecov3_queue.get_events()
|
|
types = [e.event_type for e in events]
|
|
assert TASK_STATUS_UPDATE in types, (
|
|
f"Expected {TASK_STATUS_UPDATE} in queue, got: {types}"
|
|
)
|
|
|
|
|
|
@then('aecov3 the queue should contain an event with type "{expected_type}"')
|
|
def step_aecov3_queue_has_type(context: Any, expected_type: str) -> None:
|
|
events = context.aecov3_queue.get_events()
|
|
types = [e.event_type for e in events]
|
|
assert expected_type in types, f"Expected '{expected_type}' in queue, got: {types}"
|