Files
cleveragents-core/features/steps/server_client_stubs_steps.py
T
freemo 02250473ad fix(ci): restore all CI quality gates to passing on master
Fix all failing CI quality gates (lint, unit_tests, format) without
suppressing any quality enforcement.

Root causes and fixes:

1. Format: features/steps/plan_namespaced_name_tdd_steps.py had trailing
   whitespace; fixed by running ruff format.

2. Unit tests - A2A JSON-RPC 2.0 migration (commit 9c6d6915) renamed
   A2aRequest fields (operation→method, request_id→id, a2a_version→jsonrpc)
   and A2aResponse fields (status+data→result, request_id→id) but did not
   update all step files and feature files:
   - a2a_jsonrpc_wire_format_steps.py: added use_step_matcher('re') and
     reset to 'parse' at end to prevent parallel test interference
   - a2a_facade_wiring_steps.py: updated operation= to method=, .status/.data
     to .result
   - a2a_facade_steps.py: updated request_id→id, a2a_version→jsonrpc,
     A2aResponse(request_id=..., status=...) to new API
   - m6_facade_steps.py: updated all old API usage
   - devcontainer_cleanup_steps.py: updated A2aRequest(operation=...)
   - plan_prompt_command_steps.py: updated A2aRequest(operation=...)
   - wf03_plan_prompt_confidence_steps.py: updated A2aRequest(operation=...)
   - consolidated_misc.feature: updated old A2aRequest/A2aResponse scenarios

3. Unit tests - Session CLI output changed (commit 0d5d9cf0 and others):
   - 'Session Created' → 'Session created' (lowercase)
   - 'Session Details' → 'Session Summary'
   - 'Sessions (N total)' → 'Sessions'
   - session list JSON: top-level 'total' → nested 'summary.total'
   - Fixed in: session_cli.feature, session_cli_coverage_boost.feature,
     session_cli_uncovered_branches.feature, session_list_error.feature,
     tdd_session_create_persist_steps.py

4. Unit tests - Plan list output changed (commit 1a07a891):
   - 'V3 Lifecycle Plans' → 'Plans'
   - 'Lifecycle Plans' → 'Plans'
   - Name column removed (restored in source)
   - Invariants column removed (restored in source)
   - Project truncation removed (restored in source)
   - Fixed in: plan_cli_cancel_revert_coverage.feature,
     plan_lifecycle_cli_coverage.feature, plan_cli_coverage_boost_steps.py,
     plan.py (source code restored)

5. Unit tests - Plan apply command now requires ULID (commit 300a5d6d):
   - plan_cli_coverage_r3.feature: updated 'PLAN-001' to valid ULID
   - plan_cli_coverage_r3_steps.py: added --yes flag, added new step for
     no-eligible-plans path

6. Unit tests - Various source code bugs:
   - ThoughtBlock: converted from @dataclass to Pydantic BaseModel
     (architecture test requires all dataclasses to use Pydantic)
   - session.py: added DatabaseError handling to export, import, tell commands
   - database.py: fixed rollback_to() to reuse checkpoint connection for writes
   - database.py: added _get_checkpoint_conn() helper
   - check-tls-cert.py: fixed SSLCertVerificationError.reason AttributeError

7. Unit tests - Test step bugs:
   - error_recovery_coverage_boost_steps.py: fixed invalid ULID _PLAN_ID
   - session_service_coverage_steps.py: fixed 'sha256:' prefix bug in checksum
   - database_models_new_coverage_steps.py: added 'name' field to session mock
   - async_audit_recording_steps.py: fixed Settings(audit_async=False) via env var
   - coverage_threshold_config_steps.py: added --coverage-min pattern support
   - m5_acms_smoke_steps.py: updated usage hint text
   - actor_cli_yaml_steps.py: updated 'Removed actor' → 'Actor removed'
   - aimodelscredentials_steps.py: set context.imported_class in import step
   - domain_base_model.feature: added missing 'When I examine model_config' step
   - tui_first_run_steps.py: fixed module reload to restore cleveragents.tui.*
     modules after test (prevented patch interference in subsequent tests)
   - tui_first_run_steps.py: added set_search('') step for empty string
   - resource_handler_base_coverage_r3_steps.py: use _MinimalHandler instead
     of DatabaseResourceHandler for NotImplementedError tests
   - resource_handler_crud.feature: updated to test new DatabaseHandler behavior
   - resource_handler_sandbox.feature: updated to test new DatabaseHandler behavior
   - tdd_json_decode_crash_persistence.feature: fixed @tdd_bug → @tdd_issue tags

8. Parallel test interference:
   - All step files using use_step_matcher('re') now reset to 'parse' at end
     to prevent global matcher state leaking to subsequent step files
2026-04-04 20:38:16 +00:00

452 lines
15 KiB
Python

"""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<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)
# Reset step matcher to parse (default) so subsequent step files are not affected
use_step_matcher("parse")