02250473ad
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 (commit9c6d6915) 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 (commit0d5d9cf0and 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 (commit1a07a891): - '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 (commit300a5d6d): - 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
452 lines
15 KiB
Python
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")
|