e4224eb8ec
- 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
443 lines
15 KiB
Python
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=[],
|
|
)
|