Files
cleveragents-core/robot/helper_agent_card.py
T
freemo d3b9b8c8b8 feat(a2a): Agent Card discovery endpoint
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
2026-06-11 19:57:39 -04:00

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()