feat(interfaces): add server client stubs
CI / lint (pull_request) Successful in 18s
CI / benchmark-publish (pull_request) Has been skipped
CI / quality (pull_request) Successful in 20s
CI / typecheck (pull_request) Successful in 30s
CI / build (pull_request) Successful in 15s
CI / security (pull_request) Successful in 29s
CI / unit_tests (pull_request) Successful in 1m59s
CI / integration_tests (pull_request) Successful in 3m35s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 3m39s
CI / lint (push) Successful in 24s
CI / security (push) Successful in 29s
CI / typecheck (push) Successful in 38s
CI / quality (push) Successful in 32s
CI / build (push) Successful in 15s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m5s
CI / integration_tests (push) Successful in 4m42s
CI / docker (push) Successful in 42s
CI / coverage (push) Successful in 6m53s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 24m12s
CI / lint (pull_request) Successful in 18s
CI / benchmark-publish (pull_request) Has been skipped
CI / quality (pull_request) Successful in 20s
CI / typecheck (pull_request) Successful in 30s
CI / build (pull_request) Successful in 15s
CI / security (pull_request) Successful in 29s
CI / unit_tests (pull_request) Successful in 1m59s
CI / integration_tests (pull_request) Successful in 3m35s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 3m39s
CI / lint (push) Successful in 24s
CI / security (push) Successful in 29s
CI / typecheck (push) Successful in 38s
CI / quality (push) Successful in 32s
CI / build (push) Successful in 15s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m5s
CI / integration_tests (push) Successful in 4m42s
CI / docker (push) Successful in 42s
CI / coverage (push) Successful in 6m53s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 24m12s
Add protocol stubs for remote server communication infrastructure: - ServerClient: Health check and version negotiation protocol - RemoteExecutionClient: Remote plan execution and status protocol - AuthClient: Authentication and token management protocol - StubServerClient/StubRemoteExecutionClient/StubAuthClient: Stub implementations raising NotImplementedError for all methods - ServerConnectionConfig: Validated config model (server_url, namespace, auth_token_ref, tls_verify) - Config keys: core.server_url, core.server_namespace, core.server_tls_verify - CLI: agents connect <url> stub command with explicit warning - CLI: agents info shows Server Mode (disabled/stubbed) ISSUES CLOSED: #201
This commit was merged in pull request #507.
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
"""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.acp.clients import (
|
||||
AuthClient,
|
||||
RemoteExecutionClient,
|
||||
ServerClient,
|
||||
StubAuthClient,
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.acp.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<url>[^"]+)"')
|
||||
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<url>[^"]+)" '
|
||||
r'and namespace "(?P<ns>[^"]+)"'
|
||||
)
|
||||
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<url>[^"]+)" '
|
||||
r'and auth_token_ref "(?P<ref>[^"]+)"'
|
||||
)
|
||||
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<url>[^"]+)" '
|
||||
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<expected>[^"]+)"')
|
||||
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<expected>[^"]+)"')
|
||||
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<expected>[^"]+)"')
|
||||
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<url>[^"]+)"')
|
||||
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<url>[^"]+)" '
|
||||
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<plan_id>[^"]+)" '
|
||||
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<plan_id>[^"]+)" '
|
||||
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<plan_id>[^"]+)" '
|
||||
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<token>[^"]+)" 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<token>[^"]+)" 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<key>[^"]+)" 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<key>[^"]+)" 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<key>[^"]+)" should have default "(?P<default>[^"]+)"')
|
||||
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<key>[^"]+)" should use env var "(?P<env_var>[^"]+)"')
|
||||
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<url>[^"]+)" '
|
||||
r'and format "(?P<fmt>[^"]+)"'
|
||||
)
|
||||
def step_invoke_server_connect(context: Context, url: str, fmt: str) -> None:
|
||||
"""Run ``server connect <url> --format <fmt>`` 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<text>[^"]+)"')
|
||||
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<mode>[^"]+)"')
|
||||
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)
|
||||
Reference in New Issue
Block a user