refactor(a2a): rename ACP module and symbols to A2A standard #705
@@ -1,9 +1,9 @@
|
||||
"""ASV benchmarks for ACP facade dispatch, version negotiation, and event queue.
|
||||
"""ASV benchmarks for A2A facade dispatch, version negotiation, and event queue.
|
||||
|
||||
Measures the performance of:
|
||||
- AcpLocalFacade dispatch overhead per operation
|
||||
- AcpVersionNegotiator negotiation throughput
|
||||
- AcpEventQueue publish and get_events throughput
|
||||
- A2aLocalFacade dispatch overhead per operation
|
||||
- A2aVersionNegotiator negotiation throughput
|
||||
- A2aEventQueue publish and get_events throughput
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -23,10 +23,10 @@ import cleveragents # noqa: E402
|
||||
|
||||
importlib.reload(cleveragents)
|
||||
|
||||
from cleveragents.acp.events import AcpEventQueue # noqa: E402
|
||||
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
|
||||
from cleveragents.acp.models import AcpEvent, AcpRequest # noqa: E402
|
||||
from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
|
||||
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
|
||||
from cleveragents.a2a.versioning import A2aVersionNegotiator # noqa: E402
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Facade dispatch benchmarks
|
||||
@@ -34,17 +34,17 @@ from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
|
||||
|
||||
|
||||
class FacadeDispatchSuite:
|
||||
"""Benchmark AcpLocalFacade.dispatch() overhead."""
|
||||
"""Benchmark A2aLocalFacade.dispatch() overhead."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def setup(self) -> None:
|
||||
self.facade = AcpLocalFacade()
|
||||
self.session_req = AcpRequest(operation="session.create")
|
||||
self.plan_req = AcpRequest(
|
||||
self.facade = A2aLocalFacade()
|
||||
self.session_req = A2aRequest(operation="session.create")
|
||||
self.plan_req = A2aRequest(
|
||||
operation="plan.execute", params={"plan_id": "BENCH001"}
|
||||
)
|
||||
self.context_req = AcpRequest(operation="context.get")
|
||||
self.context_req = A2aRequest(operation="context.get")
|
||||
|
||||
def time_dispatch_session_create(self) -> None:
|
||||
self.facade.dispatch(self.session_req)
|
||||
@@ -57,7 +57,7 @@ class FacadeDispatchSuite:
|
||||
|
||||
def time_dispatch_all_operations(self) -> None:
|
||||
for op in self.facade.list_operations():
|
||||
req = AcpRequest(operation=op)
|
||||
req = A2aRequest(operation=op)
|
||||
self.facade.dispatch(req)
|
||||
|
||||
def time_list_operations(self) -> None:
|
||||
@@ -70,12 +70,12 @@ class FacadeDispatchSuite:
|
||||
|
||||
|
||||
class VersionNegotiationSuite:
|
||||
"""Benchmark AcpVersionNegotiator throughput."""
|
||||
"""Benchmark A2aVersionNegotiator throughput."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def setup(self) -> None:
|
||||
self.negotiator = AcpVersionNegotiator()
|
||||
self.negotiator = A2aVersionNegotiator()
|
||||
|
||||
def time_negotiate_supported(self) -> None:
|
||||
self.negotiator.negotiate("1.0")
|
||||
@@ -96,13 +96,13 @@ class VersionNegotiationSuite:
|
||||
|
||||
|
||||
class EventQueueSuite:
|
||||
"""Benchmark AcpEventQueue throughput."""
|
||||
"""Benchmark A2aEventQueue throughput."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def setup(self) -> None:
|
||||
self.queue = AcpEventQueue()
|
||||
self.event = AcpEvent(event_type="bench.event")
|
||||
self.queue = A2aEventQueue()
|
||||
self.event = A2aEvent(event_type="bench.event")
|
||||
|
||||
def time_publish_single(self) -> None:
|
||||
self.queue.publish(self.event)
|
||||
@@ -118,7 +118,7 @@ class EventQueueSuite:
|
||||
self.queue.subscribe_local(lambda e: None)
|
||||
|
||||
def time_publish_with_subscriber(self) -> None:
|
||||
q = AcpEventQueue()
|
||||
q = A2aEventQueue()
|
||||
q.subscribe_local(lambda e: None)
|
||||
for _ in range(100):
|
||||
q.publish(self.event)
|
||||
@@ -1,10 +1,10 @@
|
||||
"""ASV benchmarks for M6 autonomy acceptance suite runtime.
|
||||
|
||||
Measures the performance of:
|
||||
- ACP local facade dispatch operations
|
||||
- A2A local facade dispatch operations
|
||||
- Automation guard evaluation
|
||||
- AutomationProfileService resolution precedence
|
||||
- ACP event queue publish/subscribe
|
||||
- A2A event queue publish/subscribe
|
||||
- Fixture loading overhead
|
||||
"""
|
||||
|
||||
@@ -25,10 +25,10 @@ import cleveragents # noqa: E402
|
||||
|
||||
importlib.reload(cleveragents)
|
||||
|
||||
from cleveragents.acp.events import AcpEventQueue # noqa: E402
|
||||
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
|
||||
from cleveragents.acp.models import AcpEvent, AcpRequest # noqa: E402
|
||||
from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
|
||||
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
|
||||
from cleveragents.a2a.versioning import A2aVersionNegotiator # noqa: E402
|
||||
from cleveragents.application.services.automation_profile_service import ( # noqa: E402
|
||||
AutomationProfileService,
|
||||
)
|
||||
@@ -43,23 +43,23 @@ _FIXTURES_DIR = Path(__file__).resolve().parents[1] / "features" / "fixtures" /
|
||||
|
||||
|
||||
class M6FacadeDispatchSuite:
|
||||
"""Benchmark ACP local facade dispatch operations."""
|
||||
"""Benchmark A2A local facade dispatch operations."""
|
||||
|
||||
def setup(self) -> None:
|
||||
self._facade = AcpLocalFacade()
|
||||
self._facade = A2aLocalFacade()
|
||||
|
||||
def time_session_create(self) -> None:
|
||||
"""Benchmark session.create dispatch."""
|
||||
self._facade.dispatch(AcpRequest(operation="session.create", params={}))
|
||||
self._facade.dispatch(A2aRequest(operation="session.create", params={}))
|
||||
|
||||
def time_plan_create(self) -> None:
|
||||
"""Benchmark plan.create dispatch."""
|
||||
self._facade.dispatch(AcpRequest(operation="plan.create", params={}))
|
||||
self._facade.dispatch(A2aRequest(operation="plan.create", params={}))
|
||||
|
||||
def time_plan_execute(self) -> None:
|
||||
"""Benchmark plan.execute dispatch."""
|
||||
self._facade.dispatch(
|
||||
AcpRequest(
|
||||
A2aRequest(
|
||||
operation="plan.execute",
|
||||
params={"plan_id": "01M6SM0KE00000000000000001"},
|
||||
)
|
||||
@@ -170,16 +170,16 @@ class M6ProfileResolutionSuite:
|
||||
)
|
||||
|
||||
def time_version_negotiation(self) -> None:
|
||||
"""Benchmark ACP version negotiation."""
|
||||
negotiator = AcpVersionNegotiator()
|
||||
"""Benchmark A2A version negotiation."""
|
||||
negotiator = A2aVersionNegotiator()
|
||||
negotiator.negotiate("1.0")
|
||||
|
||||
|
||||
class M6EventQueueSuite:
|
||||
"""Benchmark ACP event queue operations."""
|
||||
"""Benchmark A2A event queue operations."""
|
||||
|
||||
def setup(self) -> None:
|
||||
self._queue = AcpEventQueue()
|
||||
self._queue = A2aEventQueue()
|
||||
|
||||
def teardown(self) -> None:
|
||||
if not self._queue.is_closed:
|
||||
@@ -187,13 +187,13 @@ class M6EventQueueSuite:
|
||||
|
||||
def time_publish_event(self) -> None:
|
||||
"""Benchmark publishing a single event."""
|
||||
self._queue.publish(AcpEvent(event_type="plan.progress", data={"step": 1}))
|
||||
self._queue.publish(A2aEvent(event_type="plan.progress", data={"step": 1}))
|
||||
|
||||
def time_subscribe_and_publish(self) -> None:
|
||||
"""Benchmark subscribe + publish cycle."""
|
||||
q = AcpEventQueue()
|
||||
q = A2aEventQueue()
|
||||
sub_id = q.subscribe_local(lambda _e: None)
|
||||
q.publish(AcpEvent(event_type="test.event", data={}))
|
||||
q.publish(A2aEvent(event_type="test.event", data={}))
|
||||
q.unsubscribe(sub_id)
|
||||
q.close()
|
||||
|
||||
@@ -205,9 +205,9 @@ class M6EventQueueSuite:
|
||||
class M6FixtureLoadSuite:
|
||||
"""Benchmark loading M6 fixture files."""
|
||||
|
||||
def time_load_acp_facade_flows(self) -> None:
|
||||
"""Benchmark loading acp_facade_flows.json."""
|
||||
with open(_FIXTURES_DIR / "acp_facade_flows.json") as f:
|
||||
def time_load_a2a_facade_flows(self) -> None:
|
||||
"""Benchmark loading a2a_facade_flows.json."""
|
||||
with open(_FIXTURES_DIR / "a2a_facade_flows.json") as f:
|
||||
json.load(f)
|
||||
|
||||
def time_load_autonomy_guardrails(self) -> None:
|
||||
@@ -223,7 +223,7 @@ class M6FixtureLoadSuite:
|
||||
def time_load_all_fixtures(self) -> None:
|
||||
"""Benchmark loading all M6 fixture files."""
|
||||
for fname in (
|
||||
"acp_facade_flows.json",
|
||||
"a2a_facade_flows.json",
|
||||
"autonomy_guardrails.json",
|
||||
"automation_profiles.json",
|
||||
):
|
||||
|
||||
@@ -23,12 +23,12 @@ import cleveragents # noqa: E402
|
||||
|
||||
importlib.reload(cleveragents)
|
||||
|
||||
from cleveragents.acp.clients import ( # noqa: E402
|
||||
from cleveragents.a2a.clients import ( # noqa: E402
|
||||
StubAuthClient,
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig # noqa: E402
|
||||
from cleveragents.a2a.server_config import ServerConnectionConfig # noqa: E402
|
||||
from cleveragents.application.services.config_service import _REGISTRY # noqa: E402
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
@phase2 @acp @coverage-boost
|
||||
Feature: ACP clients coverage boost
|
||||
@phase2 @a2a @coverage-boost
|
||||
Feature: A2A clients coverage boost
|
||||
As a developer
|
||||
I want complete coverage of Protocol default bodies and stub validation branches
|
||||
So that all code paths in cleveragents.acp.clients are exercised
|
||||
So that all code paths in cleveragents.a2a.clients are exercised
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Protocol default method bodies (the `...` expressions)
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
Feature: ACP local facade coverage boost — uncovered validation paths
|
||||
As a developer maintaining the ACP local facade
|
||||
Feature: A2A local facade coverage boost — uncovered validation paths
|
||||
As a developer maintaining the A2A local facade
|
||||
I want all error-handling and validation paths exercised
|
||||
So that edge-case regressions are caught early
|
||||
|
||||
@@ -7,8 +7,8 @@ Feature: ACP local facade coverage boost — uncovered validation paths
|
||||
# Constructor validation (line 82)
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
Scenario: Constructing AcpLocalFacade with a non-dict raises TypeError
|
||||
When I try to create an AcpLocalFacade with a non-dict services argument
|
||||
Scenario: Constructing A2aLocalFacade with a non-dict raises TypeError
|
||||
When I try to create an A2aLocalFacade with a non-dict services argument
|
||||
Then a TypeError should be raised with message "services must be a dict or None"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
@@ -1,5 +1,5 @@
|
||||
Feature: ACP local facade wiring to live services
|
||||
As a client of the ACP local facade
|
||||
Feature: A2A local facade wiring to live services
|
||||
As a client of the A2A local facade
|
||||
I want operations to route to real application services
|
||||
So that the facade delegates to live implementations
|
||||
|
||||
@@ -8,20 +8,20 @@ Feature: ACP local facade wiring to live services
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
Scenario: session.create delegates to SessionService
|
||||
Given a wired AcpLocalFacade with a mock SessionService
|
||||
Given a wired A2aLocalFacade with a mock SessionService
|
||||
When I dispatch wired operation "session.create" with params {"actor_name": "local/test-actor"}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "session_id" equals "MOCK-SESSION-001"
|
||||
And wired response data key "status" equals "created"
|
||||
|
||||
Scenario: session.close delegates to SessionService
|
||||
Given a wired AcpLocalFacade with a mock SessionService
|
||||
Given a wired A2aLocalFacade with a mock SessionService
|
||||
When I dispatch wired operation "session.close" with params {"session_id": "MOCK-SESSION-001"}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "status" equals "closed"
|
||||
|
||||
Scenario: session.close without session_id returns error
|
||||
Given a wired AcpLocalFacade with a mock SessionService
|
||||
Given a wired A2aLocalFacade with a mock SessionService
|
||||
When I dispatch wired operation "session.close" with params {}
|
||||
Then the wired response status should be "error"
|
||||
|
||||
@@ -30,38 +30,38 @@ Feature: ACP local facade wiring to live services
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
Scenario: plan.create delegates to PlanLifecycleService
|
||||
Given a wired AcpLocalFacade with a mock PlanLifecycleService
|
||||
Given a wired A2aLocalFacade with a mock PlanLifecycleService
|
||||
When I dispatch wired operation "plan.create" with params {"action_name": "local/test-action"}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "plan_id" equals "MOCK-PLAN-001"
|
||||
And wired response data key "status" equals "created"
|
||||
|
||||
Scenario: plan.create without action_name returns error
|
||||
Given a wired AcpLocalFacade with a mock PlanLifecycleService
|
||||
Given a wired A2aLocalFacade with a mock PlanLifecycleService
|
||||
When I dispatch wired operation "plan.create" with params {}
|
||||
Then the wired response status should be "error"
|
||||
|
||||
Scenario: plan.execute delegates to PlanLifecycleService
|
||||
Given a wired AcpLocalFacade with a mock PlanLifecycleService
|
||||
Given a wired A2aLocalFacade with a mock PlanLifecycleService
|
||||
When I dispatch wired operation "plan.execute" with params {"plan_id": "MOCK-PLAN-001"}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "plan_id" equals "MOCK-PLAN-001"
|
||||
|
||||
Scenario: plan.status delegates to PlanLifecycleService
|
||||
Given a wired AcpLocalFacade with a mock PlanLifecycleService
|
||||
Given a wired A2aLocalFacade with a mock PlanLifecycleService
|
||||
When I dispatch wired operation "plan.status" with params {"plan_id": "MOCK-PLAN-001"}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "plan_id" equals "MOCK-PLAN-001"
|
||||
And wired response data key "phase" equals "strategize"
|
||||
|
||||
Scenario: plan.diff delegates to PlanLifecycleService
|
||||
Given a wired AcpLocalFacade with a mock PlanLifecycleService
|
||||
Given a wired A2aLocalFacade with a mock PlanLifecycleService
|
||||
When I dispatch wired operation "plan.diff" with params {"plan_id": "MOCK-PLAN-001"}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "plan_id" equals "MOCK-PLAN-001"
|
||||
|
||||
Scenario: plan.apply delegates to PlanLifecycleService
|
||||
Given a wired AcpLocalFacade with a mock PlanLifecycleService
|
||||
Given a wired A2aLocalFacade with a mock PlanLifecycleService
|
||||
When I dispatch wired operation "plan.apply" with params {"plan_id": "MOCK-PLAN-001"}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "plan_id" equals "MOCK-PLAN-001"
|
||||
@@ -71,13 +71,13 @@ Feature: ACP local facade wiring to live services
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
Scenario: registry.list_tools delegates to ToolRegistry
|
||||
Given a wired AcpLocalFacade with a mock ToolRegistry
|
||||
Given a wired A2aLocalFacade with a mock ToolRegistry
|
||||
When I dispatch wired operation "registry.list_tools" with params {}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data should contain tools list with 2 items
|
||||
|
||||
Scenario: registry.list_resources delegates to ResourceRegistryService
|
||||
Given a wired AcpLocalFacade with a mock ResourceRegistryService
|
||||
Given a wired A2aLocalFacade with a mock ResourceRegistryService
|
||||
When I dispatch wired operation "registry.list_resources" with params {}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data should contain resources list with 1 items
|
||||
@@ -87,7 +87,7 @@ Feature: ACP local facade wiring to live services
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
Scenario: context.get returns stub pending ACMS pipeline
|
||||
Given a wired AcpLocalFacade with no services
|
||||
Given a wired A2aLocalFacade with no services
|
||||
When I dispatch wired operation "context.get" with params {}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "stub" equals "True"
|
||||
@@ -96,8 +96,8 @@ Feature: ACP local facade wiring to live services
|
||||
# Event wiring
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
Scenario: event.subscribe delegates to AcpEventQueue
|
||||
Given a wired AcpLocalFacade with a mock AcpEventQueue
|
||||
Scenario: event.subscribe delegates to A2aEventQueue
|
||||
Given a wired A2aLocalFacade with a mock A2aEventQueue
|
||||
When I dispatch wired operation "event.subscribe" with params {}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "status" equals "subscribed"
|
||||
@@ -108,25 +108,25 @@ Feature: ACP local facade wiring to live services
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
Scenario: NOT_FOUND error code for missing resources
|
||||
Given a wired AcpLocalFacade with a raising SessionService for not-found
|
||||
Given a wired A2aLocalFacade with a raising SessionService for not-found
|
||||
When I dispatch wired operation "session.close" with params {"session_id": "nonexistent"}
|
||||
Then the wired response status should be "error"
|
||||
And wired response error code should be "NOT_FOUND"
|
||||
|
||||
Scenario: VALIDATION_ERROR code for validation failures
|
||||
Given a wired AcpLocalFacade with a raising service for validation-error
|
||||
Given a wired A2aLocalFacade with a raising service for validation-error
|
||||
When I dispatch wired operation "plan.create" with params {"action_name": "bad"}
|
||||
Then the wired response status should be "error"
|
||||
And wired response error code should be "VALIDATION_ERROR"
|
||||
|
||||
Scenario: PLAN_ERROR code for plan failures
|
||||
Given a wired AcpLocalFacade with a raising service for plan-error
|
||||
Given a wired A2aLocalFacade with a raising service for plan-error
|
||||
When I dispatch wired operation "plan.execute" with params {"plan_id": "P1"}
|
||||
Then the wired response status should be "error"
|
||||
And wired response error code should be "PLAN_ERROR"
|
||||
|
||||
Scenario: INVALID_STATE code for business rule violations
|
||||
Given a wired AcpLocalFacade with a raising service for invalid-state
|
||||
Given a wired A2aLocalFacade with a raising service for invalid-state
|
||||
When I dispatch wired operation "plan.apply" with params {"plan_id": "P1"}
|
||||
Then the wired response status should be "error"
|
||||
And wired response error code should be "INVALID_STATE"
|
||||
@@ -136,24 +136,24 @@ Feature: ACP local facade wiring to live services
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
Scenario: session.create stub when no service wired
|
||||
Given a wired AcpLocalFacade with no services
|
||||
Given a wired A2aLocalFacade with no services
|
||||
When I dispatch wired operation "session.create" with params {}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "status" equals "created"
|
||||
|
||||
Scenario: plan.create stub when no service wired
|
||||
Given a wired AcpLocalFacade with no services
|
||||
Given a wired A2aLocalFacade with no services
|
||||
When I dispatch wired operation "plan.create" with params {}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "status" equals "created"
|
||||
|
||||
Scenario: registry.list_tools stub when no service wired
|
||||
Given a wired AcpLocalFacade with no services
|
||||
Given a wired A2aLocalFacade with no services
|
||||
When I dispatch wired operation "registry.list_tools" with params {}
|
||||
Then the wired response status should be "ok"
|
||||
|
||||
Scenario: event.subscribe stub when no service wired
|
||||
Given a wired AcpLocalFacade with no services
|
||||
Given a wired A2aLocalFacade with no services
|
||||
When I dispatch wired operation "event.subscribe" with params {}
|
||||
Then the wired response status should be "ok"
|
||||
And wired response data key "status" equals "subscribed"
|
||||
+197
-197
@@ -1,39 +1,39 @@
|
||||
Feature: Consolidated Misc
|
||||
Combined scenarios from: acp_facade, agents_base_uncovered_lines, bridge_coverage, coverage_improvements, m6_autonomy_acceptance, scale_test, container_coverage_r2, lsp_registry, memory_service_coverage, migration_runner_coverage, system_coverage_boost, yaml_engine_direct_coverage
|
||||
Combined scenarios from: a2a_facade, agents_base_uncovered_lines, bridge_coverage, coverage_improvements, m6_autonomy_acceptance, scale_test, container_coverage_r2, lsp_registry, memory_service_coverage, migration_runner_coverage, system_coverage_boost, yaml_engine_direct_coverage
|
||||
|
||||
# ============================================================
|
||||
# Originally from: acp_facade.feature
|
||||
# Feature: ACP Local Facade and Server Stubs
|
||||
# Originally from: a2a_facade.feature
|
||||
# Feature: A2A Local Facade and Server Stubs
|
||||
# ============================================================
|
||||
|
||||
Scenario: Create facade with no services
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
Then the facade should be created successfully
|
||||
|
||||
|
||||
Scenario: Create facade with services dict
|
||||
Given a new AcpLocalFacade with services {"session": "mock_session"}
|
||||
Given a new A2aLocalFacade with services {"session": "mock_session"}
|
||||
Then the facade should be created successfully
|
||||
|
||||
|
||||
Scenario: Register a service on the facade
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I register a service named "planner" on the facade
|
||||
Then the service should be registered successfully
|
||||
|
||||
|
||||
Scenario: Register service with empty name raises error
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I try to register a service with an empty name
|
||||
Then an ACP ValueError should be raised
|
||||
Then an A2A ValueError should be raised
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpLocalFacade — dispatch for each supported operation
|
||||
# A2aLocalFacade — dispatch for each supported operation
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: Dispatch session.create returns session_id and status
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "session.create" with params {}
|
||||
Then the response status should be "ok"
|
||||
And response data includes key "session_id"
|
||||
@@ -41,14 +41,14 @@ Feature: Consolidated Misc
|
||||
|
||||
|
||||
Scenario: Dispatch session.close returns status closed
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "session.close" with params {}
|
||||
Then the response status should be "ok"
|
||||
And response data key "status" equals "closed"
|
||||
|
||||
|
||||
Scenario: Dispatch plan.create returns plan_id and status
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "plan.create" with params {}
|
||||
Then the response status should be "ok"
|
||||
And response data includes key "plan_id"
|
||||
@@ -56,7 +56,7 @@ Feature: Consolidated Misc
|
||||
|
||||
|
||||
Scenario: Dispatch plan.execute returns plan_id and status queued
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "plan.execute" with params {"plan_id": "PLAN001"}
|
||||
Then the response status should be "ok"
|
||||
And response data key "plan_id" equals "PLAN001"
|
||||
@@ -64,7 +64,7 @@ Feature: Consolidated Misc
|
||||
|
||||
|
||||
Scenario: Dispatch plan.status returns plan_id and phase
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "plan.status" with params {"plan_id": "PLAN001"}
|
||||
Then the response status should be "ok"
|
||||
And response data key "plan_id" equals "PLAN001"
|
||||
@@ -72,7 +72,7 @@ Feature: Consolidated Misc
|
||||
|
||||
|
||||
Scenario: Dispatch plan.diff returns plan_id and empty changes
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "plan.diff" with params {"plan_id": "PLAN001"}
|
||||
Then the response status should be "ok"
|
||||
And response data key "plan_id" equals "PLAN001"
|
||||
@@ -80,7 +80,7 @@ Feature: Consolidated Misc
|
||||
|
||||
|
||||
Scenario: Dispatch plan.apply returns plan_id and status applied
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "plan.apply" with params {"plan_id": "PLAN001"}
|
||||
Then the response status should be "ok"
|
||||
And response data key "plan_id" equals "PLAN001"
|
||||
@@ -88,51 +88,51 @@ Feature: Consolidated Misc
|
||||
|
||||
|
||||
Scenario: Dispatch registry.list_tools returns empty tools
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "registry.list_tools" with params {}
|
||||
Then the response status should be "ok"
|
||||
And response data includes key "tools"
|
||||
|
||||
|
||||
Scenario: Dispatch registry.list_resources returns empty resources
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "registry.list_resources" with params {}
|
||||
Then the response status should be "ok"
|
||||
And response data includes key "resources"
|
||||
|
||||
|
||||
Scenario: Dispatch context.get returns empty context
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "context.get" with params {}
|
||||
Then the response status should be "ok"
|
||||
And response data includes key "context"
|
||||
|
||||
|
||||
Scenario: Dispatch event.subscribe returns subscription_id and status
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch operation "event.subscribe" with params {}
|
||||
Then the response status should be "ok"
|
||||
And response data includes key "subscription_id"
|
||||
And response data key "status" equals "subscribed"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpLocalFacade — unknown operation
|
||||
# A2aLocalFacade — unknown operation
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: Dispatch unknown operation raises AcpOperationNotFoundError
|
||||
Given a new AcpLocalFacade with no services
|
||||
Scenario: Dispatch unknown operation raises A2aOperationNotFoundError
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I dispatch an unknown operation "does.not.exist"
|
||||
Then an AcpOperationNotFoundError should be raised
|
||||
Then an A2aOperationNotFoundError should be raised
|
||||
And the error operation attribute should be "does.not.exist"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpLocalFacade — list_operations
|
||||
# A2aLocalFacade — list_operations
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: list_operations returns all supported operations
|
||||
Given a new AcpLocalFacade with no services
|
||||
Given a new A2aLocalFacade with no services
|
||||
When I call list_operations on the facade
|
||||
Then the operations list should contain "session.create"
|
||||
And the operations list should contain "plan.create"
|
||||
@@ -143,161 +143,161 @@ Feature: Consolidated Misc
|
||||
And the operations list should have 11 items
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpHttpTransport — all stubs raise AcpNotAvailableError
|
||||
# A2aHttpTransport — all stubs raise A2aNotAvailableError
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: Transport send raises AcpNotAvailableError
|
||||
Given a new AcpHttpTransport
|
||||
Scenario: Transport send raises A2aNotAvailableError
|
||||
Given a new A2aHttpTransport
|
||||
When I try to send a request via the transport
|
||||
Then an AcpNotAvailableError should be raised
|
||||
Then an A2aNotAvailableError should be raised
|
||||
|
||||
|
||||
Scenario: Transport connect raises AcpNotAvailableError
|
||||
Given a new AcpHttpTransport
|
||||
Scenario: Transport connect raises A2aNotAvailableError
|
||||
Given a new A2aHttpTransport
|
||||
When I try to connect via the transport to "http://localhost:8080"
|
||||
Then an AcpNotAvailableError should be raised
|
||||
Then an A2aNotAvailableError should be raised
|
||||
|
||||
|
||||
Scenario: Transport disconnect raises AcpNotAvailableError
|
||||
Given a new AcpHttpTransport
|
||||
Scenario: Transport disconnect raises A2aNotAvailableError
|
||||
Given a new A2aHttpTransport
|
||||
When I try to disconnect the transport
|
||||
Then an AcpNotAvailableError should be raised
|
||||
Then an A2aNotAvailableError should be raised
|
||||
|
||||
|
||||
Scenario: Transport is_connected returns False
|
||||
Given a new AcpHttpTransport
|
||||
Given a new A2aHttpTransport
|
||||
Then the transport should not be connected
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpEventQueue — local mode works
|
||||
# A2aEventQueue — local mode works
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: Publish event to local queue
|
||||
Given a new AcpEventQueue
|
||||
Given a new A2aEventQueue
|
||||
When I publish an event with type "plan.started"
|
||||
Then the event queue should have 1 event
|
||||
|
||||
|
||||
Scenario: Subscribe locally and receive event
|
||||
Given a new AcpEventQueue
|
||||
Given a new A2aEventQueue
|
||||
And I subscribe locally with a callback
|
||||
When I publish an event with type "plan.completed"
|
||||
Then the callback should have been called with event type "plan.completed"
|
||||
|
||||
|
||||
Scenario: Unsubscribe removes subscription
|
||||
Given a new AcpEventQueue
|
||||
Given a new A2aEventQueue
|
||||
And I subscribe locally with a callback
|
||||
When I unsubscribe using the subscription id
|
||||
Then the unsubscribe should return True
|
||||
|
||||
|
||||
Scenario: Unsubscribe non-existent returns False
|
||||
Given a new AcpEventQueue
|
||||
Given a new A2aEventQueue
|
||||
When I unsubscribe using a non-existent subscription id
|
||||
Then the unsubscribe should return False
|
||||
|
||||
|
||||
Scenario: Get events respects limit
|
||||
Given a new AcpEventQueue
|
||||
Given a new A2aEventQueue
|
||||
When I publish 5 events
|
||||
And I get events with limit 3
|
||||
Then I should receive 3 events
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpEventQueue — remote stub raises
|
||||
# A2aEventQueue — remote stub raises
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: Remote subscribe raises AcpNotAvailableError
|
||||
Given a new AcpEventQueue
|
||||
Scenario: Remote subscribe raises A2aNotAvailableError
|
||||
Given a new A2aEventQueue
|
||||
When I try to subscribe remotely to "http://remote:9090/events"
|
||||
Then an AcpNotAvailableError should be raised
|
||||
Then an A2aNotAvailableError should be raised
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpVersionNegotiator
|
||||
# A2aVersionNegotiator
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: Negotiate supported version succeeds
|
||||
Given a new AcpVersionNegotiator
|
||||
Given a new A2aVersionNegotiator
|
||||
When I negotiate version "1.0"
|
||||
Then the negotiated version should be "1.0"
|
||||
|
||||
|
||||
Scenario: Negotiate unsupported version raises error
|
||||
Given a new AcpVersionNegotiator
|
||||
Given a new A2aVersionNegotiator
|
||||
When I try to negotiate version "2.0"
|
||||
Then an AcpVersionMismatchError should be raised
|
||||
Then an A2aVersionMismatchError should be raised
|
||||
And the error requested_version should be "2.0"
|
||||
|
||||
|
||||
Scenario: is_supported returns True for valid version
|
||||
Given a new AcpVersionNegotiator
|
||||
Given a new A2aVersionNegotiator
|
||||
Then version "1.0" should be supported
|
||||
|
||||
|
||||
Scenario: is_supported returns False for invalid version
|
||||
Given a new AcpVersionNegotiator
|
||||
Given a new A2aVersionNegotiator
|
||||
Then version "99.0" should not be supported
|
||||
|
||||
|
||||
Scenario: get_current returns current version
|
||||
Given a new AcpVersionNegotiator
|
||||
Given a new A2aVersionNegotiator
|
||||
Then the current version should be "1.0"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpRequest model validation
|
||||
# A2aRequest model validation
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: AcpRequest with valid operation succeeds
|
||||
When I create an AcpRequest with operation "session.create"
|
||||
Scenario: A2aRequest with valid operation succeeds
|
||||
When I create an A2aRequest with operation "session.create"
|
||||
Then the request should have a non-empty request_id
|
||||
And the request acp_version should be "1.0"
|
||||
And the request a2a_version should be "1.0"
|
||||
|
||||
|
||||
Scenario: AcpRequest with empty operation fails validation
|
||||
When I try to create an AcpRequest with empty operation
|
||||
Scenario: A2aRequest with empty operation fails validation
|
||||
When I try to create an A2aRequest with empty operation
|
||||
Then a validation error should be raised
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpResponse model validation
|
||||
# A2aResponse model validation
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: AcpResponse with valid status ok succeeds
|
||||
When I create an AcpResponse with status "ok" and request_id "REQ001"
|
||||
Scenario: A2aResponse with valid status ok succeeds
|
||||
When I create an A2aResponse with status "ok" and request_id "REQ001"
|
||||
Then the response should be valid
|
||||
|
||||
|
||||
Scenario: AcpResponse with invalid status fails validation
|
||||
When I try to create an AcpResponse with status "maybe"
|
||||
Scenario: A2aResponse with invalid status fails validation
|
||||
When I try to create an A2aResponse with status "maybe"
|
||||
Then a validation error should be raised
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpErrorDetail model validation
|
||||
# A2aErrorDetail model validation
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: AcpErrorDetail with valid fields succeeds
|
||||
When I create an AcpErrorDetail with code "NOT_FOUND" and message "gone"
|
||||
Scenario: A2aErrorDetail with valid fields succeeds
|
||||
When I create an A2aErrorDetail with code "NOT_FOUND" and message "gone"
|
||||
Then the error detail should be valid
|
||||
|
||||
|
||||
Scenario: AcpErrorDetail with empty code fails validation
|
||||
When I try to create an AcpErrorDetail with empty code
|
||||
Scenario: A2aErrorDetail with empty code fails validation
|
||||
When I try to create an A2aErrorDetail with empty code
|
||||
Then a validation error should be raised
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# AcpEvent model construction
|
||||
# A2aEvent model construction
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: AcpEvent auto-generates event_id and timestamp
|
||||
When I create an AcpEvent with type "plan.progress"
|
||||
Scenario: A2aEvent auto-generates event_id and timestamp
|
||||
When I create an A2aEvent with type "plan.progress"
|
||||
Then the event should have a non-empty event_id
|
||||
And the event should have a non-empty timestamp
|
||||
|
||||
@@ -306,24 +306,24 @@ Feature: Consolidated Misc
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
Scenario: AcpError is a CleverAgentsError
|
||||
Then AcpError should be a subclass of CleverAgentsError
|
||||
Scenario: A2aError is a CleverAgentsError
|
||||
Then A2aError should be a subclass of CleverAgentsError
|
||||
|
||||
|
||||
Scenario: AcpNotAvailableError is an AcpError
|
||||
Then AcpNotAvailableError should be a subclass of AcpError
|
||||
Scenario: A2aNotAvailableError is an A2aError
|
||||
Then A2aNotAvailableError should be a subclass of A2aError
|
||||
|
||||
|
||||
Scenario: AcpVersionMismatchError is an AcpError
|
||||
Then AcpVersionMismatchError should be a subclass of AcpError
|
||||
Scenario: A2aVersionMismatchError is an A2aError
|
||||
Then A2aVersionMismatchError should be a subclass of A2aError
|
||||
|
||||
|
||||
Scenario: AcpOperationNotFoundError is an AcpError
|
||||
Then AcpOperationNotFoundError should be a subclass of AcpError
|
||||
Scenario: A2aOperationNotFoundError is an A2aError
|
||||
Then A2aOperationNotFoundError should be a subclass of A2aError
|
||||
|
||||
|
||||
Scenario: AcpNotAvailableError has default message
|
||||
When I create an AcpNotAvailableError with default message
|
||||
Scenario: A2aNotAvailableError has default message
|
||||
When I create an A2aNotAvailableError with default message
|
||||
Then the error message should contain "not available in local mode"
|
||||
|
||||
|
||||
@@ -485,17 +485,17 @@ Feature: Consolidated Misc
|
||||
# Feature: M6 autonomy acceptance smoke tests
|
||||
# ============================================================
|
||||
|
||||
Scenario: M6 smoke load ACP facade flows fixture
|
||||
Scenario: M6 smoke load A2A facade flows fixture
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
When I m6 smoke load the ACP facade flows fixture
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke load the A2A facade flows fixture
|
||||
Then the m6 smoke facade fixture should have a session lifecycle entry
|
||||
And the m6 smoke facade fixture should have a plan lifecycle entry
|
||||
|
||||
|
||||
Scenario: M6 smoke load autonomy guardrails fixture
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke load the autonomy guardrails fixture
|
||||
Then the m6 smoke guardrails fixture should have a denylist entry
|
||||
And the m6 smoke guardrails fixture should have an allowlist entry
|
||||
@@ -504,237 +504,237 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke load automation profiles fixture
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke load the automation profiles fixture
|
||||
Then the m6 smoke profiles fixture should list all 8 built-in names
|
||||
And the m6 smoke profiles fixture should have a custom profile entry
|
||||
|
||||
# --- ACP facade session operations ---
|
||||
# --- A2A facade session operations ---
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP session create returns session id
|
||||
Scenario: M6 smoke A2A session create returns session id
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "session.create" with params {}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data should contain key "session_id"
|
||||
And the m6 smoke response data should contain key "status"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP session close returns closed status
|
||||
Scenario: M6 smoke A2A session close returns closed status
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "session.close" with params {}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data "status" should equal "closed"
|
||||
|
||||
# --- ACP facade plan operations ---
|
||||
# --- A2A facade plan operations ---
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP plan create returns plan id
|
||||
Scenario: M6 smoke A2A plan create returns plan id
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "plan.create" with params {}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data should contain key "plan_id"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP plan execute queues plan
|
||||
Scenario: M6 smoke A2A plan execute queues plan
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "plan.execute" with params {"plan_id": "01M6SM0KE00000000000000001"}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data "status" should equal "queued"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP plan status returns phase
|
||||
Scenario: M6 smoke A2A plan status returns phase
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "plan.status" with params {"plan_id": "01M6SM0KE00000000000000001"}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data should contain key "phase"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP plan diff returns changes list
|
||||
Scenario: M6 smoke A2A plan diff returns changes list
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "plan.diff" with params {"plan_id": "01M6SM0KE00000000000000001"}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data should contain key "changes"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP plan apply returns applied status
|
||||
Scenario: M6 smoke A2A plan apply returns applied status
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "plan.apply" with params {"plan_id": "01M6SM0KE00000000000000001"}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data "status" should equal "applied"
|
||||
|
||||
# --- ACP facade registry and context operations ---
|
||||
# --- A2A facade registry and context operations ---
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP registry list tools returns empty list
|
||||
Scenario: M6 smoke A2A registry list tools returns empty list
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "registry.list_tools" with params {}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data should contain key "tools"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP registry list resources returns empty list
|
||||
Scenario: M6 smoke A2A registry list resources returns empty list
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "registry.list_resources" with params {}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data should contain key "resources"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP context get returns context dict
|
||||
Scenario: M6 smoke A2A context get returns context dict
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "context.get" with params {}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data should contain key "context"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP event subscribe returns subscription id
|
||||
Scenario: M6 smoke A2A event subscribe returns subscription id
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch "event.subscribe" with params {}
|
||||
Then the m6 smoke response status should be "ok"
|
||||
And the m6 smoke response data should contain key "subscription_id"
|
||||
|
||||
# --- ACP facade error handling ---
|
||||
# --- A2A facade error handling ---
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP unknown operation raises error
|
||||
Scenario: M6 smoke A2A unknown operation raises error
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch unknown operation "nonexistent.op"
|
||||
Then the m6 smoke facade should raise AcpOperationNotFoundError
|
||||
Then the m6 smoke facade should raise A2aOperationNotFoundError
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP dispatch with invalid request type raises TypeError
|
||||
Scenario: M6 smoke A2A dispatch with invalid request type raises TypeError
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
When I m6 smoke dispatch with a non-AcpRequest object
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke dispatch with a non-A2aRequest object
|
||||
Then the m6 smoke facade should raise TypeError
|
||||
|
||||
# --- ACP facade service registration ---
|
||||
# --- A2A facade service registration ---
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP register service stores service
|
||||
Scenario: M6 smoke A2A register service stores service
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke register service "plan_service" on the facade
|
||||
Then the m6 smoke facade should have service "plan_service"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP list operations returns all supported
|
||||
Scenario: M6 smoke A2A list operations returns all supported
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke list facade operations
|
||||
Then the m6 smoke operations should include "session.create"
|
||||
And the m6 smoke operations should include "plan.execute"
|
||||
And the m6 smoke operations should include "event.subscribe"
|
||||
And the m6 smoke operations count should be 11
|
||||
|
||||
# --- ACP event queue ---
|
||||
# --- A2A event queue ---
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP event queue publish and retrieve
|
||||
Scenario: M6 smoke A2A event queue publish and retrieve
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
Given a m6 smoke ACP event queue
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke A2A event queue
|
||||
When I m6 smoke publish an event with type "plan.progress"
|
||||
Then the m6 smoke event queue should have 1 event
|
||||
And the m6 smoke last event type should be "plan.progress"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP event queue subscribe local callback
|
||||
Scenario: M6 smoke A2A event queue subscribe local callback
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
Given a m6 smoke ACP event queue
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke A2A event queue
|
||||
When I m6 smoke subscribe a local callback
|
||||
And I m6 smoke publish an event with type "plan.complete"
|
||||
Then the m6 smoke callback should have been called once
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP event queue unsubscribe
|
||||
Scenario: M6 smoke A2A event queue unsubscribe
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
Given a m6 smoke ACP event queue
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke A2A event queue
|
||||
When I m6 smoke subscribe a local callback
|
||||
And I m6 smoke unsubscribe the callback
|
||||
And I m6 smoke publish an event with type "plan.complete"
|
||||
Then the m6 smoke callback should not have been called
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP event queue close prevents publish
|
||||
Scenario: M6 smoke A2A event queue close prevents publish
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
Given a m6 smoke ACP event queue
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke A2A event queue
|
||||
When I m6 smoke close the event queue
|
||||
Then the m6 smoke publishing should raise RuntimeError
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP event queue remote subscribe raises error
|
||||
Scenario: M6 smoke A2A event queue remote subscribe raises error
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
Given a m6 smoke ACP event queue
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke A2A event queue
|
||||
When I m6 smoke attempt remote subscribe to "https://example.com/events"
|
||||
Then the m6 smoke facade should raise AcpNotAvailableError
|
||||
Then the m6 smoke facade should raise A2aNotAvailableError
|
||||
|
||||
# --- ACP HTTP transport stub ---
|
||||
# --- A2A HTTP transport stub ---
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP transport send raises not available
|
||||
Scenario: M6 smoke A2A transport send raises not available
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke attempt transport send
|
||||
Then the m6 smoke facade should raise AcpNotAvailableError
|
||||
Then the m6 smoke facade should raise A2aNotAvailableError
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP transport connect raises not available
|
||||
Scenario: M6 smoke A2A transport connect raises not available
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
When I m6 smoke attempt transport connect to "https://example.com/acp"
|
||||
Then the m6 smoke facade should raise AcpNotAvailableError
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke attempt transport connect to "https://example.com/a2a"
|
||||
Then the m6 smoke facade should raise A2aNotAvailableError
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP transport disconnect raises not available
|
||||
Scenario: M6 smoke A2A transport disconnect raises not available
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke attempt transport disconnect
|
||||
Then the m6 smoke facade should raise AcpNotAvailableError
|
||||
Then the m6 smoke facade should raise A2aNotAvailableError
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP transport is_connected returns false
|
||||
Scenario: M6 smoke A2A transport is_connected returns false
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke check transport is_connected
|
||||
Then the m6 smoke transport should not be connected
|
||||
|
||||
# --- ACP version negotiation ---
|
||||
# --- A2A version negotiation ---
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP version negotiation accepts 1.0
|
||||
Scenario: M6 smoke A2A version negotiation accepts 1.0
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
When I m6 smoke negotiate ACP version "1.0"
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke negotiate A2A version "1.0"
|
||||
Then the m6 smoke negotiated version should be "1.0"
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP version negotiation rejects unsupported
|
||||
Scenario: M6 smoke A2A version negotiation rejects unsupported
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
When I m6 smoke negotiate ACP version "2.0"
|
||||
Then the m6 smoke facade should raise AcpVersionMismatchError
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke negotiate A2A version "2.0"
|
||||
Then the m6 smoke facade should raise A2aVersionMismatchError
|
||||
|
||||
|
||||
Scenario: M6 smoke ACP version is_supported returns correct result
|
||||
Scenario: M6 smoke A2A version is_supported returns correct result
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke check if version "1.0" is supported
|
||||
Then the m6 smoke version support should be true
|
||||
When I m6 smoke check if version "99.0" is supported
|
||||
@@ -745,7 +745,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke all 8 built-in profiles exist
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke list all built-in profiles
|
||||
Then the m6 smoke profile count should be 8
|
||||
And the m6 smoke profiles should include "manual"
|
||||
@@ -754,7 +754,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke manual profile has all thresholds at 1.0
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke load built-in profile "manual"
|
||||
Then the m6 smoke profile auto_strategize should be 1.0
|
||||
And the m6 smoke profile auto_execute should be 1.0
|
||||
@@ -764,7 +764,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke full-auto profile has no gates
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke load built-in profile "full-auto"
|
||||
Then the m6 smoke profile auto_strategize should be 0.0
|
||||
And the m6 smoke profile auto_execute should be 0.0
|
||||
@@ -777,7 +777,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke create custom namespaced profile
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke create a profile named "acme/strict" with auto_apply 1.0
|
||||
Then the m6 smoke created profile name should be "acme/strict"
|
||||
And the m6 smoke created profile auto_apply should be 1.0
|
||||
@@ -785,21 +785,21 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke profile name validation rejects invalid
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke create a profile with invalid name "has spaces"
|
||||
Then the m6 smoke creation should raise ValueError
|
||||
|
||||
|
||||
Scenario: M6 smoke profile threshold validation rejects out of range
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke create a profile with auto_strategize 1.5
|
||||
Then the m6 smoke creation should raise ValueError
|
||||
|
||||
|
||||
Scenario: M6 smoke profile from_yaml loads correctly
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke temporary profile YAML file
|
||||
When I m6 smoke load profile from the temp YAML
|
||||
Then the m6 smoke loaded profile name should be "test-yaml-profile"
|
||||
@@ -809,7 +809,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke guard denylist blocks denied tool
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke profile with denylist guard for "rm_rf"
|
||||
When I m6 smoke check guard for tool "rm_rf"
|
||||
Then the m6 smoke guard result should not be allowed
|
||||
@@ -818,7 +818,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke guard denylist allows non-denied tool
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke profile with denylist guard for "rm_rf"
|
||||
When I m6 smoke check guard for tool "read_file"
|
||||
Then the m6 smoke guard result should be allowed
|
||||
@@ -826,7 +826,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke guard allowlist blocks unlisted tool
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke profile with allowlist guard for "read_file" and "search"
|
||||
When I m6 smoke check guard for tool "write_file"
|
||||
Then the m6 smoke guard result should not be allowed
|
||||
@@ -835,7 +835,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke guard max tool calls blocks at limit
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke profile with max 5 tool calls per step
|
||||
When I m6 smoke check guard for tool "llm_call" with 5 calls so far
|
||||
Then the m6 smoke guard result should not be allowed
|
||||
@@ -844,7 +844,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke guard cost budget blocks at cap
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke profile with max cost 10.0
|
||||
When I m6 smoke check guard for tool "llm_call" with cost 10.0
|
||||
Then the m6 smoke guard result should not be allowed
|
||||
@@ -853,7 +853,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke guard write approval blocks write operations
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke profile with write approval required
|
||||
When I m6 smoke check guard for tool "write_file" as a write operation
|
||||
Then the m6 smoke guard result should not be allowed
|
||||
@@ -862,7 +862,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke guard apply approval blocks apply phase
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke profile with apply approval required
|
||||
When I m6 smoke check guard for tool "__apply__"
|
||||
Then the m6 smoke guard result should not be allowed
|
||||
@@ -871,7 +871,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke guard with no guards allows everything
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke profile with no guards
|
||||
When I m6 smoke check guard for tool "anything"
|
||||
Then the m6 smoke guard result should be allowed
|
||||
@@ -881,7 +881,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke profile resolution plan takes precedence
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke automation profile service
|
||||
When I m6 smoke resolve profile with plan "ci" action "auto" project "manual"
|
||||
Then the m6 smoke resolved profile name should be "ci"
|
||||
@@ -889,7 +889,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke profile resolution action takes precedence over project
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke automation profile service
|
||||
When I m6 smoke resolve profile with plan null action "auto" project "manual"
|
||||
Then the m6 smoke resolved profile name should be "auto"
|
||||
@@ -897,7 +897,7 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke profile resolution falls back to global default
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke automation profile service
|
||||
When I m6 smoke resolve profile with plan null action null project null
|
||||
Then the m6 smoke resolved profile name should be "manual"
|
||||
@@ -907,39 +907,39 @@ Feature: Consolidated Misc
|
||||
|
||||
Scenario: M6 smoke service evaluate guard delegates to profile
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
And a m6 smoke A2A local facade
|
||||
Given a m6 smoke automation profile service
|
||||
When I m6 smoke evaluate guard for profile "manual" and tool "read_file"
|
||||
Then the m6 smoke guard result should be allowed
|
||||
|
||||
# --- ACP model validation ---
|
||||
# --- A2A model validation ---
|
||||
|
||||
|
||||
Scenario: M6 smoke AcpRequest validates non-empty operation
|
||||
Scenario: M6 smoke A2aRequest validates non-empty operation
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
When I m6 smoke create AcpRequest with empty operation
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke create A2aRequest with empty operation
|
||||
Then the m6 smoke creation should raise ValueError
|
||||
|
||||
|
||||
Scenario: M6 smoke AcpResponse validates status values
|
||||
Scenario: M6 smoke A2aResponse validates status values
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
When I m6 smoke create AcpResponse with invalid status "maybe"
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke create A2aResponse with invalid status "maybe"
|
||||
Then the m6 smoke creation should raise ValueError
|
||||
|
||||
|
||||
Scenario: M6 smoke AcpEvent validates non-empty event_type
|
||||
Scenario: M6 smoke A2aEvent validates non-empty event_type
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
When I m6 smoke create AcpEvent with empty event_type
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke create A2aEvent with empty event_type
|
||||
Then the m6 smoke creation should raise ValueError
|
||||
|
||||
|
||||
Scenario: M6 smoke AcpErrorDetail validates non-empty fields
|
||||
Scenario: M6 smoke A2aErrorDetail validates non-empty fields
|
||||
Given a m6 smoke test runner
|
||||
And a m6 smoke ACP local facade
|
||||
When I m6 smoke create AcpErrorDetail with empty code
|
||||
And a m6 smoke A2A local facade
|
||||
When I m6 smoke create A2aErrorDetail with empty code
|
||||
Then the m6 smoke creation should raise ValueError
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"description": "ACP local-mode facade operation flows for M6 autonomy acceptance tests",
|
||||
"description": "A2A local-mode facade operation flows for M6 autonomy acceptance tests",
|
||||
"fixtures": [
|
||||
{
|
||||
"name": "session_lifecycle",
|
||||
@@ -35,7 +35,7 @@
|
||||
{
|
||||
"name": "unknown_operation",
|
||||
"operations": [
|
||||
{"operation": "nonexistent.op", "params": {}, "expected_error": "AcpOperationNotFoundError"}
|
||||
{"operation": "nonexistent.op", "params": {}, "expected_error": "A2aOperationNotFoundError"}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -145,20 +145,20 @@ Feature: LSP server stub JSON-RPC protocol
|
||||
And the server exit code should be 1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ACP facade wiring
|
||||
# A2A facade wiring
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@lsp_acp
|
||||
Scenario: LSP server stores provided ACP facade
|
||||
Given an LSP server with ACP facade
|
||||
@lsp_a2a
|
||||
Scenario: LSP server stores provided A2A facade
|
||||
Given an LSP server with A2A facade
|
||||
And an initialize request with id 1
|
||||
When the LSP server processes all messages
|
||||
Then the response for id 1 should have result
|
||||
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
|
||||
And the server should hold the provided ACP facade
|
||||
And the server should hold the provided A2A facade
|
||||
|
||||
@lsp_acp
|
||||
Scenario: LSP server lazily creates ACP facade on property access
|
||||
@lsp_a2a
|
||||
Scenario: LSP server lazily creates A2A facade on property access
|
||||
Given an initialize request with id 1
|
||||
And an exit notification
|
||||
When the LSP server processes all messages
|
||||
|
||||
@@ -50,7 +50,7 @@ Feature: Async Resource Cleanup and Leak Prevention
|
||||
And the state manager should be marked as closed
|
||||
|
||||
Scenario: Subscriptions are disposed on cleanup
|
||||
Given I have an ACP event queue with 3 active subscriptions
|
||||
Given I have an A2A event queue with 3 active subscriptions
|
||||
When I close the event queue
|
||||
Then all subscriptions should be removed
|
||||
And the subscription count should be zero
|
||||
@@ -106,14 +106,14 @@ Feature: Async Resource Cleanup and Leak Prevention
|
||||
And I try to reset state after close
|
||||
Then a RuntimeError should be raised mentioning "closed"
|
||||
|
||||
Scenario: Publish after close raises RuntimeError on AcpEventQueue
|
||||
Given I have an ACP event queue with 1 active subscriptions
|
||||
Scenario: Publish after close raises RuntimeError on A2aEventQueue
|
||||
Given I have an A2A event queue with 1 active subscriptions
|
||||
When I close the event queue
|
||||
And I try to publish an event after close
|
||||
Then a RuntimeError should be raised mentioning "closed"
|
||||
|
||||
Scenario: AcpEventQueue exposes is_closed property
|
||||
Given I have an ACP event queue with 0 active subscriptions
|
||||
Scenario: A2aEventQueue exposes is_closed property
|
||||
Given I have an A2A event queue with 0 active subscriptions
|
||||
Then the event queue is_closed should be False
|
||||
When I close the event queue
|
||||
Then the event queue is_closed should be True
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@phase2 @acp @server-cli-coverage
|
||||
@phase2 @a2a @server-cli-coverage
|
||||
Feature: Server CLI command coverage boost
|
||||
As a developer
|
||||
I want full test coverage for server.py CLI commands
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@phase2 @acp @server-stubs
|
||||
@phase2 @a2a @server-stubs
|
||||
Feature: Server Client Stubs and Connection Config
|
||||
As a developer
|
||||
I want protocol stubs for server communication
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Step definitions for ACP clients coverage boost scenarios.
|
||||
"""Step definitions for A2A clients coverage boost scenarios.
|
||||
|
||||
Covers:
|
||||
- Protocol default method bodies (the ``...`` expression on lines 28, 36, 52, 63, 74, 90, 101)
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.acp.clients import (
|
||||
from cleveragents.a2a.clients import (
|
||||
AuthClient,
|
||||
RemoteExecutionClient,
|
||||
ServerClient,
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
"""Step definitions for ACP facade coverage-boost scenarios.
|
||||
"""Step definitions for A2A facade coverage-boost scenarios.
|
||||
|
||||
Targets uncovered lines in ``src/cleveragents/acp/facade.py``:
|
||||
Targets uncovered lines in ``src/cleveragents/a2a/facade.py``:
|
||||
|
||||
| Line | Code path |
|
||||
|------|--------------------------------------------------------|
|
||||
@@ -20,8 +20,8 @@ from unittest.mock import MagicMock
|
||||
from behave import given, then, use_step_matcher, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.acp.facade import AcpLocalFacade
|
||||
from cleveragents.acp.models import AcpRequest
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
from cleveragents.a2a.models import A2aRequest
|
||||
|
||||
use_step_matcher("re")
|
||||
|
||||
@@ -61,7 +61,7 @@ def _build_mock_plan_lifecycle_service() -> MagicMock:
|
||||
|
||||
@given(r"a coverage-boost facade with a mock PlanLifecycleService")
|
||||
def step_cb_facade_plan(context: Context) -> None:
|
||||
context.cb_facade = AcpLocalFacade(
|
||||
context.cb_facade = A2aLocalFacade(
|
||||
services={"plan_lifecycle_service": _build_mock_plan_lifecycle_service()}
|
||||
)
|
||||
|
||||
@@ -71,11 +71,11 @@ def step_cb_facade_plan(context: Context) -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when(r"I try to create an AcpLocalFacade with a non-dict services argument")
|
||||
@when(r"I try to create an A2aLocalFacade with a non-dict services argument")
|
||||
def step_cb_create_facade_non_dict(context: Context) -> None:
|
||||
context.cb_caught_error = None
|
||||
try:
|
||||
AcpLocalFacade(services=["not", "a", "dict"]) # type: ignore[arg-type]
|
||||
A2aLocalFacade(services=["not", "a", "dict"]) # type: ignore[arg-type]
|
||||
except TypeError as exc:
|
||||
context.cb_caught_error = exc
|
||||
|
||||
@@ -86,7 +86,7 @@ def step_cb_create_facade_non_dict(context: Context) -> None:
|
||||
)
|
||||
def step_cb_dispatch(context: Context, operation: str, params_json: str) -> None:
|
||||
params: dict[str, Any] = json.loads(params_json)
|
||||
request = AcpRequest(operation=operation, params=params)
|
||||
request = A2aRequest(operation=operation, params=params)
|
||||
context.cb_response = context.cb_facade.dispatch(request)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Step definitions for ACP facade and stubs Behave scenarios."""
|
||||
"""Step definitions for A2A facade and stubs Behave scenarios."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -9,39 +9,39 @@ from behave import given, then, use_step_matcher, when
|
||||
from behave.runner import Context
|
||||
from pydantic import ValidationError
|
||||
|
||||
from cleveragents.acp.errors import (
|
||||
AcpError,
|
||||
AcpNotAvailableError,
|
||||
AcpOperationNotFoundError,
|
||||
AcpVersionMismatchError,
|
||||
from cleveragents.a2a.errors import (
|
||||
A2aError,
|
||||
A2aNotAvailableError,
|
||||
A2aOperationNotFoundError,
|
||||
A2aVersionMismatchError,
|
||||
)
|
||||
from cleveragents.acp.events import AcpEventQueue
|
||||
from cleveragents.acp.facade import AcpLocalFacade
|
||||
from cleveragents.acp.models import (
|
||||
AcpErrorDetail,
|
||||
AcpEvent,
|
||||
AcpRequest,
|
||||
AcpResponse,
|
||||
from cleveragents.a2a.events import A2aEventQueue
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
from cleveragents.a2a.models import (
|
||||
A2aErrorDetail,
|
||||
A2aEvent,
|
||||
A2aRequest,
|
||||
A2aResponse,
|
||||
)
|
||||
from cleveragents.acp.transport import AcpHttpTransport
|
||||
from cleveragents.acp.versioning import AcpVersionNegotiator
|
||||
from cleveragents.a2a.transport import A2aHttpTransport
|
||||
from cleveragents.a2a.versioning import A2aVersionNegotiator
|
||||
from cleveragents.core.exceptions import CleverAgentsError
|
||||
|
||||
use_step_matcher("re")
|
||||
|
||||
|
||||
# AcpLocalFacade — creation and service registration
|
||||
# A2aLocalFacade — creation and service registration
|
||||
|
||||
|
||||
@given(r"a new AcpLocalFacade with no services")
|
||||
@given(r"a new A2aLocalFacade with no services")
|
||||
def step_facade_no_services(context: Context) -> None:
|
||||
context.facade = AcpLocalFacade()
|
||||
context.facade = A2aLocalFacade()
|
||||
|
||||
|
||||
@given(r"a new AcpLocalFacade with services (?P<services_json>.+)")
|
||||
@given(r"a new A2aLocalFacade with services (?P<services_json>.+)")
|
||||
def step_facade_with_services(context: Context, services_json: str) -> None:
|
||||
services = json.loads(services_json)
|
||||
context.facade = AcpLocalFacade(services=services)
|
||||
context.facade = A2aLocalFacade(services=services)
|
||||
|
||||
|
||||
@then(r"the facade should be created successfully")
|
||||
@@ -70,20 +70,20 @@ def step_register_empty_name(context: Context) -> None:
|
||||
context.error = exc
|
||||
|
||||
|
||||
@then(r"an ACP ValueError should be raised")
|
||||
@then(r"an A2A ValueError should be raised")
|
||||
def step_value_error_raised(context: Context) -> None:
|
||||
assert isinstance(context.caught_error, ValueError), (
|
||||
f"Expected ValueError, got {type(context.caught_error)}"
|
||||
)
|
||||
|
||||
|
||||
# AcpLocalFacade — dispatch operations
|
||||
# A2aLocalFacade — dispatch operations
|
||||
|
||||
|
||||
@when(r'I dispatch operation "(?P<operation>[^"]+)" with params (?P<params_json>.+)')
|
||||
def step_dispatch_operation(context: Context, operation: str, params_json: str) -> None:
|
||||
params: dict[str, Any] = json.loads(params_json)
|
||||
request = AcpRequest(operation=operation, params=params)
|
||||
request = A2aRequest(operation=operation, params=params)
|
||||
context.response = context.facade.dispatch(request)
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ def step_response_data_key_value(context: Context, key: str, value: str) -> None
|
||||
assert str(actual) == value, f"Expected '{value}', got '{actual}'"
|
||||
|
||||
|
||||
# AcpLocalFacade — unknown operation
|
||||
# A2aLocalFacade — unknown operation
|
||||
|
||||
|
||||
@when(r'I dispatch an unknown operation "(?P<operation>[^"]+)"')
|
||||
@@ -116,16 +116,16 @@ def step_dispatch_unknown(context: Context, operation: str) -> None:
|
||||
context.caught_error = None
|
||||
context.error = None
|
||||
try:
|
||||
request = AcpRequest(operation=operation)
|
||||
request = A2aRequest(operation=operation)
|
||||
context.facade.dispatch(request)
|
||||
except AcpOperationNotFoundError as exc:
|
||||
except A2aOperationNotFoundError as exc:
|
||||
context.caught_error = exc
|
||||
context.error = exc
|
||||
|
||||
|
||||
@then(r"an AcpOperationNotFoundError should be raised")
|
||||
@then(r"an A2aOperationNotFoundError should be raised")
|
||||
def step_op_not_found_raised(context: Context) -> None:
|
||||
assert isinstance(context.caught_error, AcpOperationNotFoundError)
|
||||
assert isinstance(context.caught_error, A2aOperationNotFoundError)
|
||||
|
||||
|
||||
@then(r'the error operation attribute should be "(?P<operation>[^"]+)"')
|
||||
@@ -133,7 +133,7 @@ def step_error_operation_attr(context: Context, operation: str) -> None:
|
||||
assert context.caught_error.operation == operation
|
||||
|
||||
|
||||
# AcpLocalFacade — list_operations
|
||||
# A2aLocalFacade — list_operations
|
||||
|
||||
|
||||
@when(r"I call list_operations on the facade")
|
||||
@@ -154,12 +154,12 @@ def step_operations_count(context: Context, count: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
# AcpHttpTransport
|
||||
# A2aHttpTransport
|
||||
|
||||
|
||||
@given(r"a new AcpHttpTransport")
|
||||
@given(r"a new A2aHttpTransport")
|
||||
def step_transport(context: Context) -> None:
|
||||
context.transport = AcpHttpTransport()
|
||||
context.transport = A2aHttpTransport()
|
||||
|
||||
|
||||
@when(r"I try to send a request via the transport")
|
||||
@@ -167,9 +167,9 @@ def step_transport_send(context: Context) -> None:
|
||||
context.caught_error = None
|
||||
context.error = None
|
||||
try:
|
||||
request = AcpRequest(operation="test.op")
|
||||
request = A2aRequest(operation="test.op")
|
||||
context.transport.send(request)
|
||||
except AcpNotAvailableError as exc:
|
||||
except A2aNotAvailableError as exc:
|
||||
context.caught_error = exc
|
||||
context.error = exc
|
||||
|
||||
@@ -180,7 +180,7 @@ def step_transport_connect(context: Context, url: str) -> None:
|
||||
context.error = None
|
||||
try:
|
||||
context.transport.connect(url)
|
||||
except AcpNotAvailableError as exc:
|
||||
except A2aNotAvailableError as exc:
|
||||
context.caught_error = exc
|
||||
context.error = exc
|
||||
|
||||
@@ -191,15 +191,15 @@ def step_transport_disconnect(context: Context) -> None:
|
||||
context.error = None
|
||||
try:
|
||||
context.transport.disconnect()
|
||||
except AcpNotAvailableError as exc:
|
||||
except A2aNotAvailableError as exc:
|
||||
context.caught_error = exc
|
||||
context.error = exc
|
||||
|
||||
|
||||
@then(r"an AcpNotAvailableError should be raised")
|
||||
@then(r"an A2aNotAvailableError should be raised")
|
||||
def step_not_available_raised(context: Context) -> None:
|
||||
assert isinstance(context.caught_error, AcpNotAvailableError), (
|
||||
f"Expected AcpNotAvailableError, got {type(context.caught_error)}"
|
||||
assert isinstance(context.caught_error, A2aNotAvailableError), (
|
||||
f"Expected A2aNotAvailableError, got {type(context.caught_error)}"
|
||||
)
|
||||
|
||||
|
||||
@@ -208,18 +208,18 @@ def step_transport_not_connected(context: Context) -> None:
|
||||
assert context.transport.is_connected() is False
|
||||
|
||||
|
||||
# AcpEventQueue — local mode
|
||||
# A2aEventQueue — local mode
|
||||
|
||||
|
||||
@given(r"a new AcpEventQueue")
|
||||
@given(r"a new A2aEventQueue")
|
||||
def step_event_queue(context: Context) -> None:
|
||||
context.queue = AcpEventQueue()
|
||||
context.callback_events: list[AcpEvent] = []
|
||||
context.queue = A2aEventQueue()
|
||||
context.callback_events: list[A2aEvent] = []
|
||||
|
||||
|
||||
@given(r"I subscribe locally with a callback")
|
||||
def step_subscribe_local(context: Context) -> None:
|
||||
def _cb(event: AcpEvent) -> None:
|
||||
def _cb(event: A2aEvent) -> None:
|
||||
context.callback_events.append(event)
|
||||
|
||||
context.subscription_id = context.queue.subscribe_local(_cb)
|
||||
@@ -227,14 +227,14 @@ def step_subscribe_local(context: Context) -> None:
|
||||
|
||||
@when(r'I publish an event with type "(?P<event_type>[^"]+)"')
|
||||
def step_publish_event(context: Context, event_type: str) -> None:
|
||||
event = AcpEvent(event_type=event_type)
|
||||
event = A2aEvent(event_type=event_type)
|
||||
context.queue.publish(event)
|
||||
|
||||
|
||||
@when(r"I publish (?P<count>\d+) events")
|
||||
def step_publish_n_events(context: Context, count: str) -> None:
|
||||
for i in range(int(count)):
|
||||
event = AcpEvent(event_type=f"test.event.{i}")
|
||||
event = A2aEvent(event_type=f"test.event.{i}")
|
||||
context.queue.publish(event)
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ def step_received_count(context: Context, count: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
# AcpEventQueue — remote stub
|
||||
# A2aEventQueue — remote stub
|
||||
|
||||
|
||||
@when(r'I try to subscribe remotely to "(?P<endpoint>[^"]+)"')
|
||||
@@ -293,17 +293,17 @@ def step_remote_subscribe(context: Context, endpoint: str) -> None:
|
||||
context.error = None
|
||||
try:
|
||||
context.queue.subscribe_remote(endpoint)
|
||||
except AcpNotAvailableError as exc:
|
||||
except A2aNotAvailableError as exc:
|
||||
context.caught_error = exc
|
||||
context.error = exc
|
||||
|
||||
|
||||
# AcpVersionNegotiator
|
||||
# A2aVersionNegotiator
|
||||
|
||||
|
||||
@given(r"a new AcpVersionNegotiator")
|
||||
@given(r"a new A2aVersionNegotiator")
|
||||
def step_version_negotiator(context: Context) -> None:
|
||||
context.negotiator = AcpVersionNegotiator()
|
||||
context.negotiator = A2aVersionNegotiator()
|
||||
|
||||
|
||||
@when(r'I negotiate version "(?P<version>[^"]+)"')
|
||||
@@ -322,14 +322,14 @@ def step_negotiate_fail(context: Context, version: str) -> None:
|
||||
context.error = None
|
||||
try:
|
||||
context.negotiator.negotiate(version)
|
||||
except AcpVersionMismatchError as exc:
|
||||
except A2aVersionMismatchError as exc:
|
||||
context.caught_error = exc
|
||||
context.error = exc
|
||||
|
||||
|
||||
@then(r"an AcpVersionMismatchError should be raised")
|
||||
@then(r"an A2aVersionMismatchError should be raised")
|
||||
def step_version_mismatch_raised(context: Context) -> None:
|
||||
assert isinstance(context.caught_error, AcpVersionMismatchError)
|
||||
assert isinstance(context.caught_error, A2aVersionMismatchError)
|
||||
|
||||
|
||||
@then(r'the error requested_version should be "(?P<version>[^"]+)"')
|
||||
@@ -352,12 +352,12 @@ def step_current_version(context: Context, version: str) -> None:
|
||||
assert context.negotiator.get_current() == version
|
||||
|
||||
|
||||
# AcpRequest model validation
|
||||
# A2aRequest model validation
|
||||
|
||||
|
||||
@when(r'I create an AcpRequest with operation "(?P<operation>[^"]+)"')
|
||||
@when(r'I create an A2aRequest with operation "(?P<operation>[^"]+)"')
|
||||
def step_create_request(context: Context, operation: str) -> None:
|
||||
context.request = AcpRequest(operation=operation)
|
||||
context.request = A2aRequest(operation=operation)
|
||||
|
||||
|
||||
@then(r"the request should have a non-empty request_id")
|
||||
@@ -365,17 +365,17 @@ def step_request_has_id(context: Context) -> None:
|
||||
assert context.request.request_id, "request_id should not be empty"
|
||||
|
||||
|
||||
@then(r'the request acp_version should be "(?P<version>[^"]+)"')
|
||||
@then(r'the request a2a_version should be "(?P<version>[^"]+)"')
|
||||
def step_request_version(context: Context, version: str) -> None:
|
||||
assert context.request.acp_version == version
|
||||
assert context.request.a2a_version == version
|
||||
|
||||
|
||||
@when(r"I try to create an AcpRequest with empty operation")
|
||||
@when(r"I try to create an A2aRequest with empty operation")
|
||||
def step_create_request_empty(context: Context) -> None:
|
||||
context.caught_error = None
|
||||
context.error = None
|
||||
try:
|
||||
AcpRequest(operation="")
|
||||
A2aRequest(operation="")
|
||||
except ValidationError as exc:
|
||||
context.caught_error = exc
|
||||
context.error = exc
|
||||
@@ -386,14 +386,14 @@ def step_create_request_empty(context: Context) -> None:
|
||||
# We reuse those shared step definitions.
|
||||
|
||||
|
||||
# AcpResponse model validation
|
||||
# A2aResponse model validation
|
||||
|
||||
|
||||
@when(
|
||||
r'I create an AcpResponse with status "(?P<status>[^"]+)" and request_id "(?P<rid>[^"]+)"'
|
||||
r'I create an A2aResponse with status "(?P<status>[^"]+)" and request_id "(?P<rid>[^"]+)"'
|
||||
)
|
||||
def step_create_response(context: Context, status: str, rid: str) -> None:
|
||||
context.response = AcpResponse(request_id=rid, status=status)
|
||||
context.response = A2aResponse(request_id=rid, status=status)
|
||||
|
||||
|
||||
@then(r"the response should be valid")
|
||||
@@ -401,25 +401,25 @@ def step_response_valid(context: Context) -> None:
|
||||
assert context.response is not None
|
||||
|
||||
|
||||
@when(r'I try to create an AcpResponse with status "(?P<status>[^"]+)"')
|
||||
@when(r'I try to create an A2aResponse with status "(?P<status>[^"]+)"')
|
||||
def step_create_response_invalid(context: Context, status: str) -> None:
|
||||
context.caught_error = None
|
||||
context.error = None
|
||||
try:
|
||||
AcpResponse(request_id="REQ", status=status)
|
||||
A2aResponse(request_id="REQ", status=status)
|
||||
except ValidationError as exc:
|
||||
context.caught_error = exc
|
||||
context.error = exc
|
||||
|
||||
|
||||
# AcpErrorDetail model validation
|
||||
# A2aErrorDetail model validation
|
||||
|
||||
|
||||
@when(
|
||||
r'I create an AcpErrorDetail with code "(?P<code>[^"]+)" and message "(?P<msg>[^"]+)"'
|
||||
r'I create an A2aErrorDetail with code "(?P<code>[^"]+)" and message "(?P<msg>[^"]+)"'
|
||||
)
|
||||
def step_create_error_detail(context: Context, code: str, msg: str) -> None:
|
||||
context.error_detail = AcpErrorDetail(code=code, message=msg)
|
||||
context.error_detail = A2aErrorDetail(code=code, message=msg)
|
||||
|
||||
|
||||
@then(r"the error detail should be valid")
|
||||
@@ -427,23 +427,23 @@ def step_error_detail_valid(context: Context) -> None:
|
||||
assert context.error_detail is not None
|
||||
|
||||
|
||||
@when(r"I try to create an AcpErrorDetail with empty code")
|
||||
@when(r"I try to create an A2aErrorDetail with empty code")
|
||||
def step_create_error_detail_empty(context: Context) -> None:
|
||||
context.caught_error = None
|
||||
context.error = None
|
||||
try:
|
||||
AcpErrorDetail(code="", message="test")
|
||||
A2aErrorDetail(code="", message="test")
|
||||
except ValidationError as exc:
|
||||
context.caught_error = exc
|
||||
context.error = exc
|
||||
|
||||
|
||||
# AcpEvent model construction
|
||||
# A2aEvent model construction
|
||||
|
||||
|
||||
@when(r'I create an AcpEvent with type "(?P<event_type>[^"]+)"')
|
||||
@when(r'I create an A2aEvent with type "(?P<event_type>[^"]+)"')
|
||||
def step_create_event(context: Context, event_type: str) -> None:
|
||||
context.event = AcpEvent(event_type=event_type)
|
||||
context.event = A2aEvent(event_type=event_type)
|
||||
|
||||
|
||||
@then(r"the event should have a non-empty event_id")
|
||||
@@ -459,29 +459,29 @@ def step_event_has_timestamp(context: Context) -> None:
|
||||
# Error hierarchy
|
||||
|
||||
|
||||
@then(r"AcpError should be a subclass of CleverAgentsError")
|
||||
def step_acp_error_hierarchy(context: Context) -> None:
|
||||
assert issubclass(AcpError, CleverAgentsError)
|
||||
@then(r"A2aError should be a subclass of CleverAgentsError")
|
||||
def step_a2a_error_hierarchy(context: Context) -> None:
|
||||
assert issubclass(A2aError, CleverAgentsError)
|
||||
|
||||
|
||||
@then(r"AcpNotAvailableError should be a subclass of AcpError")
|
||||
@then(r"A2aNotAvailableError should be a subclass of A2aError")
|
||||
def step_not_available_hierarchy(context: Context) -> None:
|
||||
assert issubclass(AcpNotAvailableError, AcpError)
|
||||
assert issubclass(A2aNotAvailableError, A2aError)
|
||||
|
||||
|
||||
@then(r"AcpVersionMismatchError should be a subclass of AcpError")
|
||||
@then(r"A2aVersionMismatchError should be a subclass of A2aError")
|
||||
def step_version_mismatch_hierarchy(context: Context) -> None:
|
||||
assert issubclass(AcpVersionMismatchError, AcpError)
|
||||
assert issubclass(A2aVersionMismatchError, A2aError)
|
||||
|
||||
|
||||
@then(r"AcpOperationNotFoundError should be a subclass of AcpError")
|
||||
@then(r"A2aOperationNotFoundError should be a subclass of A2aError")
|
||||
def step_op_not_found_hierarchy(context: Context) -> None:
|
||||
assert issubclass(AcpOperationNotFoundError, AcpError)
|
||||
assert issubclass(A2aOperationNotFoundError, A2aError)
|
||||
|
||||
|
||||
@when(r"I create an AcpNotAvailableError with default message")
|
||||
@when(r"I create an A2aNotAvailableError with default message")
|
||||
def step_create_not_available_default(context: Context) -> None:
|
||||
context.caught_error = AcpNotAvailableError()
|
||||
context.caught_error = A2aNotAvailableError()
|
||||
context.error = context.caught_error
|
||||
|
||||
|
||||
+26
-26
@@ -1,7 +1,7 @@
|
||||
"""Step definitions for ACP facade wiring Behave scenarios.
|
||||
"""Step definitions for A2A facade wiring Behave scenarios.
|
||||
|
||||
All mocks in this file are lightweight test doubles that simulate the
|
||||
service contracts used by :class:`AcpLocalFacade`. They live here
|
||||
service contracts used by :class:`A2aLocalFacade`. They live here
|
||||
(inside the test tree) per the project's mock-placement policy.
|
||||
"""
|
||||
|
||||
@@ -14,9 +14,9 @@ from unittest.mock import MagicMock
|
||||
from behave import given, then, use_step_matcher, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.acp.events import AcpEventQueue
|
||||
from cleveragents.acp.facade import AcpLocalFacade
|
||||
from cleveragents.acp.models import AcpRequest
|
||||
from cleveragents.a2a.events import A2aEventQueue
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
from cleveragents.a2a.models import A2aRequest
|
||||
from cleveragents.core.exceptions import (
|
||||
BusinessRuleViolation,
|
||||
PlanError,
|
||||
@@ -112,74 +112,74 @@ def _build_mock_resource_registry_service() -> MagicMock:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with a mock SessionService")
|
||||
@given(r"a wired A2aLocalFacade with a mock SessionService")
|
||||
def step_wired_facade_session(context: Context) -> None:
|
||||
context.wired_facade = AcpLocalFacade(
|
||||
context.wired_facade = A2aLocalFacade(
|
||||
services={"session_service": _build_mock_session_service()}
|
||||
)
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with a mock PlanLifecycleService")
|
||||
@given(r"a wired A2aLocalFacade with a mock PlanLifecycleService")
|
||||
def step_wired_facade_plan(context: Context) -> None:
|
||||
context.wired_facade = AcpLocalFacade(
|
||||
context.wired_facade = A2aLocalFacade(
|
||||
services={"plan_lifecycle_service": _build_mock_plan_lifecycle_service()}
|
||||
)
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with a mock ToolRegistry")
|
||||
@given(r"a wired A2aLocalFacade with a mock ToolRegistry")
|
||||
def step_wired_facade_tool_registry(context: Context) -> None:
|
||||
context.wired_facade = AcpLocalFacade(
|
||||
context.wired_facade = A2aLocalFacade(
|
||||
services={"tool_registry": _build_mock_tool_registry()}
|
||||
)
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with a mock ResourceRegistryService")
|
||||
@given(r"a wired A2aLocalFacade with a mock ResourceRegistryService")
|
||||
def step_wired_facade_resource_registry(context: Context) -> None:
|
||||
context.wired_facade = AcpLocalFacade(
|
||||
context.wired_facade = A2aLocalFacade(
|
||||
services={
|
||||
"resource_registry_service": (_build_mock_resource_registry_service())
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with a mock AcpEventQueue")
|
||||
@given(r"a wired A2aLocalFacade with a mock A2aEventQueue")
|
||||
def step_wired_facade_event_queue(context: Context) -> None:
|
||||
context.wired_facade = AcpLocalFacade(services={"event_queue": AcpEventQueue()})
|
||||
context.wired_facade = A2aLocalFacade(services={"event_queue": A2aEventQueue()})
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with no services")
|
||||
@given(r"a wired A2aLocalFacade with no services")
|
||||
def step_wired_facade_no_services(context: Context) -> None:
|
||||
context.wired_facade = AcpLocalFacade()
|
||||
context.wired_facade = A2aLocalFacade()
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with a raising SessionService for not-found")
|
||||
@given(r"a wired A2aLocalFacade with a raising SessionService for not-found")
|
||||
def step_wired_facade_not_found(context: Context) -> None:
|
||||
svc = MagicMock()
|
||||
svc.delete.side_effect = ResourceNotFoundError(
|
||||
resource_type="session", resource_id="nonexistent"
|
||||
)
|
||||
context.wired_facade = AcpLocalFacade(services={"session_service": svc})
|
||||
context.wired_facade = A2aLocalFacade(services={"session_service": svc})
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with a raising service for validation-error")
|
||||
@given(r"a wired A2aLocalFacade with a raising service for validation-error")
|
||||
def step_wired_facade_validation_error(context: Context) -> None:
|
||||
svc = MagicMock()
|
||||
svc.use_action.side_effect = ValidationError("Invalid action args")
|
||||
context.wired_facade = AcpLocalFacade(services={"plan_lifecycle_service": svc})
|
||||
context.wired_facade = A2aLocalFacade(services={"plan_lifecycle_service": svc})
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with a raising service for plan-error")
|
||||
@given(r"a wired A2aLocalFacade with a raising service for plan-error")
|
||||
def step_wired_facade_plan_error(context: Context) -> None:
|
||||
svc = MagicMock()
|
||||
svc.execute_plan.side_effect = PlanError("Plan execution failed")
|
||||
context.wired_facade = AcpLocalFacade(services={"plan_lifecycle_service": svc})
|
||||
context.wired_facade = A2aLocalFacade(services={"plan_lifecycle_service": svc})
|
||||
|
||||
|
||||
@given(r"a wired AcpLocalFacade with a raising service for invalid-state")
|
||||
@given(r"a wired A2aLocalFacade with a raising service for invalid-state")
|
||||
def step_wired_facade_invalid_state(context: Context) -> None:
|
||||
svc = MagicMock()
|
||||
svc.apply_plan.side_effect = BusinessRuleViolation("Cannot apply in current state")
|
||||
context.wired_facade = AcpLocalFacade(services={"plan_lifecycle_service": svc})
|
||||
context.wired_facade = A2aLocalFacade(services={"plan_lifecycle_service": svc})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -193,7 +193,7 @@ def step_wired_facade_invalid_state(context: Context) -> None:
|
||||
)
|
||||
def step_dispatch_wired(context: Context, operation: str, params_json: str) -> None:
|
||||
params: dict[str, Any] = json.loads(params_json)
|
||||
request = AcpRequest(operation=operation, params=params)
|
||||
request = A2aRequest(operation=operation, params=params)
|
||||
context.wired_response = context.wired_facade.dispatch(request)
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.acp.facade import AcpLocalFacade
|
||||
from cleveragents.acp.models import AcpRequest
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
from cleveragents.a2a.models import A2aRequest
|
||||
from cleveragents.application.services.cleanup_service import CleanupService
|
||||
from cleveragents.cli.commands.resource import app as resource_app
|
||||
from cleveragents.core.exceptions import NotFoundError
|
||||
@@ -252,7 +252,7 @@ def step_create_active_container_no_docker_with_session(
|
||||
|
||||
@given("a facade with no session service")
|
||||
def step_create_facade_no_session(context: Context) -> None:
|
||||
context.facade = AcpLocalFacade(services={})
|
||||
context.facade = A2aLocalFacade(services={})
|
||||
|
||||
|
||||
# ── When steps ───────────────────────────────────────────────
|
||||
@@ -348,7 +348,7 @@ def step_list_for_empty_session(context: Context) -> None:
|
||||
|
||||
@when('I close session "{session_id}" via the facade')
|
||||
def step_close_session_via_facade(context: Context, session_id: str) -> None:
|
||||
request = AcpRequest(
|
||||
request = A2aRequest(
|
||||
operation="session.close",
|
||||
params={"session_id": session_id},
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Step definitions for LSP server stub BDD tests.
|
||||
|
||||
Covers the JSON-RPC transport, protocol handshake, error handling,
|
||||
ACP facade wiring, and CLI serve command for the LSP server stub.
|
||||
A2A facade wiring, and CLI serve command for the LSP server stub.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -15,7 +15,7 @@ from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.acp.facade import AcpLocalFacade
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
from cleveragents.cli.commands.lsp import app as lsp_app
|
||||
from cleveragents.lsp.server import (
|
||||
MAX_CONTENT_LENGTH,
|
||||
@@ -38,7 +38,7 @@ def step_mock_transport(context: Context) -> None:
|
||||
context.lsp_server: LspServer | None = None
|
||||
context.lsp_responses: list[dict[str, Any]] = []
|
||||
context.lsp_exit_code: int | None = None
|
||||
context.lsp_facade: AcpLocalFacade | None = None
|
||||
context.lsp_facade: A2aLocalFacade | None = None
|
||||
context.lsp_error: Exception | None = None
|
||||
|
||||
|
||||
@@ -108,9 +108,9 @@ def step_missing_method(context: Context, req_id: int) -> None:
|
||||
context.lsp_transport.send_raw(framed)
|
||||
|
||||
|
||||
@given("an LSP server with ACP facade")
|
||||
@given("an LSP server with A2A facade")
|
||||
def step_server_with_facade(context: Context) -> None:
|
||||
context.lsp_facade = AcpLocalFacade()
|
||||
context.lsp_facade = A2aLocalFacade()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -349,12 +349,12 @@ def step_error_no_data(context: Context, req_id: int) -> None:
|
||||
)
|
||||
|
||||
|
||||
@then("the server should hold the provided ACP facade")
|
||||
@then("the server should hold the provided A2A facade")
|
||||
def step_server_holds_facade(context: Context) -> None:
|
||||
assert context.lsp_server is not None, "No server was created"
|
||||
assert context.lsp_facade is not None, "No facade was provided"
|
||||
assert context.lsp_server.facade is context.lsp_facade, (
|
||||
"Server does not hold the provided ACP facade instance"
|
||||
"Server does not hold the provided A2A facade instance"
|
||||
)
|
||||
|
||||
|
||||
@@ -364,10 +364,10 @@ def step_server_facade_lazy(context: Context) -> None:
|
||||
# Access the public property — this triggers lazy creation
|
||||
facade = context.lsp_server.facade
|
||||
assert facade is not None, "Facade was not lazily created"
|
||||
from cleveragents.acp.facade import AcpLocalFacade
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
|
||||
assert isinstance(facade, AcpLocalFacade), (
|
||||
f"Expected AcpLocalFacade, got {type(facade).__name__}"
|
||||
assert isinstance(facade, A2aLocalFacade), (
|
||||
f"Expected A2aLocalFacade, got {type(facade).__name__}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Step definitions for M6 ACP facade, event queue, transport, and version tests.
|
||||
"""Step definitions for M6 A2A facade, event queue, transport, and version tests.
|
||||
|
||||
Split from ``m6_autonomy_acceptance_steps.py`` to stay under the project's
|
||||
500-line guideline. All step names keep the ``m6 smoke`` prefix to avoid
|
||||
@@ -14,21 +14,21 @@ from unittest.mock import MagicMock
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.acp.errors import (
|
||||
AcpNotAvailableError,
|
||||
AcpOperationNotFoundError,
|
||||
AcpVersionMismatchError,
|
||||
from cleveragents.a2a.errors import (
|
||||
A2aNotAvailableError,
|
||||
A2aOperationNotFoundError,
|
||||
A2aVersionMismatchError,
|
||||
)
|
||||
from cleveragents.acp.events import AcpEventQueue
|
||||
from cleveragents.acp.facade import AcpLocalFacade
|
||||
from cleveragents.acp.models import (
|
||||
AcpErrorDetail,
|
||||
AcpEvent,
|
||||
AcpRequest,
|
||||
AcpResponse,
|
||||
from cleveragents.a2a.events import A2aEventQueue
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
from cleveragents.a2a.models import (
|
||||
A2aErrorDetail,
|
||||
A2aEvent,
|
||||
A2aRequest,
|
||||
A2aResponse,
|
||||
)
|
||||
from cleveragents.acp.transport import AcpHttpTransport
|
||||
from cleveragents.acp.versioning import AcpVersionNegotiator
|
||||
from cleveragents.a2a.transport import A2aHttpTransport
|
||||
from cleveragents.a2a.versioning import A2aVersionNegotiator
|
||||
|
||||
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "m6"
|
||||
|
||||
@@ -56,20 +56,20 @@ def step_m6_smoke_runner(context: Context) -> None:
|
||||
context.m6_profiles_list = []
|
||||
|
||||
|
||||
@given("a m6 smoke ACP local facade")
|
||||
@given("a m6 smoke A2A local facade")
|
||||
def step_m6_smoke_facade(context: Context) -> None:
|
||||
"""Create an AcpLocalFacade for testing."""
|
||||
context.m6_facade = AcpLocalFacade()
|
||||
"""Create an A2aLocalFacade for testing."""
|
||||
context.m6_facade = A2aLocalFacade()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Fixture loading — ACP facade flows
|
||||
# Fixture loading — A2A facade flows
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I m6 smoke load the ACP facade flows fixture")
|
||||
@when("I m6 smoke load the A2A facade flows fixture")
|
||||
def step_m6_smoke_load_facade_fixture(context: Context) -> None:
|
||||
with open(_FIXTURES_DIR / "acp_facade_flows.json") as f:
|
||||
with open(_FIXTURES_DIR / "a2a_facade_flows.json") as f:
|
||||
context.m6_fixture_data = json.load(f)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ def step_m6_smoke_facade_plan(context: Context) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ACP facade dispatch operations
|
||||
# A2A facade dispatch operations
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ def step_m6_smoke_dispatch(
|
||||
params_json: str,
|
||||
) -> None:
|
||||
params = json.loads(params_json)
|
||||
request = AcpRequest(operation=operation, params=params)
|
||||
request = A2aRequest(operation=operation, params=params)
|
||||
context.m6_response = context.m6_facade.dispatch(request)
|
||||
|
||||
|
||||
@@ -124,26 +124,26 @@ def step_m6_smoke_response_value(
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ACP facade error handling
|
||||
# A2A facade error handling
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I m6 smoke dispatch unknown operation "{operation}"')
|
||||
def step_m6_smoke_dispatch_unknown(context: Context, operation: str) -> None:
|
||||
request = AcpRequest(operation=operation, params={})
|
||||
request = A2aRequest(operation=operation, params={})
|
||||
try:
|
||||
context.m6_facade.dispatch(request)
|
||||
context.m6_error = None
|
||||
except AcpOperationNotFoundError as exc:
|
||||
except A2aOperationNotFoundError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
|
||||
@then("the m6 smoke facade should raise AcpOperationNotFoundError")
|
||||
@then("the m6 smoke facade should raise A2aOperationNotFoundError")
|
||||
def step_m6_smoke_error_op_not_found(context: Context) -> None:
|
||||
assert isinstance(context.m6_error, AcpOperationNotFoundError)
|
||||
assert isinstance(context.m6_error, A2aOperationNotFoundError)
|
||||
|
||||
|
||||
@when("I m6 smoke dispatch with a non-AcpRequest object")
|
||||
@when("I m6 smoke dispatch with a non-A2aRequest object")
|
||||
def step_m6_smoke_dispatch_invalid(context: Context) -> None:
|
||||
try:
|
||||
context.m6_facade.dispatch("not a request") # type: ignore[arg-type]
|
||||
@@ -158,7 +158,7 @@ def step_m6_smoke_error_type(context: Context) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ACP facade service registration and list operations
|
||||
# A2A facade service registration and list operations
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -189,19 +189,19 @@ def step_m6_smoke_ops_count(context: Context, count: int) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ACP event queue
|
||||
# A2A event queue
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a m6 smoke ACP event queue")
|
||||
@given("a m6 smoke A2A event queue")
|
||||
def step_m6_smoke_event_queue(context: Context) -> None:
|
||||
context.m6_event_queue = AcpEventQueue()
|
||||
context.m6_event_queue = A2aEventQueue()
|
||||
context.m6_callback_calls = []
|
||||
|
||||
|
||||
@when('I m6 smoke publish an event with type "{event_type}"')
|
||||
def step_m6_smoke_publish_event(context: Context, event_type: str) -> None:
|
||||
event = AcpEvent(event_type=event_type, data={"test": True})
|
||||
event = A2aEvent(event_type=event_type, data={"test": True})
|
||||
context.m6_event_queue.publish(event)
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ def step_m6_smoke_last_event_type(context: Context, event_type: str) -> None:
|
||||
|
||||
@when("I m6 smoke subscribe a local callback")
|
||||
def step_m6_smoke_subscribe_local(context: Context) -> None:
|
||||
def _callback(event: AcpEvent) -> None:
|
||||
def _callback(event: A2aEvent) -> None:
|
||||
context.m6_callback_calls.append(event)
|
||||
|
||||
context.m6_subscription_id = context.m6_event_queue.subscribe_local(_callback)
|
||||
@@ -247,7 +247,7 @@ def step_m6_smoke_close_queue(context: Context) -> None:
|
||||
|
||||
@then("the m6 smoke publishing should raise RuntimeError")
|
||||
def step_m6_smoke_publish_after_close(context: Context) -> None:
|
||||
event = AcpEvent(event_type="after.close", data={})
|
||||
event = A2aEvent(event_type="after.close", data={})
|
||||
try:
|
||||
context.m6_event_queue.publish(event)
|
||||
raise AssertionError("Expected RuntimeError")
|
||||
@@ -260,54 +260,54 @@ def step_m6_smoke_remote_subscribe(context: Context, endpoint: str) -> None:
|
||||
try:
|
||||
context.m6_event_queue.subscribe_remote(endpoint)
|
||||
context.m6_error = None
|
||||
except AcpNotAvailableError as exc:
|
||||
except A2aNotAvailableError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
|
||||
@then("the m6 smoke facade should raise AcpNotAvailableError")
|
||||
@then("the m6 smoke facade should raise A2aNotAvailableError")
|
||||
def step_m6_smoke_error_not_available(context: Context) -> None:
|
||||
assert isinstance(context.m6_error, AcpNotAvailableError)
|
||||
assert isinstance(context.m6_error, A2aNotAvailableError)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ACP HTTP transport stub
|
||||
# A2A HTTP transport stub
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I m6 smoke attempt transport send")
|
||||
def step_m6_smoke_transport_send(context: Context) -> None:
|
||||
transport = AcpHttpTransport()
|
||||
request = AcpRequest(operation="plan.create", params={})
|
||||
transport = A2aHttpTransport()
|
||||
request = A2aRequest(operation="plan.create", params={})
|
||||
try:
|
||||
transport.send(request)
|
||||
context.m6_error = None
|
||||
except AcpNotAvailableError as exc:
|
||||
except A2aNotAvailableError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
|
||||
@when('I m6 smoke attempt transport connect to "{url}"')
|
||||
def step_m6_smoke_transport_connect(context: Context, url: str) -> None:
|
||||
transport = AcpHttpTransport()
|
||||
transport = A2aHttpTransport()
|
||||
try:
|
||||
transport.connect(url)
|
||||
context.m6_error = None
|
||||
except AcpNotAvailableError as exc:
|
||||
except A2aNotAvailableError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
|
||||
@when("I m6 smoke attempt transport disconnect")
|
||||
def step_m6_smoke_transport_disconnect(context: Context) -> None:
|
||||
transport = AcpHttpTransport()
|
||||
transport = A2aHttpTransport()
|
||||
try:
|
||||
transport.disconnect()
|
||||
context.m6_error = None
|
||||
except AcpNotAvailableError as exc:
|
||||
except A2aNotAvailableError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
|
||||
@when("I m6 smoke check transport is_connected")
|
||||
def step_m6_smoke_transport_connected(context: Context) -> None:
|
||||
transport = AcpHttpTransport()
|
||||
transport = A2aHttpTransport()
|
||||
context.m6_transport_connected = transport.is_connected()
|
||||
|
||||
|
||||
@@ -317,17 +317,17 @@ def step_m6_smoke_transport_not_connected(context: Context) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ACP version negotiation
|
||||
# A2A version negotiation
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I m6 smoke negotiate ACP version "{version}"')
|
||||
@when('I m6 smoke negotiate A2A version "{version}"')
|
||||
def step_m6_smoke_negotiate(context: Context, version: str) -> None:
|
||||
negotiator = AcpVersionNegotiator()
|
||||
negotiator = A2aVersionNegotiator()
|
||||
try:
|
||||
context.m6_version_result = negotiator.negotiate(version)
|
||||
context.m6_error = None
|
||||
except AcpVersionMismatchError as exc:
|
||||
except A2aVersionMismatchError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
|
||||
@@ -336,14 +336,14 @@ def step_m6_smoke_negotiated(context: Context, version: str) -> None:
|
||||
assert context.m6_version_result == version
|
||||
|
||||
|
||||
@then("the m6 smoke facade should raise AcpVersionMismatchError")
|
||||
@then("the m6 smoke facade should raise A2aVersionMismatchError")
|
||||
def step_m6_smoke_error_version(context: Context) -> None:
|
||||
assert isinstance(context.m6_error, AcpVersionMismatchError)
|
||||
assert isinstance(context.m6_error, A2aVersionMismatchError)
|
||||
|
||||
|
||||
@when('I m6 smoke check if version "{version}" is supported')
|
||||
def step_m6_smoke_version_supported(context: Context, version: str) -> None:
|
||||
negotiator = AcpVersionNegotiator()
|
||||
negotiator = A2aVersionNegotiator()
|
||||
context.m6_version_supported = negotiator.is_supported(version)
|
||||
|
||||
|
||||
@@ -358,41 +358,41 @@ def step_m6_smoke_version_false(context: Context) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ACP model validation
|
||||
# A2A model validation
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I m6 smoke create AcpRequest with empty operation")
|
||||
@when("I m6 smoke create A2aRequest with empty operation")
|
||||
def step_m6_smoke_invalid_request(context: Context) -> None:
|
||||
try:
|
||||
AcpRequest(operation="")
|
||||
A2aRequest(operation="")
|
||||
context.m6_error = None
|
||||
except ValueError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
|
||||
@when('I m6 smoke create AcpResponse with invalid status "{status}"')
|
||||
@when('I m6 smoke create A2aResponse with invalid status "{status}"')
|
||||
def step_m6_smoke_invalid_response(context: Context, status: str) -> None:
|
||||
try:
|
||||
AcpResponse(request_id="test", status=status)
|
||||
A2aResponse(request_id="test", status=status)
|
||||
context.m6_error = None
|
||||
except ValueError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
|
||||
@when("I m6 smoke create AcpEvent with empty event_type")
|
||||
@when("I m6 smoke create A2aEvent with empty event_type")
|
||||
def step_m6_smoke_invalid_event(context: Context) -> None:
|
||||
try:
|
||||
AcpEvent(event_type="")
|
||||
A2aEvent(event_type="")
|
||||
context.m6_error = None
|
||||
except ValueError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
|
||||
@when("I m6 smoke create AcpErrorDetail with empty code")
|
||||
@when("I m6 smoke create A2aErrorDetail with empty code")
|
||||
def step_m6_smoke_invalid_error_detail(context: Context) -> None:
|
||||
try:
|
||||
AcpErrorDetail(code="", message="test")
|
||||
A2aErrorDetail(code="", message="test")
|
||||
context.m6_error = None
|
||||
except ValueError as exc:
|
||||
context.m6_error = exc
|
||||
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.acp.events import AcpEventQueue
|
||||
from cleveragents.a2a.events import A2aEventQueue
|
||||
from cleveragents.core.async_cleanup import AsyncResourceTracker
|
||||
from cleveragents.langgraph.state import GraphState, StateManager
|
||||
|
||||
@@ -132,9 +132,9 @@ def step_create_state_manager(context):
|
||||
)
|
||||
|
||||
|
||||
@given("I have an ACP event queue with {count:d} active subscriptions")
|
||||
@given("I have an A2A event queue with {count:d} active subscriptions")
|
||||
def step_create_event_queue(context, count):
|
||||
context.event_queue = AcpEventQueue()
|
||||
context.event_queue = A2aEventQueue()
|
||||
context.subscription_ids = []
|
||||
for _ in range(count):
|
||||
sub_id = context.event_queue.subscribe_local(lambda _evt: None)
|
||||
@@ -483,10 +483,10 @@ def step_try_reset_after_close(context):
|
||||
|
||||
@when("I try to publish an event after close")
|
||||
def step_try_publish_after_close(context):
|
||||
from cleveragents.acp.models import AcpEvent
|
||||
from cleveragents.a2a.models import A2aEvent
|
||||
|
||||
try:
|
||||
context.event_queue.publish(AcpEvent(event_type="test", data={}))
|
||||
context.event_queue.publish(A2aEvent(event_type="test", data={}))
|
||||
context.runtime_error = None
|
||||
except RuntimeError as exc:
|
||||
context.runtime_error = exc
|
||||
|
||||
@@ -9,7 +9,7 @@ from behave import given, then, use_step_matcher, when
|
||||
from behave.runner import Context
|
||||
from pydantic import ValidationError
|
||||
|
||||
from cleveragents.acp.clients import (
|
||||
from cleveragents.a2a.clients import (
|
||||
AuthClient,
|
||||
RemoteExecutionClient,
|
||||
ServerClient,
|
||||
@@ -17,7 +17,7 @@ from cleveragents.acp.clients import (
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig
|
||||
from cleveragents.a2a.server_config import ServerConnectionConfig
|
||||
from cleveragents.application.services.config_service import _REGISTRY
|
||||
|
||||
use_step_matcher("re")
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
*** Settings ***
|
||||
Documentation Smoke tests for ACP local facade and server stubs
|
||||
Documentation Smoke tests for A2A local facade and server stubs
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_acp_facade.py
|
||||
${HELPER} ${CURDIR}/helper_a2a_facade.py
|
||||
|
||||
*** Test Cases ***
|
||||
ACP Local Facade Dispatch Session Create
|
||||
A2A Local Facade Dispatch Session Create
|
||||
[Documentation] Verify local facade dispatches session.create successfully
|
||||
${result}= Run Process ${PYTHON} ${HELPER} facade-dispatch cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-facade-dispatch-ok
|
||||
Should Contain ${result.stdout} a2a-facade-dispatch-ok
|
||||
|
||||
ACP HTTP Transport Stub Error
|
||||
[Documentation] Verify HTTP transport raises AcpNotAvailableError
|
||||
A2A HTTP Transport Stub Error
|
||||
[Documentation] Verify HTTP transport raises A2aNotAvailableError
|
||||
${result}= Run Process ${PYTHON} ${HELPER} transport-stub cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-transport-stub-ok
|
||||
Should Contain ${result.stdout} a2a-transport-stub-ok
|
||||
|
||||
ACP Event Queue Local Mode
|
||||
A2A Event Queue Local Mode
|
||||
[Documentation] Verify event queue publish and subscribe work locally
|
||||
${result}= Run Process ${PYTHON} ${HELPER} event-queue cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-event-queue-ok
|
||||
Should Contain ${result.stdout} a2a-event-queue-ok
|
||||
|
||||
ACP Version Negotiation
|
||||
A2A Version Negotiation
|
||||
[Documentation] Verify version negotiation succeeds for 1.0 and fails for 2.0
|
||||
${result}= Run Process ${PYTHON} ${HELPER} version-negotiate cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-version-negotiate-ok
|
||||
Should Contain ${result.stdout} a2a-version-negotiate-ok
|
||||
|
||||
ACP Operation Listing
|
||||
A2A Operation Listing
|
||||
[Documentation] Verify list_operations returns all expected operations
|
||||
${result}= Run Process ${PYTHON} ${HELPER} list-operations cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-list-operations-ok
|
||||
Should Contain ${result.stdout} a2a-list-operations-ok
|
||||
@@ -1,11 +1,11 @@
|
||||
*** Settings ***
|
||||
Documentation Integration tests for ACP local facade wiring to live services
|
||||
Documentation Integration tests for A2A local facade wiring to live services
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_acp_facade_wiring.py
|
||||
${HELPER} ${CURDIR}/helper_a2a_facade_wiring.py
|
||||
|
||||
*** Test Cases ***
|
||||
Wired Facade Session Create
|
||||
@@ -14,7 +14,7 @@ Wired Facade Session Create
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-wired-session-create-ok
|
||||
Should Contain ${result.stdout} a2a-wired-session-create-ok
|
||||
|
||||
Wired Facade Session Close
|
||||
[Documentation] Verify session.close routes to SessionService
|
||||
@@ -22,7 +22,7 @@ Wired Facade Session Close
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-wired-session-close-ok
|
||||
Should Contain ${result.stdout} a2a-wired-session-close-ok
|
||||
|
||||
Wired Facade Plan Create
|
||||
[Documentation] Verify plan.create routes to PlanLifecycleService
|
||||
@@ -30,7 +30,7 @@ Wired Facade Plan Create
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-wired-plan-create-ok
|
||||
Should Contain ${result.stdout} a2a-wired-plan-create-ok
|
||||
|
||||
Wired Facade Plan Status
|
||||
[Documentation] Verify plan.status routes to PlanLifecycleService
|
||||
@@ -38,7 +38,7 @@ Wired Facade Plan Status
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-wired-plan-status-ok
|
||||
Should Contain ${result.stdout} a2a-wired-plan-status-ok
|
||||
|
||||
Wired Facade Registry Tools
|
||||
[Documentation] Verify registry.list_tools routes to ToolRegistry
|
||||
@@ -46,7 +46,7 @@ Wired Facade Registry Tools
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-wired-registry-tools-ok
|
||||
Should Contain ${result.stdout} a2a-wired-registry-tools-ok
|
||||
|
||||
Wired Facade Registry Resources
|
||||
[Documentation] Verify registry.list_resources routes to ResourceRegistryService
|
||||
@@ -54,7 +54,7 @@ Wired Facade Registry Resources
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-wired-registry-resources-ok
|
||||
Should Contain ${result.stdout} a2a-wired-registry-resources-ok
|
||||
|
||||
Wired Facade Context Stub
|
||||
[Documentation] Verify context.get returns stub while ACMS pipeline is pending
|
||||
@@ -62,20 +62,20 @@ Wired Facade Context Stub
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-wired-context-stub-ok
|
||||
Should Contain ${result.stdout} a2a-wired-context-stub-ok
|
||||
|
||||
Wired Facade Event Subscribe
|
||||
[Documentation] Verify event.subscribe routes to AcpEventQueue
|
||||
[Documentation] Verify event.subscribe routes to A2aEventQueue
|
||||
${result}= Run Process ${PYTHON} ${HELPER} wired-event-subscribe cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-wired-event-subscribe-ok
|
||||
Should Contain ${result.stdout} a2a-wired-event-subscribe-ok
|
||||
|
||||
Wired Facade Error Mapping
|
||||
[Documentation] Verify domain exceptions map to correct ACP error codes
|
||||
[Documentation] Verify domain exceptions map to correct A2A error codes
|
||||
${result}= Run Process ${PYTHON} ${HELPER} wired-error-mapping cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} acp-wired-error-mapping-ok
|
||||
Should Contain ${result.stdout} a2a-wired-error-mapping-ok
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Helper script for acp_facade.robot smoke tests.
|
||||
"""Helper script for a2a_facade.robot smoke tests.
|
||||
|
||||
Each subcommand is a self-contained check that prints a sentinel on success.
|
||||
"""
|
||||
@@ -13,15 +13,15 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from cleveragents.acp.errors import ( # noqa: E402
|
||||
AcpNotAvailableError,
|
||||
AcpVersionMismatchError,
|
||||
from cleveragents.a2a.errors import ( # noqa: E402
|
||||
A2aNotAvailableError,
|
||||
A2aVersionMismatchError,
|
||||
)
|
||||
from cleveragents.acp.events import AcpEventQueue # noqa: E402
|
||||
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
|
||||
from cleveragents.acp.models import AcpEvent, AcpRequest # noqa: E402
|
||||
from cleveragents.acp.transport import AcpHttpTransport # noqa: E402
|
||||
from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
|
||||
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
|
||||
from cleveragents.a2a.transport import A2aHttpTransport # noqa: E402
|
||||
from cleveragents.a2a.versioning import A2aVersionNegotiator # noqa: E402
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommands
|
||||
@@ -30,28 +30,28 @@ from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
|
||||
|
||||
def facade_dispatch() -> None:
|
||||
"""Dispatch session.create via local facade."""
|
||||
facade = AcpLocalFacade()
|
||||
request = AcpRequest(operation="session.create")
|
||||
facade = A2aLocalFacade()
|
||||
request = A2aRequest(operation="session.create")
|
||||
response = facade.dispatch(request)
|
||||
if response.status == "ok" and "session_id" in response.data:
|
||||
print("acp-facade-dispatch-ok")
|
||||
print("a2a-facade-dispatch-ok")
|
||||
else:
|
||||
print(f"FAIL: unexpected response {response}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def transport_stub() -> None:
|
||||
"""Verify transport stub raises AcpNotAvailableError."""
|
||||
transport = AcpHttpTransport()
|
||||
"""Verify transport stub raises A2aNotAvailableError."""
|
||||
transport = A2aHttpTransport()
|
||||
try:
|
||||
transport.connect("http://localhost:8080")
|
||||
print("FAIL: should have raised", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except AcpNotAvailableError:
|
||||
except A2aNotAvailableError:
|
||||
pass
|
||||
|
||||
if transport.is_connected() is False:
|
||||
print("acp-transport-stub-ok")
|
||||
print("a2a-transport-stub-ok")
|
||||
else:
|
||||
print("FAIL: is_connected should be False", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -59,12 +59,12 @@ def transport_stub() -> None:
|
||||
|
||||
def event_queue() -> None:
|
||||
"""Verify local event queue publish/subscribe."""
|
||||
queue = AcpEventQueue()
|
||||
received: list[AcpEvent] = []
|
||||
queue = A2aEventQueue()
|
||||
received: list[A2aEvent] = []
|
||||
queue.subscribe_local(lambda e: received.append(e))
|
||||
queue.publish(AcpEvent(event_type="test.event"))
|
||||
queue.publish(A2aEvent(event_type="test.event"))
|
||||
if len(received) == 1 and received[0].event_type == "test.event":
|
||||
print("acp-event-queue-ok")
|
||||
print("a2a-event-queue-ok")
|
||||
else:
|
||||
print(f"FAIL: received={received}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -72,7 +72,7 @@ def event_queue() -> None:
|
||||
|
||||
def version_negotiate() -> None:
|
||||
"""Verify version negotiation."""
|
||||
negotiator = AcpVersionNegotiator()
|
||||
negotiator = A2aVersionNegotiator()
|
||||
result = negotiator.negotiate("1.0")
|
||||
if result != "1.0":
|
||||
print("FAIL: expected 1.0", file=sys.stderr)
|
||||
@@ -82,19 +82,19 @@ def version_negotiate() -> None:
|
||||
negotiator.negotiate("2.0")
|
||||
print("FAIL: should have raised", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except AcpVersionMismatchError:
|
||||
except A2aVersionMismatchError:
|
||||
pass
|
||||
|
||||
print("acp-version-negotiate-ok")
|
||||
print("a2a-version-negotiate-ok")
|
||||
|
||||
|
||||
def list_operations() -> None:
|
||||
"""Verify list_operations returns expected operations."""
|
||||
facade = AcpLocalFacade()
|
||||
facade = A2aLocalFacade()
|
||||
ops = facade.list_operations()
|
||||
expected = {"session.create", "plan.create", "plan.execute", "context.get"}
|
||||
if expected.issubset(set(ops)) and len(ops) == 11:
|
||||
print("acp-list-operations-ok")
|
||||
print("a2a-list-operations-ok")
|
||||
else:
|
||||
print(f"FAIL: ops={ops}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Helper script for acp_facade_wiring.robot integration tests.
|
||||
"""Helper script for a2a_facade_wiring.robot integration tests.
|
||||
|
||||
Each subcommand exercises the wired ACP facade with lightweight mock
|
||||
Each subcommand exercises the wired A2A facade with lightweight mock
|
||||
services and prints a sentinel on success.
|
||||
"""
|
||||
|
||||
@@ -15,12 +15,12 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from cleveragents.acp.errors import ( # noqa: E402
|
||||
from cleveragents.a2a.errors import ( # noqa: E402
|
||||
map_domain_error,
|
||||
)
|
||||
from cleveragents.acp.events import AcpEventQueue # noqa: E402
|
||||
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
|
||||
from cleveragents.acp.models import AcpRequest # noqa: E402
|
||||
from cleveragents.a2a.events import A2aEventQueue # noqa: E402
|
||||
from cleveragents.a2a.facade import A2aLocalFacade # noqa: E402
|
||||
from cleveragents.a2a.models import A2aRequest # noqa: E402
|
||||
from cleveragents.core.exceptions import ( # noqa: E402
|
||||
BusinessRuleViolation,
|
||||
PlanError,
|
||||
@@ -102,10 +102,10 @@ def _mock_resource_registry_service() -> MagicMock:
|
||||
|
||||
def wired_session_create() -> None:
|
||||
"""Dispatch session.create through a wired facade."""
|
||||
facade = AcpLocalFacade(services={"session_service": _mock_session_service()})
|
||||
resp = facade.dispatch(AcpRequest(operation="session.create"))
|
||||
facade = A2aLocalFacade(services={"session_service": _mock_session_service()})
|
||||
resp = facade.dispatch(A2aRequest(operation="session.create"))
|
||||
if resp.status == "ok" and resp.data["session_id"] == "INTEG-SESSION-001":
|
||||
print("acp-wired-session-create-ok")
|
||||
print("a2a-wired-session-create-ok")
|
||||
else:
|
||||
print(f"FAIL: {resp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -113,15 +113,15 @@ def wired_session_create() -> None:
|
||||
|
||||
def wired_session_close() -> None:
|
||||
"""Dispatch session.close through a wired facade."""
|
||||
facade = AcpLocalFacade(services={"session_service": _mock_session_service()})
|
||||
facade = A2aLocalFacade(services={"session_service": _mock_session_service()})
|
||||
resp = facade.dispatch(
|
||||
AcpRequest(
|
||||
A2aRequest(
|
||||
operation="session.close",
|
||||
params={"session_id": "INTEG-SESSION-001"},
|
||||
)
|
||||
)
|
||||
if resp.status == "ok" and resp.data["status"] == "closed":
|
||||
print("acp-wired-session-close-ok")
|
||||
print("a2a-wired-session-close-ok")
|
||||
else:
|
||||
print(f"FAIL: {resp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -129,17 +129,17 @@ def wired_session_close() -> None:
|
||||
|
||||
def wired_plan_create() -> None:
|
||||
"""Dispatch plan.create through a wired facade."""
|
||||
facade = AcpLocalFacade(
|
||||
facade = A2aLocalFacade(
|
||||
services={"plan_lifecycle_service": _mock_plan_lifecycle_service()}
|
||||
)
|
||||
resp = facade.dispatch(
|
||||
AcpRequest(
|
||||
A2aRequest(
|
||||
operation="plan.create",
|
||||
params={"action_name": "local/test"},
|
||||
)
|
||||
)
|
||||
if resp.status == "ok" and resp.data["plan_id"] == "INTEG-PLAN-001":
|
||||
print("acp-wired-plan-create-ok")
|
||||
print("a2a-wired-plan-create-ok")
|
||||
else:
|
||||
print(f"FAIL: {resp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -147,17 +147,17 @@ def wired_plan_create() -> None:
|
||||
|
||||
def wired_plan_status() -> None:
|
||||
"""Dispatch plan.status through a wired facade."""
|
||||
facade = AcpLocalFacade(
|
||||
facade = A2aLocalFacade(
|
||||
services={"plan_lifecycle_service": _mock_plan_lifecycle_service()}
|
||||
)
|
||||
resp = facade.dispatch(
|
||||
AcpRequest(
|
||||
A2aRequest(
|
||||
operation="plan.status",
|
||||
params={"plan_id": "INTEG-PLAN-001"},
|
||||
)
|
||||
)
|
||||
if resp.status == "ok" and resp.data["phase"] == "strategize":
|
||||
print("acp-wired-plan-status-ok")
|
||||
print("a2a-wired-plan-status-ok")
|
||||
else:
|
||||
print(f"FAIL: {resp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -165,11 +165,11 @@ def wired_plan_status() -> None:
|
||||
|
||||
def wired_registry_tools() -> None:
|
||||
"""Dispatch registry.list_tools through a wired facade."""
|
||||
facade = AcpLocalFacade(services={"tool_registry": _mock_tool_registry()})
|
||||
resp = facade.dispatch(AcpRequest(operation="registry.list_tools"))
|
||||
facade = A2aLocalFacade(services={"tool_registry": _mock_tool_registry()})
|
||||
resp = facade.dispatch(A2aRequest(operation="registry.list_tools"))
|
||||
tools = resp.data.get("tools", [])
|
||||
if resp.status == "ok" and len(tools) == 1:
|
||||
print("acp-wired-registry-tools-ok")
|
||||
print("a2a-wired-registry-tools-ok")
|
||||
else:
|
||||
print(f"FAIL: {resp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -177,13 +177,13 @@ def wired_registry_tools() -> None:
|
||||
|
||||
def wired_registry_resources() -> None:
|
||||
"""Dispatch registry.list_resources through a wired facade."""
|
||||
facade = AcpLocalFacade(
|
||||
facade = A2aLocalFacade(
|
||||
services={"resource_registry_service": _mock_resource_registry_service()}
|
||||
)
|
||||
resp = facade.dispatch(AcpRequest(operation="registry.list_resources"))
|
||||
resp = facade.dispatch(A2aRequest(operation="registry.list_resources"))
|
||||
resources = resp.data.get("resources", [])
|
||||
if resp.status == "ok" and len(resources) == 1:
|
||||
print("acp-wired-registry-resources-ok")
|
||||
print("a2a-wired-registry-resources-ok")
|
||||
else:
|
||||
print(f"FAIL: {resp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -191,10 +191,10 @@ def wired_registry_resources() -> None:
|
||||
|
||||
def wired_context_stub() -> None:
|
||||
"""Dispatch context.get and verify stub response."""
|
||||
facade = AcpLocalFacade()
|
||||
resp = facade.dispatch(AcpRequest(operation="context.get"))
|
||||
facade = A2aLocalFacade()
|
||||
resp = facade.dispatch(A2aRequest(operation="context.get"))
|
||||
if resp.status == "ok" and resp.data.get("stub") is True:
|
||||
print("acp-wired-context-stub-ok")
|
||||
print("a2a-wired-context-stub-ok")
|
||||
else:
|
||||
print(f"FAIL: {resp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -202,19 +202,19 @@ def wired_context_stub() -> None:
|
||||
|
||||
def wired_event_subscribe() -> None:
|
||||
"""Dispatch event.subscribe through a wired facade."""
|
||||
queue = AcpEventQueue()
|
||||
facade = AcpLocalFacade(services={"event_queue": queue})
|
||||
resp = facade.dispatch(AcpRequest(operation="event.subscribe"))
|
||||
queue = A2aEventQueue()
|
||||
facade = A2aLocalFacade(services={"event_queue": queue})
|
||||
resp = facade.dispatch(A2aRequest(operation="event.subscribe"))
|
||||
sub_id = resp.data.get("subscription_id", "")
|
||||
if resp.status == "ok" and sub_id:
|
||||
print("acp-wired-event-subscribe-ok")
|
||||
print("a2a-wired-event-subscribe-ok")
|
||||
else:
|
||||
print(f"FAIL: {resp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def wired_error_mapping() -> None:
|
||||
"""Verify domain-to-ACP error code mapping."""
|
||||
"""Verify domain-to-A2A error code mapping."""
|
||||
cases: list[tuple[Exception, str]] = [
|
||||
(ResourceNotFoundError(resource_type="x", resource_id="1"), "NOT_FOUND"),
|
||||
(ValidationError("bad"), "VALIDATION_ERROR"),
|
||||
@@ -229,7 +229,7 @@ def wired_error_mapping() -> None:
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
print("acp-wired-error-mapping-ok")
|
||||
print("a2a-wired-error-mapping-ok")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -14,16 +14,16 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from cleveragents.acp.errors import ( # noqa: E402
|
||||
AcpNotAvailableError,
|
||||
AcpOperationNotFoundError,
|
||||
AcpVersionMismatchError,
|
||||
from cleveragents.a2a.errors import ( # noqa: E402
|
||||
A2aNotAvailableError,
|
||||
A2aOperationNotFoundError,
|
||||
A2aVersionMismatchError,
|
||||
)
|
||||
from cleveragents.acp.events import AcpEventQueue # noqa: E402
|
||||
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
|
||||
from cleveragents.acp.models import AcpEvent, AcpRequest # noqa: E402
|
||||
from cleveragents.acp.transport import AcpHttpTransport # noqa: E402
|
||||
from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
|
||||
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
|
||||
from cleveragents.a2a.transport import A2aHttpTransport # noqa: E402
|
||||
from cleveragents.a2a.versioning import A2aVersionNegotiator # noqa: E402
|
||||
from cleveragents.application.services.automation_profile_service import ( # noqa: E402
|
||||
AutomationProfileService,
|
||||
)
|
||||
@@ -44,13 +44,13 @@ _FIXTURES_DIR = Path(__file__).resolve().parents[1] / "features" / "fixtures" /
|
||||
|
||||
def facade_session() -> None:
|
||||
"""Dispatch session.create and session.close."""
|
||||
facade = AcpLocalFacade()
|
||||
facade = A2aLocalFacade()
|
||||
|
||||
resp_create = facade.dispatch(AcpRequest(operation="session.create", params={}))
|
||||
resp_create = facade.dispatch(A2aRequest(operation="session.create", params={}))
|
||||
assert resp_create.status == "ok", f"Expected ok, got {resp_create.status}"
|
||||
assert "session_id" in resp_create.data
|
||||
|
||||
resp_close = facade.dispatch(AcpRequest(operation="session.close", params={}))
|
||||
resp_close = facade.dispatch(A2aRequest(operation="session.close", params={}))
|
||||
assert resp_close.status == "ok"
|
||||
assert resp_close.data["status"] == "closed"
|
||||
|
||||
@@ -59,33 +59,33 @@ def facade_session() -> None:
|
||||
|
||||
def facade_plan() -> None:
|
||||
"""Dispatch plan create/execute/status/diff/apply."""
|
||||
facade = AcpLocalFacade()
|
||||
facade = A2aLocalFacade()
|
||||
|
||||
resp = facade.dispatch(AcpRequest(operation="plan.create", params={}))
|
||||
resp = facade.dispatch(A2aRequest(operation="plan.create", params={}))
|
||||
assert resp.status == "ok"
|
||||
plan_id = resp.data["plan_id"]
|
||||
assert plan_id
|
||||
|
||||
resp = facade.dispatch(
|
||||
AcpRequest(operation="plan.execute", params={"plan_id": plan_id})
|
||||
A2aRequest(operation="plan.execute", params={"plan_id": plan_id})
|
||||
)
|
||||
assert resp.status == "ok"
|
||||
assert resp.data["status"] == "queued"
|
||||
|
||||
resp = facade.dispatch(
|
||||
AcpRequest(operation="plan.status", params={"plan_id": plan_id})
|
||||
A2aRequest(operation="plan.status", params={"plan_id": plan_id})
|
||||
)
|
||||
assert resp.status == "ok"
|
||||
assert "phase" in resp.data
|
||||
|
||||
resp = facade.dispatch(
|
||||
AcpRequest(operation="plan.diff", params={"plan_id": plan_id})
|
||||
A2aRequest(operation="plan.diff", params={"plan_id": plan_id})
|
||||
)
|
||||
assert resp.status == "ok"
|
||||
assert "changes" in resp.data
|
||||
|
||||
resp = facade.dispatch(
|
||||
AcpRequest(operation="plan.apply", params={"plan_id": plan_id})
|
||||
A2aRequest(operation="plan.apply", params={"plan_id": plan_id})
|
||||
)
|
||||
assert resp.status == "ok"
|
||||
assert resp.data["status"] == "applied"
|
||||
@@ -94,13 +94,13 @@ def facade_plan() -> None:
|
||||
|
||||
|
||||
def facade_unknown_op() -> None:
|
||||
"""Verify unknown operation raises AcpOperationNotFoundError."""
|
||||
facade = AcpLocalFacade()
|
||||
"""Verify unknown operation raises A2aOperationNotFoundError."""
|
||||
facade = A2aLocalFacade()
|
||||
try:
|
||||
facade.dispatch(AcpRequest(operation="nonexistent.op", params={}))
|
||||
print("FAIL: expected AcpOperationNotFoundError")
|
||||
facade.dispatch(A2aRequest(operation="nonexistent.op", params={}))
|
||||
print("FAIL: expected A2aOperationNotFoundError")
|
||||
sys.exit(1)
|
||||
except AcpOperationNotFoundError:
|
||||
except A2aOperationNotFoundError:
|
||||
pass
|
||||
|
||||
print("m6-facade-unknown-op-ok")
|
||||
@@ -108,17 +108,17 @@ def facade_unknown_op() -> None:
|
||||
|
||||
def event_queue() -> None:
|
||||
"""Publish events and verify local subscriber receives them."""
|
||||
queue = AcpEventQueue()
|
||||
queue = A2aEventQueue()
|
||||
received = []
|
||||
|
||||
sub_id = queue.subscribe_local(lambda e: received.append(e))
|
||||
queue.publish(AcpEvent(event_type="plan.progress", data={"step": 1}))
|
||||
queue.publish(A2aEvent(event_type="plan.progress", data={"step": 1}))
|
||||
assert len(received) == 1
|
||||
assert received[0].event_type == "plan.progress"
|
||||
|
||||
# Unsubscribe and verify no more callbacks
|
||||
queue.unsubscribe(sub_id)
|
||||
queue.publish(AcpEvent(event_type="plan.complete", data={}))
|
||||
queue.publish(A2aEvent(event_type="plan.complete", data={}))
|
||||
assert len(received) == 1 # still 1
|
||||
|
||||
# Verify get_events returns both
|
||||
@@ -128,7 +128,7 @@ def event_queue() -> None:
|
||||
# Close and verify publish raises
|
||||
queue.close()
|
||||
try:
|
||||
queue.publish(AcpEvent(event_type="after.close", data={}))
|
||||
queue.publish(A2aEvent(event_type="after.close", data={}))
|
||||
print("FAIL: expected RuntimeError")
|
||||
sys.exit(1)
|
||||
except RuntimeError:
|
||||
@@ -138,31 +138,31 @@ def event_queue() -> None:
|
||||
|
||||
|
||||
def transport_stub() -> None:
|
||||
"""Verify HTTP transport stub raises AcpNotAvailableError."""
|
||||
transport = AcpHttpTransport()
|
||||
"""Verify HTTP transport stub raises A2aNotAvailableError."""
|
||||
transport = A2aHttpTransport()
|
||||
|
||||
# send
|
||||
try:
|
||||
transport.send(AcpRequest(operation="plan.create", params={}))
|
||||
print("FAIL: expected AcpNotAvailableError on send")
|
||||
transport.send(A2aRequest(operation="plan.create", params={}))
|
||||
print("FAIL: expected A2aNotAvailableError on send")
|
||||
sys.exit(1)
|
||||
except AcpNotAvailableError:
|
||||
except A2aNotAvailableError:
|
||||
pass
|
||||
|
||||
# connect
|
||||
try:
|
||||
transport.connect("https://example.com/acp")
|
||||
print("FAIL: expected AcpNotAvailableError on connect")
|
||||
transport.connect("https://example.com/a2a")
|
||||
print("FAIL: expected A2aNotAvailableError on connect")
|
||||
sys.exit(1)
|
||||
except AcpNotAvailableError:
|
||||
except A2aNotAvailableError:
|
||||
pass
|
||||
|
||||
# disconnect
|
||||
try:
|
||||
transport.disconnect()
|
||||
print("FAIL: expected AcpNotAvailableError on disconnect")
|
||||
print("FAIL: expected A2aNotAvailableError on disconnect")
|
||||
sys.exit(1)
|
||||
except AcpNotAvailableError:
|
||||
except A2aNotAvailableError:
|
||||
pass
|
||||
|
||||
# is_connected
|
||||
@@ -172,8 +172,8 @@ def transport_stub() -> None:
|
||||
|
||||
|
||||
def version_negotiation() -> None:
|
||||
"""Negotiate supported and unsupported ACP versions."""
|
||||
negotiator = AcpVersionNegotiator()
|
||||
"""Negotiate supported and unsupported A2A versions."""
|
||||
negotiator = A2aVersionNegotiator()
|
||||
|
||||
# Supported
|
||||
result = negotiator.negotiate("1.0")
|
||||
@@ -184,9 +184,9 @@ def version_negotiation() -> None:
|
||||
# Unsupported
|
||||
try:
|
||||
negotiator.negotiate("2.0")
|
||||
print("FAIL: expected AcpVersionMismatchError")
|
||||
print("FAIL: expected A2aVersionMismatchError")
|
||||
sys.exit(1)
|
||||
except AcpVersionMismatchError:
|
||||
except A2aVersionMismatchError:
|
||||
pass
|
||||
|
||||
assert negotiator.is_supported("99.0") is False
|
||||
@@ -271,7 +271,7 @@ def profile_resolution() -> None:
|
||||
def fixture_loading() -> None:
|
||||
"""Load all M6 fixture files and verify structure."""
|
||||
for fname in (
|
||||
"acp_facade_flows.json",
|
||||
"a2a_facade_flows.json",
|
||||
"autonomy_guardrails.json",
|
||||
"automation_profiles.json",
|
||||
):
|
||||
@@ -286,16 +286,16 @@ def fixture_loading() -> None:
|
||||
def full_flow() -> None:
|
||||
"""End-to-end: facade dispatch + guard check + profile resolution."""
|
||||
# Step 1: Facade dispatch
|
||||
facade = AcpLocalFacade()
|
||||
resp = facade.dispatch(AcpRequest(operation="session.create", params={}))
|
||||
facade = A2aLocalFacade()
|
||||
resp = facade.dispatch(A2aRequest(operation="session.create", params={}))
|
||||
assert resp.status == "ok"
|
||||
|
||||
resp = facade.dispatch(AcpRequest(operation="plan.create", params={}))
|
||||
resp = facade.dispatch(A2aRequest(operation="plan.create", params={}))
|
||||
assert resp.status == "ok"
|
||||
plan_id = resp.data["plan_id"]
|
||||
|
||||
resp = facade.dispatch(
|
||||
AcpRequest(operation="plan.execute", params={"plan_id": plan_id})
|
||||
A2aRequest(operation="plan.execute", params={"plan_id": plan_id})
|
||||
)
|
||||
assert resp.status == "ok"
|
||||
|
||||
@@ -318,12 +318,12 @@ def full_flow() -> None:
|
||||
assert p.name == "ci"
|
||||
|
||||
# Step 4: Version negotiation
|
||||
negotiator = AcpVersionNegotiator()
|
||||
negotiator = A2aVersionNegotiator()
|
||||
assert negotiator.negotiate("1.0") == "1.0"
|
||||
|
||||
# Step 5: Event queue
|
||||
queue = AcpEventQueue()
|
||||
queue.publish(AcpEvent(event_type="flow.complete", data={}))
|
||||
queue = A2aEventQueue()
|
||||
queue.publish(A2aEvent(event_type="flow.complete", data={}))
|
||||
assert len(queue.get_events()) == 1
|
||||
queue.close()
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from cleveragents.acp.clients import ( # noqa: E402
|
||||
from cleveragents.a2a.clients import ( # noqa: E402
|
||||
AuthClient,
|
||||
RemoteExecutionClient,
|
||||
ServerClient,
|
||||
@@ -22,7 +22,7 @@ from cleveragents.acp.clients import ( # noqa: E402
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig # noqa: E402
|
||||
from cleveragents.a2a.server_config import ServerConnectionConfig # noqa: E402
|
||||
from cleveragents.application.services.config_service import _REGISTRY # noqa: E402
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
*** Settings ***
|
||||
Documentation M6 autonomy acceptance E2E smoke tests — ACP facade, guardrails, audit
|
||||
Documentation M6 autonomy acceptance E2E smoke tests — A2A facade, guardrails, audit
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
@@ -8,7 +8,7 @@ Suite Teardown Cleanup Test Environment
|
||||
${HELPER} ${CURDIR}/helper_m6_autonomy_acceptance.py
|
||||
|
||||
*** Test Cases ***
|
||||
M6 ACP Facade Session Lifecycle
|
||||
M6 A2A Facade Session Lifecycle
|
||||
[Documentation] Dispatch session.create and session.close via local facade
|
||||
${result}= Run Process ${PYTHON} ${HELPER} facade-session cwd=${WORKSPACE} timeout=30s
|
||||
Log ${result.stdout}
|
||||
@@ -16,7 +16,7 @@ M6 ACP Facade Session Lifecycle
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m6-facade-session-ok
|
||||
|
||||
M6 ACP Facade Plan Lifecycle
|
||||
M6 A2A Facade Plan Lifecycle
|
||||
[Documentation] Dispatch plan create/execute/status/diff/apply operations
|
||||
${result}= Run Process ${PYTHON} ${HELPER} facade-plan cwd=${WORKSPACE} timeout=30s
|
||||
Log ${result.stdout}
|
||||
@@ -24,15 +24,15 @@ M6 ACP Facade Plan Lifecycle
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m6-facade-plan-ok
|
||||
|
||||
M6 ACP Facade Unknown Operation Error
|
||||
[Documentation] Verify unknown operations raise AcpOperationNotFoundError
|
||||
M6 A2A Facade Unknown Operation Error
|
||||
[Documentation] Verify unknown operations raise A2aOperationNotFoundError
|
||||
${result}= Run Process ${PYTHON} ${HELPER} facade-unknown-op cwd=${WORKSPACE} timeout=30s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m6-facade-unknown-op-ok
|
||||
|
||||
M6 ACP Event Queue Publish Subscribe
|
||||
M6 A2A Event Queue Publish Subscribe
|
||||
[Documentation] Publish events and verify local subscriber receives them
|
||||
${result}= Run Process ${PYTHON} ${HELPER} event-queue cwd=${WORKSPACE} timeout=30s
|
||||
Log ${result.stdout}
|
||||
@@ -40,16 +40,16 @@ M6 ACP Event Queue Publish Subscribe
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m6-event-queue-ok
|
||||
|
||||
M6 ACP Transport Stub Rejects All
|
||||
[Documentation] Verify HTTP transport stub raises AcpNotAvailableError
|
||||
M6 A2A Transport Stub Rejects All
|
||||
[Documentation] Verify HTTP transport stub raises A2aNotAvailableError
|
||||
${result}= Run Process ${PYTHON} ${HELPER} transport-stub cwd=${WORKSPACE} timeout=30s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m6-transport-stub-ok
|
||||
|
||||
M6 ACP Version Negotiation
|
||||
[Documentation] Negotiate supported and unsupported ACP versions
|
||||
M6 A2A Version Negotiation
|
||||
[Documentation] Negotiate supported and unsupported A2A versions
|
||||
${result}= Run Process ${PYTHON} ${HELPER} version-negotiation cwd=${WORKSPACE} timeout=30s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
*** Settings ***
|
||||
Documentation Integration smoke tests for async resource cleanup (#321).
|
||||
... Validates that AsyncResourceTracker, enhanced bridge cleanup,
|
||||
... AcpEventQueue.close(), and StateManager.close() work end-to-end
|
||||
... A2aEventQueue.close(), and StateManager.close() work end-to-end
|
||||
... by executing small Python driver scripts in a subprocess.
|
||||
|
||||
Library OperatingSystem
|
||||
@@ -78,14 +78,14 @@ Test AsyncResourceTracker Timeout Warning
|
||||
${output} = Get File ${TEST_OUTPUT}
|
||||
Should Contain ${output} PASS: timeout warning
|
||||
|
||||
Test AcpEventQueue Close
|
||||
[Documentation] Verify AcpEventQueue.close() removes subscriptions
|
||||
Test A2aEventQueue Close
|
||||
[Documentation] Verify A2aEventQueue.close() removes subscriptions
|
||||
${script} = Catenate SEPARATOR=\n
|
||||
... import sys
|
||||
... sys.path.insert(0, '${WORKSPACE_ROOT}/src')
|
||||
... from cleveragents.acp.events import AcpEventQueue
|
||||
... from cleveragents.a2a.events import A2aEventQueue
|
||||
...
|
||||
... q = AcpEventQueue()
|
||||
... q = A2aEventQueue()
|
||||
... q.subscribe_local(lambda e: None)
|
||||
... q.subscribe_local(lambda e: None)
|
||||
... assert len(q._subscriptions) == 2
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""A2A (Agent-to-Agent Protocol) integration package.
|
||||
|
||||
Provides the local-mode facade, server-mode transport stubs, request/response
|
||||
models, event streaming stubs, and version negotiation for the A2A boundary.
|
||||
|
||||
In **local mode** the :class:`A2aLocalFacade` maps A2A operation names to
|
||||
direct Python method calls on existing application services. No serialization,
|
||||
no network, no authentication.
|
||||
|
||||
In **server mode** the :class:`A2aHttpTransport` is a stub that raises
|
||||
:class:`A2aNotAvailableError` for every operation. When server mode is
|
||||
implemented the concrete transport will replace these stubs.
|
||||
|
||||
Server client protocols (:class:`ServerClient`, :class:`RemoteExecutionClient`,
|
||||
:class:`AuthClient`) and their stub implementations are provided for forward
|
||||
compatibility. :class:`ServerConnectionConfig` validates connection parameters.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from cleveragents.a2a.clients import (
|
||||
AuthClient,
|
||||
RemoteExecutionClient,
|
||||
ServerClient,
|
||||
StubAuthClient,
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.a2a.errors import (
|
||||
A2aError,
|
||||
A2aNotAvailableError,
|
||||
A2aOperationNotFoundError,
|
||||
A2aVersionMismatchError,
|
||||
)
|
||||
from cleveragents.a2a.events import A2aEventQueue
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
from cleveragents.a2a.models import (
|
||||
A2aErrorDetail,
|
||||
A2aEvent,
|
||||
A2aRequest,
|
||||
A2aResponse,
|
||||
A2aVersion,
|
||||
)
|
||||
from cleveragents.a2a.server_config import ServerConnectionConfig
|
||||
from cleveragents.a2a.transport import A2aHttpTransport
|
||||
from cleveragents.a2a.versioning import A2aVersionNegotiator
|
||||
|
||||
__all__ = [
|
||||
"A2aError",
|
||||
"A2aErrorDetail",
|
||||
"A2aEvent",
|
||||
"A2aEventQueue",
|
||||
"A2aHttpTransport",
|
||||
"A2aLocalFacade",
|
||||
"A2aNotAvailableError",
|
||||
"A2aOperationNotFoundError",
|
||||
"A2aRequest",
|
||||
"A2aResponse",
|
||||
"A2aVersion",
|
||||
"A2aVersionMismatchError",
|
||||
"A2aVersionNegotiator",
|
||||
"AuthClient",
|
||||
"RemoteExecutionClient",
|
||||
"ServerClient",
|
||||
"ServerConnectionConfig",
|
||||
"StubAuthClient",
|
||||
"StubRemoteExecutionClient",
|
||||
"StubServerClient",
|
||||
]
|
||||
@@ -1,11 +1,11 @@
|
||||
"""ACP error hierarchy and domain-to-ACP error mapping.
|
||||
"""A2A error hierarchy and domain-to-A2A error mapping.
|
||||
|
||||
All ACP-specific exceptions inherit from :class:`AcpError` which itself
|
||||
All A2A-specific exceptions inherit from :class:`A2aError` which itself
|
||||
extends the project-wide :class:`CleverAgentsError`. This mirrors the
|
||||
pattern established by the LSP package.
|
||||
|
||||
The :func:`map_domain_error` function translates domain exceptions into
|
||||
structured :class:`AcpErrorDetail` instances using a well-defined error
|
||||
structured :class:`A2aErrorDetail` instances using a well-defined error
|
||||
code taxonomy so that clients receive consistent, machine-readable error
|
||||
responses regardless of which internal service produced the fault.
|
||||
"""
|
||||
@@ -26,7 +26,7 @@ from cleveragents.core.exceptions import (
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ACP error code constants
|
||||
# A2A error code constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NOT_FOUND: str = "NOT_FOUND"
|
||||
@@ -44,8 +44,8 @@ INTERNAL_ERROR: str = "INTERNAL_ERROR"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AcpError(CleverAgentsError):
|
||||
"""Base exception for all ACP errors."""
|
||||
class A2aError(CleverAgentsError):
|
||||
"""Base exception for all A2A errors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -55,19 +55,19 @@ class AcpError(CleverAgentsError):
|
||||
super().__init__(message, details)
|
||||
|
||||
|
||||
class AcpNotAvailableError(AcpError):
|
||||
class A2aNotAvailableError(A2aError):
|
||||
"""Raised when a server-mode operation is attempted in local mode."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "ACP server transport is not available in local mode",
|
||||
message: str = "A2A server transport is not available in local mode",
|
||||
details: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
super().__init__(message, details)
|
||||
|
||||
|
||||
class AcpVersionMismatchError(AcpError):
|
||||
"""Raised when client and server ACP versions are incompatible."""
|
||||
class A2aVersionMismatchError(A2aError):
|
||||
"""Raised when client and server A2A versions are incompatible."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -81,8 +81,8 @@ class AcpVersionMismatchError(AcpError):
|
||||
self.supported_versions = supported_versions
|
||||
|
||||
|
||||
class AcpOperationNotFoundError(AcpError):
|
||||
"""Raised when an unknown ACP operation is requested."""
|
||||
class A2aOperationNotFoundError(A2aError):
|
||||
"""Raised when an unknown A2A operation is requested."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -95,15 +95,15 @@ class AcpOperationNotFoundError(AcpError):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain-to-ACP error mapping
|
||||
# Domain-to-A2A error mapping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def map_domain_error(exc: Exception) -> tuple[str, str]:
|
||||
"""Map a domain exception to an ACP error code and message.
|
||||
"""Map a domain exception to an A2A error code and message.
|
||||
|
||||
Returns a ``(code, message)`` tuple suitable for constructing an
|
||||
:class:`~cleveragents.acp.models.AcpErrorDetail`.
|
||||
:class:`~cleveragents.a2a.models.A2aErrorDetail`.
|
||||
|
||||
The mapping precedence is intentional: more specific exception types
|
||||
are checked before their base classes so that, for example, a
|
||||
@@ -148,9 +148,9 @@ __all__ = [
|
||||
"NOT_FOUND",
|
||||
"PLAN_ERROR",
|
||||
"VALIDATION_ERROR",
|
||||
"AcpError",
|
||||
"AcpNotAvailableError",
|
||||
"AcpOperationNotFoundError",
|
||||
"AcpVersionMismatchError",
|
||||
"A2aError",
|
||||
"A2aNotAvailableError",
|
||||
"A2aOperationNotFoundError",
|
||||
"A2aVersionMismatchError",
|
||||
"map_domain_error",
|
||||
]
|
||||
@@ -1,8 +1,8 @@
|
||||
"""ACP event streaming — local queue and remote stub.
|
||||
"""A2A event streaming — local queue and remote stub.
|
||||
|
||||
The :class:`AcpEventQueue` provides a working in-memory event queue for
|
||||
The :class:`A2aEventQueue` provides a working in-memory event queue for
|
||||
local mode and a stub for remote subscriptions that raises
|
||||
:class:`AcpNotAvailableError`.
|
||||
:class:`A2aNotAvailableError`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -13,8 +13,8 @@ from typing import Any
|
||||
import structlog
|
||||
from ulid import ULID
|
||||
|
||||
from cleveragents.acp.errors import AcpNotAvailableError
|
||||
from cleveragents.acp.models import AcpEvent
|
||||
from cleveragents.a2a.errors import A2aNotAvailableError
|
||||
from cleveragents.a2a.models import A2aEvent
|
||||
|
||||
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -24,12 +24,12 @@ _REMOTE_MSG = (
|
||||
)
|
||||
|
||||
|
||||
class AcpEventQueue:
|
||||
class A2aEventQueue:
|
||||
"""In-memory event queue with local pub/sub and remote stub."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._events: list[AcpEvent] = []
|
||||
self._subscriptions: dict[str, Callable[[AcpEvent], Any]] = {}
|
||||
self._events: list[A2aEvent] = []
|
||||
self._subscriptions: dict[str, Callable[[A2aEvent], Any]] = {}
|
||||
self._is_closed: bool = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -41,15 +41,15 @@ class AcpEventQueue:
|
||||
"""Whether this queue has been closed."""
|
||||
return self._is_closed
|
||||
|
||||
def publish(self, event: AcpEvent) -> None:
|
||||
def publish(self, event: A2aEvent) -> None:
|
||||
"""Append *event* to the local queue and notify subscribers."""
|
||||
if self._is_closed:
|
||||
raise RuntimeError("Cannot publish to a closed event queue")
|
||||
if not isinstance(event, AcpEvent):
|
||||
raise TypeError("event must be an AcpEvent instance")
|
||||
if not isinstance(event, A2aEvent):
|
||||
raise TypeError("event must be an A2aEvent instance")
|
||||
self._events.append(event)
|
||||
logger.debug(
|
||||
"acp.event.published",
|
||||
"a2a.event.published",
|
||||
event_id=event.event_id,
|
||||
event_type=event.event_type,
|
||||
)
|
||||
@@ -58,17 +58,17 @@ class AcpEventQueue:
|
||||
callback(event)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"acp.event.callback_error",
|
||||
"a2a.event.callback_error",
|
||||
subscription_id=sub_id,
|
||||
)
|
||||
|
||||
def subscribe_local(self, callback: Callable[[AcpEvent], Any]) -> str:
|
||||
def subscribe_local(self, callback: Callable[[A2aEvent], Any]) -> str:
|
||||
"""Register a local callback and return a subscription ID."""
|
||||
if not callable(callback):
|
||||
raise TypeError("callback must be callable")
|
||||
sub_id = str(ULID())
|
||||
self._subscriptions[sub_id] = callback
|
||||
logger.debug("acp.event.subscribed", subscription_id=sub_id)
|
||||
logger.debug("a2a.event.subscribed", subscription_id=sub_id)
|
||||
return sub_id
|
||||
|
||||
def unsubscribe(self, subscription_id: str) -> bool:
|
||||
@@ -77,10 +77,10 @@ class AcpEventQueue:
|
||||
raise ValueError("subscription_id must be a non-empty string")
|
||||
removed = self._subscriptions.pop(subscription_id, None) is not None
|
||||
if removed:
|
||||
logger.debug("acp.event.unsubscribed", subscription_id=subscription_id)
|
||||
logger.debug("a2a.event.unsubscribed", subscription_id=subscription_id)
|
||||
return removed
|
||||
|
||||
def get_events(self, limit: int = 100) -> list[AcpEvent]:
|
||||
def get_events(self, limit: int = 100) -> list[A2aEvent]:
|
||||
"""Return the most recent *limit* events from the queue."""
|
||||
if not isinstance(limit, int) or limit < 1:
|
||||
raise ValueError("limit must be a positive integer")
|
||||
@@ -97,7 +97,7 @@ class AcpEventQueue:
|
||||
self._subscriptions.clear()
|
||||
self._events.clear()
|
||||
if count:
|
||||
logger.info("acp.event_queue.closed", subscription_count=count)
|
||||
logger.info("a2a.event_queue.closed", subscription_count=count)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Remote stub (raises)
|
||||
@@ -106,17 +106,17 @@ class AcpEventQueue:
|
||||
def subscribe_remote(self, endpoint: str) -> None:
|
||||
"""Subscribe to a remote event stream.
|
||||
|
||||
Always raises :class:`AcpNotAvailableError` in local mode.
|
||||
Always raises :class:`A2aNotAvailableError` in local mode.
|
||||
"""
|
||||
if not endpoint or not isinstance(endpoint, str):
|
||||
raise ValueError("endpoint must be a non-empty string")
|
||||
logger.warning("%s (endpoint=%s)", _REMOTE_MSG, endpoint)
|
||||
raise AcpNotAvailableError(
|
||||
raise A2aNotAvailableError(
|
||||
_REMOTE_MSG,
|
||||
details={"endpoint": endpoint},
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AcpEventQueue",
|
||||
"A2aEventQueue",
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
"""ACP local-mode facade routing operations to application services.
|
||||
"""A2A local-mode facade routing operations to application services.
|
||||
|
||||
In local mode every ACP operation maps 1:1 to a method call on the
|
||||
In local mode every A2A operation maps 1:1 to a method call on the
|
||||
appropriate application service. No serialization, network, or auth.
|
||||
|
||||
Service dependencies are supplied via the ``services`` dict passed to the
|
||||
@@ -12,7 +12,7 @@ constructor. Expected keys:
|
||||
| ``plan_lifecycle_service`` | ``PlanLifecycleService`` | plan.* operations |
|
||||
| ``tool_registry`` | ``ToolRegistry`` | registry.list_tools |
|
||||
| ``resource_registry_service``| ``ResourceRegistryService`` | registry.list_resources|
|
||||
| ``event_queue`` | ``AcpEventQueue`` | event.subscribe |
|
||||
| ``event_queue`` | ``A2aEventQueue`` | event.subscribe |
|
||||
|
||||
When a service is absent the handler falls back to a safe stub
|
||||
response so the facade never crashes due to missing wiring.
|
||||
@@ -26,19 +26,19 @@ from typing import TYPE_CHECKING, Any
|
||||
import structlog
|
||||
from ulid import ULID
|
||||
|
||||
from cleveragents.acp.errors import (
|
||||
AcpOperationNotFoundError,
|
||||
from cleveragents.a2a.errors import (
|
||||
A2aOperationNotFoundError,
|
||||
map_domain_error,
|
||||
)
|
||||
from cleveragents.acp.models import (
|
||||
AcpErrorDetail,
|
||||
AcpRequest,
|
||||
AcpResponse,
|
||||
AcpVersion,
|
||||
from cleveragents.a2a.models import (
|
||||
A2aErrorDetail,
|
||||
A2aRequest,
|
||||
A2aResponse,
|
||||
A2aVersion,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cleveragents.acp.events import AcpEventQueue
|
||||
from cleveragents.a2a.events import A2aEventQueue
|
||||
from cleveragents.application.services.plan_lifecycle_service import (
|
||||
PlanLifecycleService,
|
||||
)
|
||||
@@ -69,8 +69,8 @@ _SUPPORTED_OPERATIONS: list[str] = [
|
||||
]
|
||||
|
||||
|
||||
class AcpLocalFacade:
|
||||
"""Local-mode facade that dispatches ACP operations to services.
|
||||
class A2aLocalFacade:
|
||||
"""Local-mode facade that dispatches A2A operations to services.
|
||||
|
||||
Accepts service dependencies via the ``services`` dict. Each handler
|
||||
checks for the presence of the required service and delegates to it
|
||||
@@ -82,7 +82,7 @@ class AcpLocalFacade:
|
||||
raise TypeError("services must be a dict or None")
|
||||
self._services: dict[str, Any] = dict(services) if services else {}
|
||||
# PERF-1 fix: cache the handler dispatch dict instead of
|
||||
# rebuilding it on every ACP request.
|
||||
# rebuilding it on every A2A request.
|
||||
self._handler_map: dict[str, Any] | None = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -108,57 +108,57 @@ class AcpLocalFacade:
|
||||
return svc # type: ignore[return-value]
|
||||
|
||||
@property
|
||||
def _event_queue(self) -> AcpEventQueue | None:
|
||||
def _event_queue(self) -> A2aEventQueue | None:
|
||||
return self._services.get("event_queue") # type: ignore[return-value]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def dispatch(self, request: AcpRequest) -> AcpResponse:
|
||||
"""Route an :class:`AcpRequest` to the appropriate handler.
|
||||
def dispatch(self, request: A2aRequest) -> A2aResponse:
|
||||
"""Route an :class:`A2aRequest` to the appropriate handler.
|
||||
|
||||
Returns an :class:`AcpResponse` with ``status='ok'`` on success or
|
||||
Returns an :class:`A2aResponse` with ``status='ok'`` on success or
|
||||
``status='error'`` when the operation fails. Domain exceptions
|
||||
are mapped to ACP error codes via :func:`map_domain_error`.
|
||||
are mapped to A2A error codes via :func:`map_domain_error`.
|
||||
"""
|
||||
if not isinstance(request, AcpRequest):
|
||||
raise TypeError("request must be an AcpRequest instance")
|
||||
if not isinstance(request, A2aRequest):
|
||||
raise TypeError("request must be an A2aRequest instance")
|
||||
|
||||
start = time.monotonic()
|
||||
try:
|
||||
data = self._route_operation(request.operation, request.params)
|
||||
elapsed = (time.monotonic() - start) * 1000.0
|
||||
logger.info(
|
||||
"acp.local.dispatch",
|
||||
"a2a.local.dispatch",
|
||||
operation=request.operation,
|
||||
request_id=request.request_id,
|
||||
timing_ms=round(elapsed, 2),
|
||||
)
|
||||
return AcpResponse(
|
||||
acp_version=AcpVersion.CURRENT,
|
||||
return A2aResponse(
|
||||
a2a_version=A2aVersion.CURRENT,
|
||||
request_id=request.request_id,
|
||||
status="ok",
|
||||
data=data,
|
||||
timing_ms=round(elapsed, 2),
|
||||
)
|
||||
except AcpOperationNotFoundError:
|
||||
except A2aOperationNotFoundError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
elapsed = (time.monotonic() - start) * 1000.0
|
||||
code, message = map_domain_error(exc)
|
||||
logger.error(
|
||||
"acp.local.dispatch.error",
|
||||
"a2a.local.dispatch.error",
|
||||
operation=request.operation,
|
||||
request_id=request.request_id,
|
||||
error_code=code,
|
||||
error=message,
|
||||
)
|
||||
return AcpResponse(
|
||||
acp_version=AcpVersion.CURRENT,
|
||||
return A2aResponse(
|
||||
a2a_version=A2aVersion.CURRENT,
|
||||
request_id=request.request_id,
|
||||
status="error",
|
||||
error=AcpErrorDetail(
|
||||
error=A2aErrorDetail(
|
||||
code=code,
|
||||
message=message,
|
||||
),
|
||||
@@ -173,10 +173,10 @@ class AcpLocalFacade:
|
||||
# PERF-1 fix: invalidate cached handler map so new service
|
||||
# wiring is picked up on the next dispatch.
|
||||
self._handler_map = None
|
||||
logger.debug("acp.local.service_registered", service_name=name)
|
||||
logger.debug("a2a.local.service_registered", service_name=name)
|
||||
|
||||
def list_operations(self) -> list[str]:
|
||||
"""Return the list of supported ACP operation names."""
|
||||
"""Return the list of supported A2A operation names."""
|
||||
return list(_SUPPORTED_OPERATIONS)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -191,8 +191,8 @@ class AcpLocalFacade:
|
||||
"""Map *operation* to a handler and return the result dict."""
|
||||
handler = self._handlers().get(operation)
|
||||
if handler is None:
|
||||
raise AcpOperationNotFoundError(
|
||||
message=f"Unknown ACP operation: {operation}",
|
||||
raise A2aOperationNotFoundError(
|
||||
message=f"Unknown A2A operation: {operation}",
|
||||
operation=operation,
|
||||
)
|
||||
return handler(params)
|
||||
@@ -267,13 +267,13 @@ class AcpLocalFacade:
|
||||
)
|
||||
if stopped:
|
||||
logger.info(
|
||||
"acp.session.close.devcontainer_cleanup",
|
||||
"a2a.session.close.devcontainer_cleanup",
|
||||
session_id=session_id,
|
||||
stopped_count=len(stopped),
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"acp.session.close.devcontainer_cleanup_failed",
|
||||
"a2a.session.close.devcontainer_cleanup_failed",
|
||||
session_id=session_id,
|
||||
exc_info=True,
|
||||
)
|
||||
@@ -406,5 +406,5 @@ class AcpLocalFacade:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AcpLocalFacade",
|
||||
"A2aLocalFacade",
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
"""ACP request/response envelope models.
|
||||
"""A2A request/response envelope models.
|
||||
|
||||
Pydantic v2 models for the ACP wire format. In local mode these are
|
||||
Pydantic v2 models for the A2A wire format. In local mode these are
|
||||
used purely as validated data containers — no serialization to JSON
|
||||
actually occurs over a network.
|
||||
"""
|
||||
@@ -18,8 +18,8 @@ from ulid import ULID
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AcpVersion:
|
||||
"""ACP protocol version constants."""
|
||||
class A2aVersion:
|
||||
"""A2A protocol version constants."""
|
||||
|
||||
CURRENT: str = "1.0"
|
||||
SUPPORTED: tuple[str, ...] = ("1.0",)
|
||||
@@ -45,8 +45,8 @@ def _iso_now_factory() -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AcpErrorDetail(BaseModel):
|
||||
"""Structured error payload inside an :class:`AcpResponse`."""
|
||||
class A2aErrorDetail(BaseModel):
|
||||
"""Structured error payload inside an :class:`A2aResponse`."""
|
||||
|
||||
model_config = ConfigDict(strict=False)
|
||||
|
||||
@@ -62,12 +62,12 @@ class AcpErrorDetail(BaseModel):
|
||||
return value
|
||||
|
||||
|
||||
class AcpRequest(BaseModel):
|
||||
"""Inbound ACP operation envelope."""
|
||||
class A2aRequest(BaseModel):
|
||||
"""Inbound A2A operation envelope."""
|
||||
|
||||
model_config = ConfigDict(strict=False)
|
||||
|
||||
acp_version: str = AcpVersion.CURRENT
|
||||
a2a_version: str = A2aVersion.CURRENT
|
||||
request_id: str = ""
|
||||
operation: str
|
||||
params: dict[str, Any] = {}
|
||||
@@ -81,22 +81,22 @@ class AcpRequest(BaseModel):
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _default_request_id(self) -> AcpRequest:
|
||||
def _default_request_id(self) -> A2aRequest:
|
||||
if not self.request_id:
|
||||
self.request_id = _ulid_factory()
|
||||
return self
|
||||
|
||||
|
||||
class AcpResponse(BaseModel):
|
||||
"""Outbound ACP result envelope."""
|
||||
class A2aResponse(BaseModel):
|
||||
"""Outbound A2A result envelope."""
|
||||
|
||||
model_config = ConfigDict(strict=False)
|
||||
|
||||
acp_version: str = AcpVersion.CURRENT
|
||||
a2a_version: str = A2aVersion.CURRENT
|
||||
request_id: str
|
||||
status: str
|
||||
data: dict[str, Any] = {}
|
||||
error: AcpErrorDetail | None = None
|
||||
error: A2aErrorDetail | None = None
|
||||
timing_ms: float | None = None
|
||||
|
||||
@field_validator("status")
|
||||
@@ -107,7 +107,7 @@ class AcpResponse(BaseModel):
|
||||
return value
|
||||
|
||||
|
||||
class AcpEvent(BaseModel):
|
||||
class A2aEvent(BaseModel):
|
||||
"""Server-sent event envelope for plan progress and streaming."""
|
||||
|
||||
model_config = ConfigDict(strict=False)
|
||||
@@ -126,7 +126,7 @@ class AcpEvent(BaseModel):
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_defaults(self) -> AcpEvent:
|
||||
def _set_defaults(self) -> A2aEvent:
|
||||
if not self.event_id:
|
||||
self.event_id = _ulid_factory()
|
||||
if not self.timestamp:
|
||||
@@ -135,9 +135,9 @@ class AcpEvent(BaseModel):
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AcpErrorDetail",
|
||||
"AcpEvent",
|
||||
"AcpRequest",
|
||||
"AcpResponse",
|
||||
"AcpVersion",
|
||||
"A2aErrorDetail",
|
||||
"A2aEvent",
|
||||
"A2aRequest",
|
||||
"A2aResponse",
|
||||
"A2aVersion",
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
"""ACP server-mode HTTP transport stub.
|
||||
"""A2A server-mode HTTP transport stub.
|
||||
|
||||
Every method raises :class:`AcpNotAvailableError`. When server mode is
|
||||
Every method raises :class:`A2aNotAvailableError`. When server mode is
|
||||
implemented (separate project) the concrete transport will replace this stub.
|
||||
"""
|
||||
|
||||
@@ -8,57 +8,57 @@ from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
|
||||
from cleveragents.acp.errors import AcpNotAvailableError
|
||||
from cleveragents.acp.models import AcpRequest, AcpResponse
|
||||
from cleveragents.a2a.errors import A2aNotAvailableError
|
||||
from cleveragents.a2a.models import A2aRequest, A2aResponse
|
||||
|
||||
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
|
||||
|
||||
_SERVER_MODE_MSG = (
|
||||
"ACP HTTP transport is not available in local mode"
|
||||
"A2A HTTP transport is not available in local mode"
|
||||
" - server mode will be implemented as a separate project"
|
||||
)
|
||||
|
||||
|
||||
class AcpHttpTransport:
|
||||
class A2aHttpTransport:
|
||||
"""Stub HTTP transport — all mutating methods raise on invocation."""
|
||||
|
||||
def send(self, request: AcpRequest) -> AcpResponse:
|
||||
"""Send an ACP request over HTTP.
|
||||
def send(self, request: A2aRequest) -> A2aResponse:
|
||||
"""Send an A2A request over HTTP.
|
||||
|
||||
Always raises :class:`AcpNotAvailableError` in local mode.
|
||||
Always raises :class:`A2aNotAvailableError` in local mode.
|
||||
"""
|
||||
if not isinstance(request, AcpRequest):
|
||||
raise TypeError("request must be an AcpRequest instance")
|
||||
if not isinstance(request, A2aRequest):
|
||||
raise TypeError("request must be an A2aRequest instance")
|
||||
logger.warning(
|
||||
"%s (operation=%s)",
|
||||
_SERVER_MODE_MSG,
|
||||
request.operation,
|
||||
)
|
||||
raise AcpNotAvailableError(
|
||||
raise A2aNotAvailableError(
|
||||
_SERVER_MODE_MSG,
|
||||
details={"operation": request.operation},
|
||||
)
|
||||
|
||||
def connect(self, base_url: str) -> None:
|
||||
"""Open a connection to the ACP server.
|
||||
"""Open a connection to the A2A server.
|
||||
|
||||
Always raises :class:`AcpNotAvailableError` in local mode.
|
||||
Always raises :class:`A2aNotAvailableError` in local mode.
|
||||
"""
|
||||
if not base_url or not isinstance(base_url, str):
|
||||
raise ValueError("base_url must be a non-empty string")
|
||||
logger.warning("%s (url=%s)", _SERVER_MODE_MSG, base_url)
|
||||
raise AcpNotAvailableError(
|
||||
raise A2aNotAvailableError(
|
||||
_SERVER_MODE_MSG,
|
||||
details={"base_url": base_url},
|
||||
)
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Close the connection to the ACP server.
|
||||
"""Close the connection to the A2A server.
|
||||
|
||||
Always raises :class:`AcpNotAvailableError` in local mode.
|
||||
Always raises :class:`A2aNotAvailableError` in local mode.
|
||||
"""
|
||||
logger.warning(_SERVER_MODE_MSG)
|
||||
raise AcpNotAvailableError(_SERVER_MODE_MSG)
|
||||
raise A2aNotAvailableError(_SERVER_MODE_MSG)
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Return connection status. Always ``False`` in local mode."""
|
||||
@@ -66,5 +66,5 @@ class AcpHttpTransport:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AcpHttpTransport",
|
||||
"A2aHttpTransport",
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
"""ACP version negotiation.
|
||||
"""A2A version negotiation.
|
||||
|
||||
Ensures that a requested ACP version is within the set of supported
|
||||
versions. Raises :class:`AcpVersionMismatchError` when the requested
|
||||
Ensures that a requested A2A version is within the set of supported
|
||||
versions. Raises :class:`A2aVersionMismatchError` when the requested
|
||||
version is not supported.
|
||||
"""
|
||||
|
||||
@@ -9,13 +9,13 @@ from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
|
||||
from cleveragents.acp.errors import AcpVersionMismatchError
|
||||
from cleveragents.a2a.errors import A2aVersionMismatchError
|
||||
|
||||
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class AcpVersionNegotiator:
|
||||
"""Negotiates ACP protocol versions."""
|
||||
class A2aVersionNegotiator:
|
||||
"""Negotiates A2A protocol versions."""
|
||||
|
||||
CURRENT_VERSION: str = "1.0"
|
||||
SUPPORTED_VERSIONS: tuple[str, ...] = ("1.0",)
|
||||
@@ -25,16 +25,16 @@ class AcpVersionNegotiator:
|
||||
|
||||
Raises:
|
||||
ValueError: If *requested* is empty.
|
||||
AcpVersionMismatchError: If version is not in
|
||||
A2aVersionMismatchError: If version is not in
|
||||
:attr:`SUPPORTED_VERSIONS`.
|
||||
"""
|
||||
if not requested or not isinstance(requested, str):
|
||||
raise ValueError("requested version must be a non-empty string")
|
||||
if requested in self.SUPPORTED_VERSIONS:
|
||||
logger.debug("acp.version.negotiated", version=requested)
|
||||
logger.debug("a2a.version.negotiated", version=requested)
|
||||
return requested
|
||||
raise AcpVersionMismatchError(
|
||||
message=f"ACP version '{requested}' is not supported",
|
||||
raise A2aVersionMismatchError(
|
||||
message=f"A2A version '{requested}' is not supported",
|
||||
requested_version=requested,
|
||||
supported_versions=list(self.SUPPORTED_VERSIONS),
|
||||
)
|
||||
@@ -46,10 +46,10 @@ class AcpVersionNegotiator:
|
||||
return version in self.SUPPORTED_VERSIONS
|
||||
|
||||
def get_current(self) -> str:
|
||||
"""Return the current ACP protocol version."""
|
||||
"""Return the current A2A protocol version."""
|
||||
return self.CURRENT_VERSION
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AcpVersionNegotiator",
|
||||
"A2aVersionNegotiator",
|
||||
]
|
||||
@@ -1,69 +0,0 @@
|
||||
"""ACP (Agent Communication Protocol) integration package.
|
||||
|
||||
Provides the local-mode facade, server-mode transport stubs, request/response
|
||||
models, event streaming stubs, and version negotiation for the ACP boundary.
|
||||
|
||||
In **local mode** the :class:`AcpLocalFacade` maps ACP operation names to
|
||||
direct Python method calls on existing application services. No serialization,
|
||||
no network, no authentication.
|
||||
|
||||
In **server mode** the :class:`AcpHttpTransport` is a stub that raises
|
||||
:class:`AcpNotAvailableError` for every operation. When server mode is
|
||||
implemented the concrete transport will replace these stubs.
|
||||
|
||||
Server client protocols (:class:`ServerClient`, :class:`RemoteExecutionClient`,
|
||||
:class:`AuthClient`) and their stub implementations are provided for forward
|
||||
compatibility. :class:`ServerConnectionConfig` validates connection parameters.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from cleveragents.acp.clients import (
|
||||
AuthClient,
|
||||
RemoteExecutionClient,
|
||||
ServerClient,
|
||||
StubAuthClient,
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.acp.errors import (
|
||||
AcpError,
|
||||
AcpNotAvailableError,
|
||||
AcpOperationNotFoundError,
|
||||
AcpVersionMismatchError,
|
||||
)
|
||||
from cleveragents.acp.events import AcpEventQueue
|
||||
from cleveragents.acp.facade import AcpLocalFacade
|
||||
from cleveragents.acp.models import (
|
||||
AcpErrorDetail,
|
||||
AcpEvent,
|
||||
AcpRequest,
|
||||
AcpResponse,
|
||||
AcpVersion,
|
||||
)
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig
|
||||
from cleveragents.acp.transport import AcpHttpTransport
|
||||
from cleveragents.acp.versioning import AcpVersionNegotiator
|
||||
|
||||
__all__ = [
|
||||
"AcpError",
|
||||
"AcpErrorDetail",
|
||||
"AcpEvent",
|
||||
"AcpEventQueue",
|
||||
"AcpHttpTransport",
|
||||
"AcpLocalFacade",
|
||||
"AcpNotAvailableError",
|
||||
"AcpOperationNotFoundError",
|
||||
"AcpRequest",
|
||||
"AcpResponse",
|
||||
"AcpVersion",
|
||||
"AcpVersionMismatchError",
|
||||
"AcpVersionNegotiator",
|
||||
"AuthClient",
|
||||
"RemoteExecutionClient",
|
||||
"ServerClient",
|
||||
"ServerConnectionConfig",
|
||||
"StubAuthClient",
|
||||
"StubRemoteExecutionClient",
|
||||
"StubServerClient",
|
||||
]
|
||||
@@ -394,8 +394,8 @@ def serve(
|
||||
" All other methods → MethodNotFound (-32601)"
|
||||
)
|
||||
|
||||
# Lazy import: LspServer transitively imports AcpLocalFacade
|
||||
# which pulls the full ACP dependency chain (~200ms cold).
|
||||
# Lazy import: LspServer transitively imports A2aLocalFacade
|
||||
# which pulls the full A2A dependency chain (~200ms cold).
|
||||
# Non-serve subcommands (add, remove, list, show) do not
|
||||
# need LspServer, so deferring the import avoids that cost
|
||||
# for the common case.
|
||||
|
||||
@@ -15,7 +15,7 @@ from pydantic import ValidationError
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig
|
||||
from cleveragents.a2a.server_config import ServerConnectionConfig
|
||||
from cleveragents.application.services.config_service import ConfigService
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ Content-Length header framing. Supports the ``initialize``,
|
||||
``shutdown``, and ``exit`` lifecycle methods. All other LSP
|
||||
methods return a JSON-RPC ``MethodNotFound`` (-32601) error.
|
||||
|
||||
An :class:`AcpLocalFacade` instance is stored so that the
|
||||
dispatch path can be wired through ACP when server mode lands.
|
||||
An :class:`A2aLocalFacade` instance is stored so that the
|
||||
dispatch path can be wired through A2A when server mode lands.
|
||||
In the current stub the facade is a placeholder — all methods
|
||||
are handled by hardcoded stub handlers.
|
||||
|
||||
@@ -26,7 +26,7 @@ from typing import Any, BinaryIO
|
||||
|
||||
import structlog
|
||||
|
||||
from cleveragents.acp.facade import AcpLocalFacade
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
|
||||
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -99,11 +99,11 @@ class LspServer:
|
||||
|
||||
The server reads Content-Length framed JSON-RPC messages from
|
||||
*input_stream* and writes responses to *output_stream*. LSP
|
||||
requests are routed through an :class:`AcpLocalFacade` instance
|
||||
requests are routed through an :class:`A2aLocalFacade` instance
|
||||
in local mode.
|
||||
|
||||
Attributes:
|
||||
_facade: ACP local-mode facade for request routing.
|
||||
_facade: A2A local-mode facade for request routing.
|
||||
_input: Binary stream to read from (default: ``sys.stdin.buffer``).
|
||||
_output: Binary stream to write to (default: ``sys.stdout.buffer``).
|
||||
_running: Whether the event loop is active.
|
||||
@@ -115,7 +115,7 @@ class LspServer:
|
||||
self,
|
||||
input_stream: BinaryIO | None = None,
|
||||
output_stream: BinaryIO | None = None,
|
||||
facade: AcpLocalFacade | None = None,
|
||||
facade: A2aLocalFacade | None = None,
|
||||
) -> None:
|
||||
if input_stream is not None and not hasattr(input_stream, "read"):
|
||||
raise TypeError("input_stream must support read()")
|
||||
@@ -123,22 +123,22 @@ class LspServer:
|
||||
raise TypeError("output_stream must support write()")
|
||||
self._input: BinaryIO = input_stream or sys.stdin.buffer
|
||||
self._output: BinaryIO = output_stream or sys.stdout.buffer
|
||||
self._facade: AcpLocalFacade | None = facade
|
||||
self._facade: A2aLocalFacade | None = facade
|
||||
self._running: bool = False
|
||||
self._initialized: bool = False
|
||||
self._shutdown_requested: bool = False
|
||||
|
||||
@property
|
||||
def facade(self) -> AcpLocalFacade:
|
||||
"""Return the ACP facade, creating it lazily on first access.
|
||||
def facade(self) -> A2aLocalFacade:
|
||||
"""Return the A2A facade, creating it lazily on first access.
|
||||
|
||||
The facade is stored for future server-mode wiring but is
|
||||
not called in the current stub. Lazy creation avoids the
|
||||
overhead of ``AcpLocalFacade()`` construction when the
|
||||
overhead of ``A2aLocalFacade()`` construction when the
|
||||
facade is never needed (e.g., benchmarks).
|
||||
"""
|
||||
if self._facade is None:
|
||||
self._facade = AcpLocalFacade()
|
||||
self._facade = A2aLocalFacade()
|
||||
return self._facade
|
||||
|
||||
@property
|
||||
|
||||
Reference in New Issue
Block a user