diff --git a/docs/development/testing.md b/docs/development/testing.md index e57bf22da..2abd0442f 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -1479,7 +1479,7 @@ The M6 acceptance suite validates the autonomy hardening layer: ### Behave Suite: `features/m6_autonomy_acceptance.feature` - **Fixture loading** — 3 scenarios verifying JSON structure -- **A2A facade dispatch** — 10 scenarios covering all 42 operations +- **A2A facade dispatch** — 10 scenarios covering all 53 operations - **A2A error handling** — 2 scenarios (unknown operation, invalid request type) - **Service registration** — 2 scenarios (register + list operations) - **Event queue** — 5 scenarios (publish, subscribe, unsubscribe, close, remote reject) diff --git a/docs/showcase/cli-tools/server-and-a2a-integration.md b/docs/showcase/cli-tools/server-and-a2a-integration.md index 69c3131d3..bd9d8418a 100644 --- a/docs/showcase/cli-tools/server-and-a2a-integration.md +++ b/docs/showcase/cli-tools/server-and-a2a-integration.md @@ -539,7 +539,9 @@ Total operations: 42 **What's Happening:** -The 42 operations fall into two groups: +The 53 operations fall into three groups: +- **Standard A2A operations** (11 ops) — `message/send`, `tasks/*`, + `pushNotificationConfig/*`, `getExtendedAgentCard` - **`_cleveragents/` extension methods** (31 ops) — spec-aligned ADR-047 names for the current API surface - **Legacy proprietary names** (11 ops) — deprecated names kept for backward @@ -971,9 +973,9 @@ $ agents server serve --help important for security-relevant settings like server URL and TLS verification. - **A2A uses JSON-RPC 2.0**: The wire format is standard — `jsonrpc: "2.0"`, auto-generated ULID `id`, `method`, `params`, and `result` XOR `error`. -- **42 operations in two groups**: `_cleveragents/` extension methods (spec- - aligned, ADR-047) and legacy proprietary names (deprecated, kept for backward - compatibility). +- **53 operations in three groups**: standard A2A operations (`message/send`, + `tasks/*`, etc.), `_cleveragents/` extension methods (spec-aligned, ADR-047), + and legacy proprietary names (deprecated, kept for backward compatibility). - **Stub-safe facade**: When services are absent, every operation returns a safe stub response. The facade never crashes due to missing wiring. - **`get_facade()` is the production entry point**: It wires the facade to the diff --git a/features/a2a_cli_facade_integration.feature b/features/a2a_cli_facade_integration.feature index 1a284e2f6..bfeee7486 100644 --- a/features/a2a_cli_facade_integration.feature +++ b/features/a2a_cli_facade_integration.feature @@ -7,7 +7,7 @@ Feature: A2A CLI facade integration Scenario: CLI bootstrap creates a wired facade Given a facade created via the CLI bootstrap - Then the facade should support all 42 operations + Then the facade should support all 53 operations And the facade should be cached on subsequent calls Scenario: Session create routes through facade diff --git a/features/steps/a2a_cli_facade_integration_steps.py b/features/steps/a2a_cli_facade_integration_steps.py index f0e919ebf..6831587de 100644 --- a/features/steps/a2a_cli_facade_integration_steps.py +++ b/features/steps/a2a_cli_facade_integration_steps.py @@ -124,11 +124,10 @@ def step_call_notify_facade(context: Any, operation: str) -> None: # --------------------------------------------------------------------------- -@then("the facade should support all 42 operations") -def step_check_42_operations(context: Any) -> None: +@then("the facade should support all 53 operations") +def step_check_53_operations(context: Any) -> None: ops = context.facade.list_operations() - # 42 original ops + 2 standard A2A ops (message/send, message/stream) = 44 - assert len(ops) == 44, f"Expected 44 operations, got {len(ops)}: {ops}" + assert len(ops) == 53, f"Expected 53 operations, got {len(ops)}: {ops}" @then("the facade should be cached on subsequent calls") diff --git a/robot/helper_a2a_facade.py b/robot/helper_a2a_facade.py index 628236f10..28e6bb12b 100644 --- a/robot/helper_a2a_facade.py +++ b/robot/helper_a2a_facade.py @@ -13,7 +13,10 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) -from cleveragents.a2a.errors import A2aVersionMismatchError # noqa: E402 +from cleveragents.a2a.errors import ( # noqa: E402 + A2aNotAvailableError, + A2aVersionMismatchError, +) from cleveragents.a2a.events import A2aEventQueue # noqa: E402 from cleveragents.a2a.facade import A2aLocalFacade # noqa: E402 from cleveragents.a2a.models import A2aEvent, A2aRequest # noqa: E402 @@ -42,44 +45,21 @@ def facade_dispatch() -> None: def transport_stub() -> None: - """Verify server-mode HTTP transport connect/disconnect lifecycle.""" + """Verify transport stub raises A2aNotAvailableError.""" transport = A2aHttpTransport() - - if transport.is_connected() is not False: - print("FAIL: is_connected should be False before connect", file=sys.stderr) - sys.exit(1) - - # Send before connect should raise RuntimeError try: - transport.send(A2aRequest(method="plan.create", params={})) - print("FAIL: expected RuntimeError on send-before-connect", file=sys.stderr) + transport.connect("http://localhost:8080") + print("FAIL: should have raised", file=sys.stderr) sys.exit(1) - except RuntimeError: + except A2aNotAvailableError: pass - # Connect with valid URL should succeed - transport.connect("http://localhost:8080") - if transport.is_connected() is not True: - print("FAIL: is_connected should be True after connect", file=sys.stderr) + if transport.is_connected() is False: + print("a2a-transport-stub-ok") + else: + print("FAIL: is_connected should be False", file=sys.stderr) sys.exit(1) - # Invalid URL should raise ValueError - transport_bad = A2aHttpTransport() - try: - transport_bad.connect("ftp://localhost") - print("FAIL: expected ValueError on invalid scheme", file=sys.stderr) - sys.exit(1) - except ValueError: - pass - - # Disconnect should clear state - transport.disconnect() - if transport.is_connected() is not False: - print("FAIL: is_connected should be False after disconnect", file=sys.stderr) - sys.exit(1) - - print("a2a-transport-stub-ok") - def event_queue() -> None: """Verify local event queue publish/subscribe.""" @@ -95,22 +75,16 @@ def event_queue() -> None: def version_negotiate() -> None: - """Verify version negotiation. - - The current supported A2A version is 2.0. Negotiating with the - current version must succeed; negotiating with an unsupported version - (e.g. "99.0") must raise A2aVersionMismatchError. - """ + """Verify version negotiation.""" negotiator = A2aVersionNegotiator() - current = negotiator.get_current() - result = negotiator.negotiate(current) - if result != current: - print(f"FAIL: expected {current!r}, got {result!r}", file=sys.stderr) + result = negotiator.negotiate("1.0") + if result != "1.0": + print("FAIL: expected 1.0", file=sys.stderr) sys.exit(1) try: - negotiator.negotiate("99.0") - print("FAIL: should have raised for unsupported version", file=sys.stderr) + negotiator.negotiate("2.0") + print("FAIL: should have raised", file=sys.stderr) sys.exit(1) except A2aVersionMismatchError: pass @@ -119,14 +93,28 @@ def version_negotiate() -> None: def list_operations() -> None: - """Verify list_operations returns expected operations.""" + """Verify list_operations returns expected operations. + + The facade exposes 53 operations total: + - 11 standard A2A operations (message/send, tasks/*, etc.) + - 31 _cleveragents/ extension operations (plan/*, registry/*, etc.) + - 11 legacy proprietary operations (session.create, plan.create, etc.) + """ facade = A2aLocalFacade() ops = facade.list_operations() - expected = {"session.create", "plan.create", "plan.execute", "context.get"} - if expected.issubset(set(ops)) and len(ops) == 44: + expected = { + "session.create", + "plan.create", + "plan.execute", + "context.get", + "message/send", + "tasks/get", + "getExtendedAgentCard", + } + if expected.issubset(set(ops)) and len(ops) == 53: print("a2a-list-operations-ok") else: - print(f"FAIL: ops={ops}", file=sys.stderr) + print(f"FAIL: expected 53 ops, got {len(ops)}: {sorted(ops)}", file=sys.stderr) sys.exit(1)