7e9a45044b
CI / integration_tests (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled
CI / helm (push) Has been cancelled
CI / security (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / unit_tests (push) Has been cancelled
CI / quality (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / status-check (push) Has been cancelled
CI / coverage (push) Has been cancelled
The CLI `session create` command created the session via SessionService.create()
(which commits via auto_commit=True), then called _facade_dispatch("session.create")
for A2A protocol bookkeeping. The facade handler unconditionally called
svc.create() on a second PersistentSessionService instance (a new Factory
resolution from the DI container with its own engine), creating a duplicate
session in the database.
The fix makes A2aLocalFacade._handle_session_create() idempotent: when a
session_id is already present in the params, it acknowledges the existing
session without creating a new one. The CLI dispatch params were also fixed
to use the correct key name (actor_name instead of actor).
Changes:
- src/cleveragents/a2a/facade.py: Early return in _handle_session_create when
session_id is already supplied, preventing duplicate session creation.
- src/cleveragents/cli/commands/session.py: Fixed param key from "actor" to
"actor_name" for consistency with the facade handler.
- features/a2a_facade_wiring.feature: Added idempotency scenario verifying
that session.create with an existing session_id does not call svc.create().
- features/steps/a2a_facade_wiring_steps.py: Added step asserting mock
SessionService.create was not called.
- features/tdd_session_create_persist.feature: Removed @tdd_expected_fail tag
now that the bug is fixed.
- robot/e2e/e2e_session_create_persist.robot: Removed tdd_expected_fail tag,
updated documentation.
- .semgrep.yml: Excluded wrapping.py from no-exec/no-compile-exec rules
(pre-existing sandboxed exec usage for tool transforms).
ISSUES CLOSED: #1141
Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
168 lines
8.3 KiB
Gherkin
168 lines
8.3 KiB
Gherkin
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
|
|
|
|
# ---------------------------------------------------------------
|
|
# Session wiring
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: session.create delegates to 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.create with existing session_id is idempotent
|
|
Given a wired A2aLocalFacade with a mock SessionService
|
|
When I dispatch wired operation "session.create" with params {"session_id": "EXISTING-SESSION-999"}
|
|
Then the wired response status should be "ok"
|
|
And wired response data key "session_id" equals "EXISTING-SESSION-999"
|
|
And wired response data key "status" equals "created"
|
|
And the mock SessionService create should not have been called
|
|
|
|
Scenario: session.close delegates to 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 A2aLocalFacade with a mock SessionService
|
|
When I dispatch wired operation "session.close" with params {}
|
|
Then the wired response status should be "error"
|
|
|
|
# ---------------------------------------------------------------
|
|
# Plan lifecycle wiring
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: plan.create delegates to 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 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 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 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 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 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"
|
|
|
|
# ---------------------------------------------------------------
|
|
# Registry wiring
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: registry.list_tools delegates to 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 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
|
|
|
|
# ---------------------------------------------------------------
|
|
# Context stub
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: context.get returns stub pending ACMS pipeline
|
|
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"
|
|
|
|
# ---------------------------------------------------------------
|
|
# Event wiring
|
|
# ---------------------------------------------------------------
|
|
|
|
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"
|
|
And wired response data key "subscription_id" should not be empty
|
|
|
|
# ---------------------------------------------------------------
|
|
# Error mapping
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: NOT_FOUND error code for missing resources
|
|
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 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 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 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"
|
|
|
|
# ---------------------------------------------------------------
|
|
# Fallback stubs when no service is wired
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: session.create stub when no service wired
|
|
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 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 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 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"
|