Files
cleveragents-core/features/steps/agent_card_steps.py
T
HAL9000 e4224eb8ec fix(a2a): resolve JSON-RPC protocol conformance and code quality issues
- Fix malformed JSON parse error: wrap request.json() in try/except,
  return -32700 Parse error instead of unhandled HTTP 500
- Fix JSON-RPC error responses: add required 'id' field to all error
  responses per JSON-RPC 2.0 Section 5
- Fix HTTP status codes: return HTTP 200 for all JSON-RPC responses
  (error codes expressed in JSON body per JSON-RPC 2.0 over HTTP)
- Fix error message leakage: return generic messages instead of raw
  Pydantic ValidationError internals (CWE-209)
- Add catch-all exception handler returning -32603 Internal error
- Fix Agent Card url field: use base server URL without /a2a suffix;
  interfaces[0].url correctly uses endpoint URL with /a2a suffix
- Fix 0.0.0.0 host: substitute 127.0.0.1 for Agent Card URL
- Fix context typing: replace context: Any with context: Context in
  all step files per project convention
- Fix inline imports: move all imports to top of step files
- Fix _COMMANDS typing: use dict[str, Callable[[], None]] to avoid
  type: ignore suppression in helper files
- Add BDD scenarios for entity-sync and namespace-mgmt skill enumeration
- Update server_lifecycle.feature: correct HTTP status assertions to 200

ISSUES CLOSED: #867
2026-06-11 19:58:52 -04:00

443 lines
15 KiB
Python

"""Step definitions for ``agent_card.feature``.
Tests Agent Card model construction, conformance validation,
serialization, discovery endpoint, and skill enumeration.
"""
from __future__ import annotations
from behave import given, then, when # type: ignore[import-untyped]
from behave.runner import Context
from fastapi.testclient import TestClient
from pydantic import ValidationError
from cleveragents.a2a.agent_card import (
AgentCard,
AgentCardExtension,
AgentCardInterface,
AgentCardSecurityScheme,
AgentCardSkill,
agent_card_to_dict,
build_agent_card,
validate_agent_card_conformance,
)
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.infrastructure.server.asgi_app import create_asgi_app
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given("an A2aLocalFacade for agent card testing")
def step_given_facade_for_card(context: Context) -> None:
context.facade = A2aLocalFacade()
@given("an agent card with empty url")
def step_given_card_empty_url(context: Context) -> None:
context.agent_card = AgentCard(
name="Test",
version="1.0",
url="",
)
@given('an agent card with version "{version}"')
def step_given_card_version(context: Context, version: str) -> None:
context.agent_card = AgentCard(
name="Test",
version=version,
url="http://localhost/a2a",
)
@given("an agent card with empty supported versions")
def step_given_card_empty_supported_versions(context: Context) -> None:
context.agent_card = AgentCard(
name="Test",
version="1.0",
url="http://localhost/a2a",
supportedVersions=[],
)
# ---------------------------------------------------------------------------
# When steps -- build
# ---------------------------------------------------------------------------
@when("I build an agent card from the facade")
def step_build_card_from_facade(context: Context) -> None:
context.agent_card = build_agent_card(facade=context.facade)
@when("I build an agent card without a facade")
def step_build_card_no_facade(context: Context) -> None:
context.agent_card = build_agent_card()
@when("I try to build an agent card with an empty host")
def step_try_build_card_empty_host(context: Context) -> None:
context.caught_exception = None
try:
build_agent_card(host="")
except ValueError as exc:
context.caught_exception = exc
@when("I try to build an agent card with port {port:d}")
def step_try_build_card_bad_port(context: Context, port: int) -> None:
context.caught_exception = None
try:
build_agent_card(port=port)
except ValueError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# When steps -- model validation
# ---------------------------------------------------------------------------
@when("I try to create an AgentCard with empty name")
def step_try_card_empty_name(context: Context) -> None:
context.caught_exception = None
try:
AgentCard(name="", version="1.0")
except ValidationError as exc:
context.caught_exception = exc
@when("I try to create an AgentCard with empty version")
def step_try_card_empty_version(context: Context) -> None:
context.caught_exception = None
try:
AgentCard(name="Test", version="")
except ValidationError as exc:
context.caught_exception = exc
@when("I try to create an AgentCardSkill with empty id")
def step_try_skill_empty_id(context: Context) -> None:
context.caught_exception = None
try:
AgentCardSkill(id="", name="Test")
except ValidationError as exc:
context.caught_exception = exc
@when("I try to create an AgentCardSkill with empty skill name")
def step_try_skill_empty_name(context: Context) -> None:
context.caught_exception = None
try:
AgentCardSkill(id="test", name="")
except ValidationError as exc:
context.caught_exception = exc
@when("I try to create an AgentCardExtension with empty uri")
def step_try_extension_empty_uri(context: Context) -> None:
context.caught_exception = None
try:
AgentCardExtension(uri="")
except ValidationError as exc:
context.caught_exception = exc
@when("I try to create an AgentCardSecurityScheme with empty type")
def step_try_security_empty_type(context: Context) -> None:
context.caught_exception = None
try:
AgentCardSecurityScheme(type="")
except ValidationError as exc:
context.caught_exception = exc
@when("I try to create an AgentCardInterface with empty protocol")
def step_try_interface_empty_protocol(context: Context) -> None:
context.caught_exception = None
try:
AgentCardInterface(protocol="")
except ValidationError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# When steps -- conformance validation
# ---------------------------------------------------------------------------
@when("I validate the agent card conformance")
def step_validate_conformance(context: Context) -> None:
context.violations = validate_agent_card_conformance(context.agent_card)
@when("I try to validate conformance of a non-AgentCard object")
def step_try_validate_non_card(context: Context) -> None:
context.caught_exception = None
try:
validate_agent_card_conformance("not-a-card") # type: ignore[arg-type]
except TypeError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# When steps -- serialization
# ---------------------------------------------------------------------------
@when("I serialize the agent card to dict")
def step_serialize_card(context: Context) -> None:
context.card_dict = agent_card_to_dict(context.agent_card)
@when("I try to serialize a non-AgentCard object")
def step_try_serialize_non_card(context: Context) -> None:
context.caught_exception = None
try:
agent_card_to_dict("not-a-card") # type: ignore[arg-type]
except TypeError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# When steps -- endpoint
# ---------------------------------------------------------------------------
@given("a running ASGI test client with facade")
def step_given_test_client_with_facade(context: Context) -> None:
facade = A2aLocalFacade()
app = create_asgi_app(facade=facade)
context.test_client = TestClient(app)
@when("I request GET /.well-known/agent.json and parse JSON")
def step_get_agent_card_parse(context: Context) -> None:
context.response = context.test_client.get("/.well-known/agent.json")
context.card_json = context.response.json()
# ---------------------------------------------------------------------------
# Then steps -- card properties
# ---------------------------------------------------------------------------
@then('the agent card name should be "{expected}"')
def step_check_card_name(context: Context, expected: str) -> None:
assert context.agent_card.name == expected, (
f"Expected name '{expected}', got '{context.agent_card.name}'"
)
@then('the agent card version should be "{expected}"')
def step_check_card_version(context: Context, expected: str) -> None:
assert context.agent_card.version == expected, (
f"Expected version '{expected}', got '{context.agent_card.version}'"
)
@then("the agent card should have a non-empty url")
def step_check_card_url(context: Context) -> None:
assert context.agent_card.url, "Expected non-empty url"
@then("the agent card should have at least {count:d} skill")
@then("the agent card should have at least {count:d} skills")
def step_check_card_skills_count(context: Context, count: int) -> None:
actual = len(context.agent_card.skills)
assert actual >= count, f"Expected at least {count} skills, got {actual}"
@then("the agent card should have {count:d} skills")
def step_check_card_exact_skills(context: Context, count: int) -> None:
actual = len(context.agent_card.skills)
assert actual == count, f"Expected {count} skills, got {actual}"
@then("the agent card should have at least {count:d} extension")
@then("the agent card should have at least {count:d} extensions")
def step_check_card_extensions_count(context: Context, count: int) -> None:
actual = len(context.agent_card.extensions)
assert actual >= count, f"Expected at least {count} extensions, got {actual}"
@then("the agent card should have at least {count:d} security scheme")
@then("the agent card should have at least {count:d} security schemes")
def step_check_card_security_count(context: Context, count: int) -> None:
actual = len(context.agent_card.securitySchemes)
assert actual >= count, f"Expected at least {count} security schemes, got {actual}"
@then("the agent card should have at least {count:d} interface")
@then("the agent card should have at least {count:d} interfaces")
def step_check_card_interfaces_count(context: Context, count: int) -> None:
actual = len(context.agent_card.interfaces)
assert actual >= count, f"Expected at least {count} interfaces, got {actual}"
@then('the first interface protocol should be "{expected}"')
def step_check_first_interface(context: Context, expected: str) -> None:
assert context.agent_card.interfaces, "No interfaces found"
assert context.agent_card.interfaces[0].protocol == expected
@then('the agent card supported versions should contain "{version}"')
def step_check_supported_versions(context: Context, version: str) -> None:
assert version in context.agent_card.supportedVersions, (
f"'{version}' not in {context.agent_card.supportedVersions}"
)
@then("the agent card should list extension operations")
def step_check_extension_ops(context: Context) -> None:
assert context.agent_card.supportedOperations, (
"Expected non-empty supportedOperations"
)
for op in context.agent_card.supportedOperations:
assert op.startswith("_cleveragents/"), f"Operation '{op}' has wrong prefix"
@then('the agent card default input modes should contain "{mode}"')
def step_check_input_modes(context: Context, mode: str) -> None:
assert mode in context.agent_card.defaultInputModes
@then('the agent card default output modes should contain "{mode}"')
def step_check_output_modes(context: Context, mode: str) -> None:
assert mode in context.agent_card.defaultOutputModes
@then("the agent card streaming capability should be false")
def step_check_streaming_false(context: Context) -> None:
assert context.agent_card.capabilities.streaming is False
@then("the agent card push notifications capability should be false")
def step_check_push_false(context: Context) -> None:
assert context.agent_card.capabilities.pushNotifications is False
# ---------------------------------------------------------------------------
# Then steps -- validation errors
# ---------------------------------------------------------------------------
@then("a ValueError should be raised for agent card")
def step_check_value_error_card(context: Context) -> None:
assert context.caught_exception is not None, "Expected ValueError"
assert isinstance(context.caught_exception, ValueError)
@then("a validation error should be raised for agent card")
def step_check_validation_error_card(context: Context) -> None:
assert context.caught_exception is not None, "Expected ValidationError"
assert isinstance(context.caught_exception, ValidationError)
@then("a TypeError should be raised for conformance validation")
def step_check_type_error_conformance(context: Context) -> None:
assert context.caught_exception is not None, "Expected TypeError"
assert isinstance(context.caught_exception, TypeError)
@then("a TypeError should be raised for serialization")
def step_check_type_error_serialization(context: Context) -> None:
assert context.caught_exception is not None, "Expected TypeError"
assert isinstance(context.caught_exception, TypeError)
# ---------------------------------------------------------------------------
# Then steps -- conformance
# ---------------------------------------------------------------------------
@then("the conformance violations should be empty")
def step_check_no_violations(context: Context) -> None:
assert context.violations == [], (
f"Expected no violations, got: {context.violations}"
)
@then('the conformance violations should include "{text}"')
def step_check_violation_text(context: Context, text: str) -> None:
joined = " | ".join(context.violations)
assert any(text in v for v in context.violations), (
f"Expected violation containing '{text}', got: {joined}"
)
# ---------------------------------------------------------------------------
# Then steps -- serialization
# ---------------------------------------------------------------------------
@then('the serialized dict should have key "{key}"')
def step_check_dict_key(context: Context, key: str) -> None:
assert key in context.card_dict, (
f"Key '{key}' not in serialized dict: {list(context.card_dict.keys())}"
)
# ---------------------------------------------------------------------------
# Then steps -- JSON endpoint
# ---------------------------------------------------------------------------
@then('the agent card JSON should have key "{key}" with value "{value}"')
def step_check_json_key_value(context: Context, key: str, value: str) -> None:
actual = context.card_json.get(key)
assert actual == value, f"Expected '{key}'='{value}', got '{actual}'"
@then('the agent card JSON "{key}" should be a dict')
def step_check_json_dict(context: Context, key: str) -> None:
val = context.card_json.get(key)
assert isinstance(val, dict), f"Expected dict for '{key}', got {type(val)}"
@then('the agent card JSON "{key}" should be a non-empty list')
def step_check_json_list(context: Context, key: str) -> None:
val = context.card_json.get(key)
assert isinstance(val, list), f"Expected list for '{key}', got {type(val)}"
assert len(val) > 0, f"Expected non-empty list for '{key}'"
@then('all supported operations should start with "_cleveragents/"')
def step_check_ops_prefix(context: Context) -> None:
ops = context.card_json.get("supportedOperations", [])
for op in ops:
assert op.startswith("_cleveragents/"), f"Operation '{op}' has wrong prefix"
# ---------------------------------------------------------------------------
# Then steps -- skill enumeration
# ---------------------------------------------------------------------------
@then('the agent card skills should include id "{skill_id}"')
def step_check_skill_id(context: Context, skill_id: str) -> None:
ids = [s.id for s in context.agent_card.skills]
assert skill_id in ids, f"Skill '{skill_id}' not in {ids}"
@given("an agent card with empty default input modes")
def step_given_card_empty_input_modes(context: Context) -> None:
context.agent_card = AgentCard(
name="Test",
version="1.0",
url="http://localhost/a2a",
defaultInputModes=[],
)
@given("an agent card with empty default output modes")
def step_given_card_empty_output_modes(context: Context) -> None:
context.agent_card = AgentCard(
name="Test",
version="1.0",
url="http://localhost/a2a",
defaultOutputModes=[],
)