"""Step definitions for server client stubs Behave scenarios.""" from __future__ import annotations import os import tempfile from behave import given, then, use_step_matcher, when from behave.runner import Context from pydantic import ValidationError from cleveragents.a2a.clients import ( AuthClient, RemoteExecutionClient, ServerClient, StubAuthClient, StubRemoteExecutionClient, StubServerClient, ) from cleveragents.a2a.server_config import ServerConnectionConfig from cleveragents.application.services.config_service import _REGISTRY use_step_matcher("re") # --------------------------------------------------------------------------- # ServerConnectionConfig — creation # --------------------------------------------------------------------------- @when(r'I create a ServerConnectionConfig with url "(?P[^"]+)"') def step_create_config(context: Context, url: str) -> None: context.server_config = ServerConnectionConfig(server_url=url) @when( r'I create a ServerConnectionConfig with url "(?P[^"]+)" ' r'and namespace "(?P[^"]+)"' ) def step_create_config_ns(context: Context, url: str, ns: str) -> None: context.server_config = ServerConnectionConfig(server_url=url, namespace=ns) @when( r'I create a ServerConnectionConfig with url "(?P[^"]+)" ' r'and auth_token_ref "(?P[^"]+)"' ) def step_create_config_auth(context: Context, url: str, ref: str) -> None: context.server_config = ServerConnectionConfig(server_url=url, auth_token_ref=ref) @when( r'I create a ServerConnectionConfig with url "(?P[^"]+)" ' r"and tls_verify false" ) def step_create_config_tls_false(context: Context, url: str) -> None: context.server_config = ServerConnectionConfig(server_url=url, tls_verify=False) # --------------------------------------------------------------------------- # ServerConnectionConfig — assertions # --------------------------------------------------------------------------- @then(r'the config server_url should be "(?P[^"]+)"') def step_config_url(context: Context, expected: str) -> None: assert context.server_config.server_url == expected, ( f"Expected '{expected}', got '{context.server_config.server_url}'" ) @then(r'the config namespace should be "(?P[^"]+)"') def step_config_namespace(context: Context, expected: str) -> None: assert context.server_config.namespace == expected, ( f"Expected '{expected}', got '{context.server_config.namespace}'" ) @then(r'the config auth_token_ref should be "(?P[^"]+)"') def step_config_auth_ref(context: Context, expected: str) -> None: assert context.server_config.auth_token_ref == expected, ( f"Expected '{expected}', got '{context.server_config.auth_token_ref}'" ) @then(r"the config tls_verify should be true") def step_config_tls_true(context: Context) -> None: assert context.server_config.tls_verify is True @then(r"the config tls_verify should be false") def step_config_tls_false(context: Context) -> None: assert context.server_config.tls_verify is False @then(r"the config should be immutable") def step_config_immutable(context: Context) -> None: cfg = context.server_config raised = False try: cfg.__setattr__("server_url", "http://changed.example.com") except (ValidationError, AttributeError, TypeError): raised = True assert raised, "Setting attribute on frozen model should have raised an error" # --------------------------------------------------------------------------- # ServerConnectionConfig — validation errors # --------------------------------------------------------------------------- @when(r"I try to create a ServerConnectionConfig with empty url") def step_create_config_empty_url(context: Context) -> None: context.caught_error = None try: ServerConnectionConfig(server_url="") except ValidationError as exc: context.caught_error = exc @when(r'I try to create a ServerConnectionConfig with url "(?P[^"]+)"') def step_create_config_invalid_url(context: Context, url: str) -> None: context.caught_error = None try: ServerConnectionConfig(server_url=url) except ValidationError as exc: context.caught_error = exc @when( r'I try to create a ServerConnectionConfig with url "(?P[^"]+)" ' r"and empty namespace" ) def step_create_config_empty_ns(context: Context, url: str) -> None: context.caught_error = None try: ServerConnectionConfig(server_url=url, namespace="") except ValidationError as exc: context.caught_error = exc @then(r"a server config validation error should be raised") def step_server_config_validation_error(context: Context) -> None: assert context.caught_error is not None, "Expected a ValidationError" assert isinstance(context.caught_error, ValidationError), ( f"Expected ValidationError, got {type(context.caught_error)}" ) # --------------------------------------------------------------------------- # StubServerClient # --------------------------------------------------------------------------- @given(r"a new StubServerClient") def step_stub_server_client(context: Context) -> None: context.stub_server = StubServerClient() @when(r"I call health_check on the stub server client") def step_call_health_check(context: Context) -> None: context.caught_error = None context.ctx_error = None try: context.stub_server.health_check() except NotImplementedError as exc: context.caught_error = exc context.ctx_error = exc @when(r"I call get_version on the stub server client") def step_call_get_version(context: Context) -> None: context.caught_error = None context.ctx_error = None try: context.stub_server.get_version() except NotImplementedError as exc: context.caught_error = exc context.ctx_error = exc # --------------------------------------------------------------------------- # StubRemoteExecutionClient # --------------------------------------------------------------------------- @given(r"a new StubRemoteExecutionClient") def step_stub_exec_client(context: Context) -> None: context.stub_exec = StubRemoteExecutionClient() @when( r'I call execute_plan with plan_id "(?P[^"]+)" ' r"on the stub execution client" ) def step_call_execute_plan(context: Context, plan_id: str) -> None: context.caught_error = None context.ctx_error = None try: context.stub_exec.execute_plan(plan_id) except NotImplementedError as exc: context.caught_error = exc context.ctx_error = exc @when( r'I call get_plan_status with plan_id "(?P[^"]+)" ' r"on the stub execution client" ) def step_call_get_plan_status(context: Context, plan_id: str) -> None: context.caught_error = None context.ctx_error = None try: context.stub_exec.get_plan_status(plan_id) except NotImplementedError as exc: context.caught_error = exc context.ctx_error = exc @when( r'I call cancel_plan with plan_id "(?P[^"]+)" ' r"on the stub execution client" ) def step_call_cancel_plan(context: Context, plan_id: str) -> None: context.caught_error = None context.ctx_error = None try: context.stub_exec.cancel_plan(plan_id) except NotImplementedError as exc: context.caught_error = exc context.ctx_error = exc @when(r"I call execute_plan with empty plan_id on the stub execution client") def step_call_execute_plan_empty(context: Context) -> None: context.caught_error = None try: context.stub_exec.execute_plan("") except ValueError as exc: context.caught_error = exc # --------------------------------------------------------------------------- # StubAuthClient # --------------------------------------------------------------------------- @given(r"a new StubAuthClient") def step_stub_auth_client(context: Context) -> None: context.stub_auth = StubAuthClient() @when(r'I call authenticate with token "(?P[^"]+)" on the stub auth client') def step_call_authenticate(context: Context, token: str) -> None: context.caught_error = None context.ctx_error = None try: context.stub_auth.authenticate(token) except NotImplementedError as exc: context.caught_error = exc context.ctx_error = exc @when(r'I call validate_token with token "(?P[^"]+)" on the stub auth client') def step_call_validate_token(context: Context, token: str) -> None: context.caught_error = None context.ctx_error = None try: context.stub_auth.validate_token(token) except NotImplementedError as exc: context.caught_error = exc context.ctx_error = exc @when(r"I call authenticate with empty token on the stub auth client") def step_call_authenticate_empty(context: Context) -> None: context.caught_error = None try: context.stub_auth.authenticate("") except ValueError as exc: context.caught_error = exc # --------------------------------------------------------------------------- # NotImplementedError and ValueError assertions # --------------------------------------------------------------------------- # Note: the step "a NotImplementedError should be raised with message # containing ..." is already defined in project_context_cli_steps.py # and looks at context.ctx_error. Our stub steps store errors in # context.caught_error, so we bridge by also writing to ctx_error # in each step that catches NotImplementedError. @then(r"a ValueError should be raised for empty plan_id") def step_value_error_plan_id(context: Context) -> None: assert context.caught_error is not None, "Expected ValueError" assert isinstance(context.caught_error, ValueError), ( f"Expected ValueError, got {type(context.caught_error)}" ) @then(r"a ValueError should be raised for empty token") def step_value_error_token(context: Context) -> None: assert context.caught_error is not None, "Expected ValueError" assert isinstance(context.caught_error, ValueError), ( f"Expected ValueError, got {type(context.caught_error)}" ) # --------------------------------------------------------------------------- # Protocol conformance # --------------------------------------------------------------------------- @then(r"StubServerClient should be a runtime instance of ServerClient") def step_server_protocol(context: Context) -> None: assert isinstance(StubServerClient(), ServerClient) @then( r"StubRemoteExecutionClient should be a runtime instance of RemoteExecutionClient" ) def step_exec_protocol(context: Context) -> None: assert isinstance(StubRemoteExecutionClient(), RemoteExecutionClient) @then(r"StubAuthClient should be a runtime instance of AuthClient") def step_auth_protocol(context: Context) -> None: assert isinstance(StubAuthClient(), AuthClient) # --------------------------------------------------------------------------- # Config key registration # --------------------------------------------------------------------------- @then(r'config key "(?P[^"]+)" should be registered') def step_config_key_registered(context: Context, key: str) -> None: assert key in _REGISTRY, f"Config key '{key}' not found in registry" @then(r'config key "(?P[^"]+)" should have default true') def step_config_key_default_true(context: Context, key: str) -> None: entry = _REGISTRY[key] assert entry.default is True, f"Expected True, got {entry.default}" @then(r'config key "(?P[^"]+)" should have default "(?P[^"]+)"') def step_config_key_default_str(context: Context, key: str, default: str) -> None: entry = _REGISTRY[key] assert str(entry.default) == default, f"Expected '{default}', got '{entry.default}'" @then(r'config key "(?P[^"]+)" should use env var "(?P[^"]+)"') def step_config_key_env_var(context: Context, key: str, env_var: str) -> None: entry = _REGISTRY[key] assert entry.env_var == env_var, ( f"Expected env var '{env_var}', got '{entry.env_var}'" ) # --------------------------------------------------------------------------- # CLI integration (isolated — uses a temporary HOME to avoid config bleed) # --------------------------------------------------------------------------- @given(r"a temporary home directory for server tests") def step_tmp_home_for_server(context: Context) -> None: """Set HOME to a fresh temporary directory so config writes are isolated.""" context.server_test_home = tempfile.mkdtemp() context.server_test_old_home = os.environ.get("HOME") os.environ["HOME"] = context.server_test_home @when( r'I invoke server connect with url "(?P[^"]+)" ' r'and format "(?P[^"]+)"' ) def step_invoke_server_connect(context: Context, url: str, fmt: str) -> None: """Run ``server connect --format `` and capture output.""" import io import sys from cleveragents.cli.main import main as cli_main captured = io.StringIO() old_stdout = sys.stdout sys.stdout = captured try: context.server_connect_exit = cli_main( ["server", "connect", url, "--format", fmt] ) except SystemExit as exc: context.server_connect_exit = exc.code if exc.code is not None else 0 finally: sys.stdout = old_stdout # Restore HOME old_home = getattr(context, "server_test_old_home", None) if old_home is not None: os.environ["HOME"] = old_home else: os.environ.pop("HOME", None) context.server_connect_output = captured.getvalue() @then(r'the server connect output should contain "(?P[^"]+)"') def step_server_connect_output_contains(context: Context, text: str) -> None: assert text in context.server_connect_output, ( f"Expected '{text}' in output: {context.server_connect_output!r}" ) @then(r"the server connect should exit with non-zero code") def step_server_connect_non_zero(context: Context) -> None: assert context.server_connect_exit != 0, ( f"Expected non-zero exit, got {context.server_connect_exit}" ) # --------------------------------------------------------------------------- # Server mode resolution # --------------------------------------------------------------------------- @given(r"no server URL is configured") def step_no_server_url(context: Context) -> None: # Ensure the env var is not set and use a clean temp HOME os.environ.pop("CLEVERAGENTS_SERVER_URL", None) context.clean_home = tempfile.mkdtemp() context.old_home = os.environ.get("HOME") os.environ["HOME"] = context.clean_home @then(r'resolve_server_mode should return "(?P[^"]+)"') def step_resolve_server_mode(context: Context, mode: str) -> None: from cleveragents.cli.commands.server import resolve_server_mode try: result = resolve_server_mode() assert result == mode, f"Expected '{mode}', got '{result}'" finally: old_home = getattr(context, "old_home", None) if old_home is not None: os.environ["HOME"] = old_home else: os.environ.pop("HOME", None) # Reset step matcher to parse (default) so subsequent step files are not affected use_step_matcher("parse")