d3b9b8c8b8
Implement A2A Agent Card discovery per ADR-047 and issue #867: - AgentCard Pydantic model (agent_card.py) with nested models for skills, capabilities, extensions, security schemes, and interfaces. - Factory function build_agent_card() enumerates supported operations from the facade and maps them to A2A skills (plan-lifecycle, registry-crud, context-mgmt, entity-sync, namespace-mgmt, health-diagnostics). - Version negotiation: supportedVersions and version fields from A2aVersion constants. - A2A conformance validation (validate_agent_card_conformance) checks required fields, version support, and structural integrity. - Updated ASGI app to build the Agent Card from the facade model instead of a raw dict, with conformance validation at startup. - Behave BDD tests: 34 scenarios covering model construction, field validation, conformance checks, serialization, endpoint responses, and skill enumeration from facade operations. - Robot Framework integration tests: 6 test cases for build, conformance, endpoint, serialization, skill enumeration, and version info. - Updated A2A package exports and CHANGELOG. ISSUES CLOSED: #867
146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Integration test helper for Agent Card discovery tests.
|
|
|
|
Each sub-command exercises a real import / instantiation path and prints
|
|
a unique sentinel on success. Robot tests parse the output for that
|
|
sentinel.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
|
|
|
|
def _test_build_card() -> None:
|
|
"""Build an Agent Card from a facade and verify structure."""
|
|
from cleveragents.a2a.agent_card import build_agent_card
|
|
from cleveragents.a2a.facade import A2aLocalFacade
|
|
|
|
facade = A2aLocalFacade()
|
|
card = build_agent_card(facade=facade)
|
|
assert card.name == "CleverAgents"
|
|
assert card.version == "1.0"
|
|
assert card.url
|
|
assert len(card.skills) > 0
|
|
assert len(card.extensions) > 0
|
|
assert len(card.supportedVersions) > 0
|
|
assert len(card.supportedOperations) > 0
|
|
print("build-card-ok")
|
|
|
|
|
|
def _test_conformance() -> None:
|
|
"""Validate Agent Card A2A conformance."""
|
|
from cleveragents.a2a.agent_card import (
|
|
build_agent_card,
|
|
validate_agent_card_conformance,
|
|
)
|
|
from cleveragents.a2a.facade import A2aLocalFacade
|
|
|
|
facade = A2aLocalFacade()
|
|
card = build_agent_card(facade=facade)
|
|
violations = validate_agent_card_conformance(card)
|
|
assert violations == [], f"Conformance violations: {violations}"
|
|
print("conformance-ok")
|
|
|
|
|
|
def _test_endpoint() -> None:
|
|
"""Exercise the /.well-known/agent.json endpoint via TestClient."""
|
|
from fastapi.testclient import TestClient
|
|
|
|
from cleveragents.a2a.facade import A2aLocalFacade
|
|
from cleveragents.infrastructure.server.asgi_app import create_asgi_app
|
|
|
|
facade = A2aLocalFacade()
|
|
app = create_asgi_app(facade=facade)
|
|
client = TestClient(app)
|
|
|
|
resp = client.get("/.well-known/agent.json")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["name"] == "CleverAgents"
|
|
assert "skills" in data
|
|
assert "supportedVersions" in data
|
|
assert "supportedOperations" in data
|
|
assert isinstance(data["skills"], list)
|
|
assert len(data["skills"]) > 0
|
|
assert isinstance(data["supportedOperations"], list)
|
|
assert len(data["supportedOperations"]) > 0
|
|
# Verify all operations have the correct prefix
|
|
for op in data["supportedOperations"]:
|
|
assert op.startswith("_cleveragents/"), f"Bad prefix: {op}"
|
|
print("endpoint-ok")
|
|
|
|
|
|
def _test_serialization() -> None:
|
|
"""Verify Agent Card serializes to valid JSON dict."""
|
|
from cleveragents.a2a.agent_card import (
|
|
agent_card_to_dict,
|
|
build_agent_card,
|
|
)
|
|
from cleveragents.a2a.facade import A2aLocalFacade
|
|
|
|
facade = A2aLocalFacade()
|
|
card = build_agent_card(facade=facade)
|
|
data = agent_card_to_dict(card)
|
|
# Round-trip through JSON to ensure serializability
|
|
serialized = json.dumps(data)
|
|
parsed = json.loads(serialized)
|
|
assert parsed["name"] == "CleverAgents"
|
|
assert "skills" in parsed
|
|
assert "supportedOperations" in parsed
|
|
print("serialization-ok")
|
|
|
|
|
|
def _test_skill_enumeration() -> None:
|
|
"""Verify skills are correctly enumerated from facade operations."""
|
|
from cleveragents.a2a.agent_card import build_agent_card
|
|
from cleveragents.a2a.facade import A2aLocalFacade
|
|
|
|
facade = A2aLocalFacade()
|
|
card = build_agent_card(facade=facade)
|
|
skill_ids = [s.id for s in card.skills]
|
|
assert "plan-lifecycle" in skill_ids, f"Missing plan-lifecycle: {skill_ids}"
|
|
assert "registry-crud" in skill_ids, f"Missing registry-crud: {skill_ids}"
|
|
assert "context-mgmt" in skill_ids, f"Missing context-mgmt: {skill_ids}"
|
|
assert "health-diagnostics" in skill_ids, f"Missing health-diagnostics: {skill_ids}"
|
|
print("skill-enumeration-ok")
|
|
|
|
|
|
def _test_version_info() -> None:
|
|
"""Verify version negotiation information in Agent Card."""
|
|
from cleveragents.a2a.agent_card import build_agent_card
|
|
from cleveragents.a2a.facade import A2aLocalFacade
|
|
from cleveragents.a2a.models import A2aVersion
|
|
|
|
facade = A2aLocalFacade()
|
|
card = build_agent_card(facade=facade)
|
|
assert card.version == A2aVersion.CURRENT
|
|
assert A2aVersion.CURRENT in card.supportedVersions
|
|
for v in A2aVersion.SUPPORTED:
|
|
assert v in card.supportedVersions
|
|
print("version-info-ok")
|
|
|
|
|
|
_COMMANDS: dict[str, object] = {
|
|
"build-card": _test_build_card,
|
|
"conformance": _test_conformance,
|
|
"endpoint": _test_endpoint,
|
|
"serialization": _test_serialization,
|
|
"skill-enumeration": _test_skill_enumeration,
|
|
"version-info": _test_version_info,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
|
|
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
handler = _COMMANDS[sys.argv[1]]
|
|
handler() # type: ignore[operator]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|