Files
temp/features/steps/a2a_cli_facade_integration_steps.py
freemo 9c6d69153e fix(a2a): rename A2aRequest/A2aResponse fields to comply with JSON-RPC 2.0 wire format
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
2026-04-03 00:29:46 +00:00

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}"
)