287 lines
12 KiB
Gherkin
287 lines
12 KiB
Gherkin
@phase2 @acp @facade
|
|
Feature: ACP Local Facade and Server Stubs
|
|
As a developer
|
|
I want an ACP integration layer with local-mode facade and server stubs
|
|
So that ACP operations work locally and server mode is stubbed out
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpLocalFacade — creation and service registration
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: Create facade with no services
|
|
Given a new AcpLocalFacade 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"}
|
|
Then the facade should be created successfully
|
|
|
|
Scenario: Register a service on the facade
|
|
Given a new AcpLocalFacade 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
|
|
When I try to register a service with an empty name
|
|
Then an ACP ValueError should be raised
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpLocalFacade — dispatch for each supported operation
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: Dispatch session.create returns session_id and status
|
|
Given a new AcpLocalFacade 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"
|
|
And response data key "status" equals "created"
|
|
|
|
Scenario: Dispatch session.close returns status closed
|
|
Given a new AcpLocalFacade 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
|
|
When I dispatch operation "plan.create" with params {}
|
|
Then the response status should be "ok"
|
|
And response data includes key "plan_id"
|
|
And response data key "status" equals "created"
|
|
|
|
Scenario: Dispatch plan.execute returns plan_id and status queued
|
|
Given a new AcpLocalFacade 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"
|
|
And response data key "status" equals "queued"
|
|
|
|
Scenario: Dispatch plan.status returns plan_id and phase
|
|
Given a new AcpLocalFacade 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"
|
|
And response data key "phase" equals "unknown"
|
|
|
|
Scenario: Dispatch plan.diff returns plan_id and empty changes
|
|
Given a new AcpLocalFacade 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"
|
|
And response data includes key "changes"
|
|
|
|
Scenario: Dispatch plan.apply returns plan_id and status applied
|
|
Given a new AcpLocalFacade 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"
|
|
And response data key "status" equals "applied"
|
|
|
|
Scenario: Dispatch registry.list_tools returns empty tools
|
|
Given a new AcpLocalFacade 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
|
|
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
|
|
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
|
|
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
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: Dispatch unknown operation raises AcpOperationNotFoundError
|
|
Given a new AcpLocalFacade with no services
|
|
When I dispatch an unknown operation "does.not.exist"
|
|
Then an AcpOperationNotFoundError should be raised
|
|
And the error operation attribute should be "does.not.exist"
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpLocalFacade — list_operations
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: list_operations returns all supported operations
|
|
Given a new AcpLocalFacade 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"
|
|
And the operations list should contain "plan.execute"
|
|
And the operations list should contain "registry.list_tools"
|
|
And the operations list should contain "context.get"
|
|
And the operations list should contain "event.subscribe"
|
|
And the operations list should have 11 items
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpHttpTransport — all stubs raise AcpNotAvailableError
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: Transport send raises AcpNotAvailableError
|
|
Given a new AcpHttpTransport
|
|
When I try to send a request via the transport
|
|
Then an AcpNotAvailableError should be raised
|
|
|
|
Scenario: Transport connect raises AcpNotAvailableError
|
|
Given a new AcpHttpTransport
|
|
When I try to connect via the transport to "http://localhost:8080"
|
|
Then an AcpNotAvailableError should be raised
|
|
|
|
Scenario: Transport disconnect raises AcpNotAvailableError
|
|
Given a new AcpHttpTransport
|
|
When I try to disconnect the transport
|
|
Then an AcpNotAvailableError should be raised
|
|
|
|
Scenario: Transport is_connected returns False
|
|
Given a new AcpHttpTransport
|
|
Then the transport should not be connected
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpEventQueue — local mode works
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: Publish event to local queue
|
|
Given a new AcpEventQueue
|
|
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
|
|
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
|
|
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
|
|
When I unsubscribe using a non-existent subscription id
|
|
Then the unsubscribe should return False
|
|
|
|
Scenario: Get events respects limit
|
|
Given a new AcpEventQueue
|
|
When I publish 5 events
|
|
And I get events with limit 3
|
|
Then I should receive 3 events
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpEventQueue — remote stub raises
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: Remote subscribe raises AcpNotAvailableError
|
|
Given a new AcpEventQueue
|
|
When I try to subscribe remotely to "http://remote:9090/events"
|
|
Then an AcpNotAvailableError should be raised
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpVersionNegotiator
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: Negotiate supported version succeeds
|
|
Given a new AcpVersionNegotiator
|
|
When I negotiate version "1.0"
|
|
Then the negotiated version should be "1.0"
|
|
|
|
Scenario: Negotiate unsupported version raises error
|
|
Given a new AcpVersionNegotiator
|
|
When I try to negotiate version "2.0"
|
|
Then an AcpVersionMismatchError should be raised
|
|
And the error requested_version should be "2.0"
|
|
|
|
Scenario: is_supported returns True for valid version
|
|
Given a new AcpVersionNegotiator
|
|
Then version "1.0" should be supported
|
|
|
|
Scenario: is_supported returns False for invalid version
|
|
Given a new AcpVersionNegotiator
|
|
Then version "99.0" should not be supported
|
|
|
|
Scenario: get_current returns current version
|
|
Given a new AcpVersionNegotiator
|
|
Then the current version should be "1.0"
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpRequest model validation
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: AcpRequest with valid operation succeeds
|
|
When I create an AcpRequest with operation "session.create"
|
|
Then the request should have a non-empty request_id
|
|
And the request acp_version should be "1.0"
|
|
|
|
Scenario: AcpRequest with empty operation fails validation
|
|
When I try to create an AcpRequest with empty operation
|
|
Then a validation error should be raised
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpResponse model validation
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: AcpResponse with valid status ok succeeds
|
|
When I create an AcpResponse 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"
|
|
Then a validation error should be raised
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpErrorDetail model validation
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: AcpErrorDetail with valid fields succeeds
|
|
When I create an AcpErrorDetail 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
|
|
Then a validation error should be raised
|
|
|
|
# -----------------------------------------------------------------------
|
|
# AcpEvent model construction
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: AcpEvent auto-generates event_id and timestamp
|
|
When I create an AcpEvent with type "plan.progress"
|
|
Then the event should have a non-empty event_id
|
|
And the event should have a non-empty timestamp
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Error hierarchy
|
|
# -----------------------------------------------------------------------
|
|
|
|
Scenario: AcpError is a CleverAgentsError
|
|
Then AcpError should be a subclass of CleverAgentsError
|
|
|
|
Scenario: AcpNotAvailableError is an AcpError
|
|
Then AcpNotAvailableError should be a subclass of AcpError
|
|
|
|
Scenario: AcpVersionMismatchError is an AcpError
|
|
Then AcpVersionMismatchError should be a subclass of AcpError
|
|
|
|
Scenario: AcpOperationNotFoundError is an AcpError
|
|
Then AcpOperationNotFoundError should be a subclass of AcpError
|
|
|
|
Scenario: AcpNotAvailableError has default message
|
|
When I create an AcpNotAvailableError with default message
|
|
Then the error message should contain "not available in local mode"
|