forked from cleveragents/cleveragents-core
e13308b322
Implement the server application module (ADR-048) with: - FastAPI application factory (app.py) exposing the A2A JSON-RPC 2.0 endpoint as the sole client-facing interface. Routes: POST /a2a (JSON-RPC dispatch), GET /.well-known/agent.json (Agent Card), and GET /health (container orchestration). - RemoteGraph adapter (remote_graph.py) for invoking actor graphs via LangGraph Platform. Each actor graph deploys as a separate RemoteGraph enabling independent scaling. Includes health tracking, invocation counting, and a RemoteGraphRegistry for managing multiple graph deployments. - Database configuration (database.py) supporting PostgreSQL for production and SQLite for development. Config-driven via CLEVERAGENTS_DATABASE_URL and CLEVERAGENTS_SERVER_ENV environment variables with validated Pydantic settings model. - Docker deployment (docker/Dockerfile + docker-compose.yml) with multi-stage build, non-root user, health checks, and PostgreSQL service. - FastAPI dependency added to pyproject.toml. - 26 Behave BDD scenarios covering app factory, A2A dispatch, Agent Card, health endpoint, RemoteGraph config/adapter/registry, and database configuration. - 10 Robot Framework integration tests with helper script verifying end-to-end server functionality. ISSUES CLOSED: #693
161 lines
6.5 KiB
Gherkin
161 lines
6.5 KiB
Gherkin
Feature: Server application with LangGraph Platform integration
|
|
As a CleverAgents server operator
|
|
I want the server to expose an A2A JSON-RPC 2.0 endpoint
|
|
So that clients can communicate via the A2A protocol over HTTP
|
|
|
|
# ---------------------------------------------------------------
|
|
# FastAPI application factory
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: create_app returns a FastAPI instance
|
|
When I create a server application
|
|
Then the app should be a FastAPI instance
|
|
|
|
Scenario: Server has the A2A endpoint
|
|
Given a server application
|
|
Then the app should have a POST /a2a route
|
|
|
|
Scenario: Server has the Agent Card endpoint
|
|
Given a server application
|
|
Then the app should have a GET /.well-known/agent.json route
|
|
|
|
Scenario: Server has the health endpoint
|
|
Given a server application
|
|
Then the app should have a GET /health route
|
|
|
|
Scenario: Server does not expose OpenAPI docs
|
|
Given a server application
|
|
Then the app should not expose /docs
|
|
And the app should not expose /redoc
|
|
And the app should not expose /openapi.json
|
|
|
|
# ---------------------------------------------------------------
|
|
# A2A JSON-RPC dispatch via server endpoint
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: A2A endpoint dispatches message/send
|
|
Given a server application with a test client
|
|
When I POST a JSON-RPC message/send request to /a2a
|
|
Then the response should contain a valid JSON-RPC result
|
|
And the result should have a task id
|
|
|
|
Scenario: A2A endpoint returns parse error for invalid JSON
|
|
Given a server application with a test client
|
|
When I POST invalid JSON to /a2a
|
|
Then the response should contain a JSON-RPC parse error
|
|
|
|
Scenario: A2A endpoint returns method not found for unknown method
|
|
Given a server application with a test client
|
|
When I POST a JSON-RPC request for method "unknown/method" to /a2a
|
|
Then the response should contain a JSON-RPC method not found error
|
|
|
|
Scenario: A2A endpoint returns 204 for notifications
|
|
Given a server application with a test client
|
|
When I POST a JSON-RPC notification to /a2a
|
|
Then the response status code should be 204
|
|
|
|
# ---------------------------------------------------------------
|
|
# Agent Card
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: Agent Card contains server name and capabilities
|
|
Given a server application with a test client
|
|
When I GET /.well-known/agent.json
|
|
Then the Agent Card should have name "CleverAgents Server"
|
|
And the Agent Card should have authentication schemes
|
|
|
|
# ---------------------------------------------------------------
|
|
# Health endpoint
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: Health endpoint returns status ok
|
|
Given a server application with a test client
|
|
When I GET /health
|
|
Then the health status should be "ok"
|
|
And the health response should have graph_count
|
|
|
|
# ---------------------------------------------------------------
|
|
# RemoteGraph adapter
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: RemoteGraphConfig validates platform URL
|
|
When I create a RemoteGraphConfig with an empty platform URL
|
|
Then a ValueError should be raised for platform URL
|
|
|
|
Scenario: RemoteGraphConfig validates graph name
|
|
When I create a RemoteGraphConfig with an empty graph name
|
|
Then a ValueError should be raised for graph name
|
|
|
|
Scenario: RemoteGraphConfig rejects non-HTTP URLs
|
|
When I create a RemoteGraphConfig with platform URL "ftp://bad"
|
|
Then a ValueError should be raised for platform URL
|
|
|
|
Scenario: RemoteGraphAdapter stores config
|
|
Given a RemoteGraphConfig for "test_graph" at "http://localhost:8123"
|
|
When I create a RemoteGraphAdapter with that config
|
|
Then the adapter graph_name should be "test_graph"
|
|
And the adapter platform_url should be "http://localhost:8123"
|
|
|
|
Scenario: RemoteGraphAdapter invoke raises without platform
|
|
Given a RemoteGraphAdapter for "test_graph"
|
|
When I invoke the adapter with sample state
|
|
Then a RemoteGraphError should be raised
|
|
|
|
Scenario: RemoteGraphAdapter health is healthy by default
|
|
Given a RemoteGraphAdapter for "test_graph"
|
|
Then the adapter health should be "healthy"
|
|
|
|
Scenario: RemoteGraphAdapter tracks invocation errors
|
|
Given a RemoteGraphAdapter for "test_graph"
|
|
When I invoke the adapter and catch the error
|
|
Then the adapter health should be "unhealthy"
|
|
And the adapter last_error should not be None
|
|
|
|
# ---------------------------------------------------------------
|
|
# RemoteGraph registry
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: RemoteGraphRegistry registers and retrieves adapters
|
|
Given an empty RemoteGraphRegistry
|
|
And a RemoteGraphAdapter for "strategy_actor"
|
|
When I register the adapter in the graph registry
|
|
Then the graph registry should have 1 adapter
|
|
And the graph registry should contain "strategy_actor"
|
|
|
|
Scenario: RemoteGraphRegistry lists graph names
|
|
Given a RemoteGraphRegistry with adapters "alpha" and "beta"
|
|
Then the registry graph list should be ["alpha", "beta"]
|
|
|
|
Scenario: RemoteGraphRegistry health summary
|
|
Given a RemoteGraphRegistry with adapters "alpha" and "beta"
|
|
Then the registry health summary should have keys "alpha" and "beta"
|
|
|
|
# ---------------------------------------------------------------
|
|
# Database configuration
|
|
# ---------------------------------------------------------------
|
|
|
|
Scenario: DatabaseConfig defaults to SQLite
|
|
When I create a default DatabaseConfig
|
|
Then the database URL should start with "sqlite://"
|
|
And is_sqlite should be True
|
|
And is_postgres should be False
|
|
|
|
Scenario: DatabaseConfig accepts PostgreSQL URL
|
|
When I create a DatabaseConfig with URL "postgresql://localhost/test"
|
|
Then the database URL should start with "postgresql://"
|
|
And is_sqlite should be False
|
|
And is_postgres should be True
|
|
|
|
Scenario: DatabaseConfig rejects empty URL
|
|
When I create a DatabaseConfig with an empty URL
|
|
Then a ValueError should be raised for database URL
|
|
|
|
Scenario: DatabaseConfig rejects invalid scheme
|
|
When I create a DatabaseConfig with URL "mysql://localhost/test"
|
|
Then a ValueError should be raised for database URL
|
|
|
|
Scenario: create_engine_from_config creates an engine for SQLite
|
|
Given a SQLite DatabaseConfig
|
|
When I create an engine from the config
|
|
Then the engine should not be None
|