"""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=[], )