forked from HAL9000/cleveragents-core
9c6d69153e
Rewrites the A2aRequest and A2aResponse Pydantic models to use the field names mandated by the JSON-RPC 2.0 specification, fixing a fundamental protocol compliance issue that prevented external A2A-compliant clients from communicating with the server. Changes: - A2aRequest: a2a_version→jsonrpc (fixed '2.0'), request_id→id, operation→method; auth field removed (not in JSON-RPC 2.0) - A2aResponse: a2a_version→jsonrpc, request_id→id, status+data→result (success path), timing_ms removed; added _result_xor_error validator enforcing mutual exclusion of result and error fields - A2aLocalFacade.dispatch(): updated to use request.method, request.id, result=data, error=A2aErrorDetail(...) - A2aHttpTransport.send(): updated to use request.method - CLI call sites (session.py, plan.py): updated A2aRequest(method=...) and response.result / response.error field access - All existing A2A Behave step files updated to new field names - New 35-scenario Behave feature (a2a_jsonrpc_wire_format.feature) covering serialisation, deserialisation, validation, and facade dispatch - New 7-test Robot Framework suite (a2a_jsonrpc_wire_format.robot) for end-to-end wire format verification ISSUES CLOSED: #1501
152 lines
5.4 KiB
Python
152 lines
5.4 KiB
Python
"""Step definitions for a2a_cli_facade_integration.feature.
|
|
|
|
Tests the integration between CLI commands and the A2A local facade,
|
|
verifying that the facade bootstrap, dispatch helpers, and
|
|
notification wrappers work correctly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
|
|
try:
|
|
from cleveragents.a2a.cli_bootstrap import get_facade, reset_facade
|
|
from cleveragents.a2a.facade import A2aLocalFacade
|
|
from cleveragents.a2a.models import A2aRequest
|
|
except ImportError:
|
|
pass # a2a module not available
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a facade created via the CLI bootstrap")
|
|
def step_create_bootstrap_facade(context: Any) -> None:
|
|
reset_facade()
|
|
context.facade = get_facade()
|
|
|
|
|
|
@given("a facade with a mock session service")
|
|
def step_facade_with_mock_session(context: Any) -> None:
|
|
mock_session_svc = MagicMock()
|
|
mock_session_svc.create.return_value = MagicMock(
|
|
session_id="test-session-001",
|
|
actor_name="stub",
|
|
)
|
|
context.facade = A2aLocalFacade(services={"session_service": mock_session_svc})
|
|
|
|
|
|
@given("a facade with a mock plan lifecycle service")
|
|
def step_facade_with_mock_plan(context: Any) -> None:
|
|
mock_plan_svc = MagicMock()
|
|
mock_plan_svc.get_plan.return_value = MagicMock(
|
|
identity=MagicMock(plan_id="test-plan-001"),
|
|
phase=MagicMock(value="execute"),
|
|
state=MagicMock(value="complete"),
|
|
)
|
|
context.facade = A2aLocalFacade(services={"plan_lifecycle_service": mock_plan_svc})
|
|
|
|
|
|
@given("a facade that raises an error on dispatch")
|
|
def step_facade_raises_error(context: Any) -> None:
|
|
context.error_facade = MagicMock(spec=A2aLocalFacade)
|
|
context.error_facade.dispatch.side_effect = RuntimeError("test error")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I dispatch a session.create operation via the CLI facade helper")
|
|
def step_dispatch_session_create(context: Any) -> None:
|
|
request = A2aRequest(method="session.create", params={"actor": "stub"})
|
|
context.response = context.facade.dispatch(request)
|
|
|
|
|
|
@when("I dispatch a plan.create operation via the CLI facade helper")
|
|
def step_dispatch_plan_create(context: Any) -> None:
|
|
request = A2aRequest(method="plan.create", params={"action_name": "test"})
|
|
context.response = context.facade.dispatch(request)
|
|
|
|
|
|
@when("I dispatch a plan.execute operation via the CLI facade helper")
|
|
def step_dispatch_plan_execute(context: Any) -> None:
|
|
request = A2aRequest(method="plan.execute", params={"plan_id": "test-plan-001"})
|
|
context.response = context.facade.dispatch(request)
|
|
|
|
|
|
@when("I dispatch a plan.apply operation via the CLI facade helper")
|
|
def step_dispatch_plan_apply(context: Any) -> None:
|
|
request = A2aRequest(method="plan.apply", params={"plan_id": "test-plan-001"})
|
|
context.response = context.facade.dispatch(request)
|
|
|
|
|
|
@when("I dispatch a plan.status operation via the CLI facade helper")
|
|
def step_dispatch_plan_status(context: Any) -> None:
|
|
request = A2aRequest(method="plan.status", params={"plan_id": "test-plan-001"})
|
|
context.response = context.facade.dispatch(request)
|
|
|
|
|
|
@when("I dispatch a plan.diff operation via the CLI facade helper")
|
|
def step_dispatch_plan_diff(context: Any) -> None:
|
|
request = A2aRequest(method="plan.diff", params={"plan_id": "test-plan-001"})
|
|
context.response = context.facade.dispatch(request)
|
|
|
|
|
|
@when('I call the plan notify_facade helper with operation "{operation}"')
|
|
def step_call_notify_facade(context: Any, operation: str) -> None:
|
|
# Patch the bootstrap module so _notify_facade gets the error facade
|
|
with patch(
|
|
"cleveragents.a2a.cli_bootstrap.get_facade",
|
|
return_value=context.error_facade,
|
|
):
|
|
from cleveragents.a2a.cli_bootstrap import reset_facade
|
|
|
|
reset_facade()
|
|
from cleveragents.cli.commands.plan import _notify_facade
|
|
|
|
# This should not raise even though the facade errors
|
|
context.notify_exception = None
|
|
try:
|
|
_notify_facade(operation, {"plan_id": "test"})
|
|
except Exception as e:
|
|
context.notify_exception = e
|
|
reset_facade()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the facade should support all 42 operations")
|
|
def step_check_42_operations(context: Any) -> None:
|
|
ops = context.facade.list_operations()
|
|
assert len(ops) == 42, f"Expected 42 operations, got {len(ops)}: {ops}"
|
|
|
|
|
|
@then("the facade should be cached on subsequent calls")
|
|
def step_check_facade_cached(context: Any) -> None:
|
|
facade2 = get_facade()
|
|
assert facade2 is context.facade, "Facade should be the same cached instance"
|
|
reset_facade()
|
|
|
|
|
|
@then("the facade should return an ok response")
|
|
def step_check_ok_response(context: Any) -> None:
|
|
assert context.response.error is None, (
|
|
f"Expected no error, got error: {context.response.error}"
|
|
)
|
|
|
|
|
|
@then("no exception should propagate to the caller")
|
|
def step_check_no_exception(context: Any) -> None:
|
|
assert context.notify_exception is None, (
|
|
f"Expected no exception, got: {context.notify_exception}"
|
|
)
|