forked from HAL9000/cleveragents-core
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
275 lines
9.1 KiB
Python
275 lines
9.1 KiB
Python
"""Step definitions for A2A facade wiring Behave scenarios.
|
|
|
|
All mocks in this file are lightweight test doubles that simulate the
|
|
service contracts used by :class:`A2aLocalFacade`. They live here
|
|
(inside the test tree) per the project's mock-placement policy.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, use_step_matcher, when
|
|
from behave.runner import Context
|
|
|
|
try:
|
|
from cleveragents.a2a.events import A2aEventQueue
|
|
from cleveragents.a2a.facade import A2aLocalFacade
|
|
from cleveragents.a2a.models import A2aRequest
|
|
except ImportError:
|
|
A2aEventQueue = None # type: ignore[assignment,misc]
|
|
A2aLocalFacade = None # type: ignore[assignment,misc]
|
|
A2aRequest = None # type: ignore[assignment,misc]
|
|
from cleveragents.core.exceptions import (
|
|
BusinessRuleViolation,
|
|
PlanError,
|
|
ResourceNotFoundError,
|
|
ValidationError,
|
|
)
|
|
|
|
use_step_matcher("re")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mock helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _MockSession:
|
|
"""Minimal session stub."""
|
|
|
|
def __init__(self, session_id: str = "MOCK-SESSION-001") -> None:
|
|
self.session_id = session_id
|
|
|
|
|
|
class _MockPlanIdentity:
|
|
"""Minimal PlanIdentity stub."""
|
|
|
|
def __init__(self, plan_id: str = "MOCK-PLAN-001") -> None:
|
|
self.plan_id = plan_id
|
|
|
|
|
|
class _MockPlan:
|
|
"""Minimal plan stub with phase and state."""
|
|
|
|
def __init__(self, plan_id: str = "MOCK-PLAN-001") -> None:
|
|
self.identity = _MockPlanIdentity(plan_id)
|
|
self.phase = MagicMock()
|
|
self.phase.value = "strategize"
|
|
self.state = MagicMock()
|
|
self.state.value = "queued"
|
|
|
|
|
|
class _MockToolSpec:
|
|
"""Minimal ToolSpec stub."""
|
|
|
|
def __init__(self, name: str, description: str) -> None:
|
|
self.name = name
|
|
self.description = description
|
|
|
|
|
|
class _MockResource:
|
|
"""Minimal resource stub."""
|
|
|
|
def __init__(self, resource_id: str, name: str, resource_type_name: str) -> None:
|
|
self.resource_id = resource_id
|
|
self.name = name
|
|
self.resource_type_name = resource_type_name
|
|
|
|
|
|
def _build_mock_session_service() -> MagicMock:
|
|
svc = MagicMock()
|
|
svc.create.return_value = _MockSession()
|
|
svc.delete.return_value = None
|
|
return svc
|
|
|
|
|
|
def _build_mock_plan_lifecycle_service() -> MagicMock:
|
|
svc = MagicMock()
|
|
svc.use_action.return_value = _MockPlan()
|
|
svc.execute_plan.return_value = _MockPlan()
|
|
svc.get_plan.return_value = _MockPlan()
|
|
svc.apply_plan.return_value = _MockPlan()
|
|
return svc
|
|
|
|
|
|
def _build_mock_tool_registry() -> MagicMock:
|
|
registry = MagicMock()
|
|
registry.list_tools.return_value = [
|
|
_MockToolSpec("local/tool-a", "Tool A"),
|
|
_MockToolSpec("local/tool-b", "Tool B"),
|
|
]
|
|
return registry
|
|
|
|
|
|
def _build_mock_resource_registry_service() -> MagicMock:
|
|
svc = MagicMock()
|
|
svc.list_resources.return_value = [
|
|
_MockResource("RES-001", "my-repo", resource_type_name="git-checkout"),
|
|
]
|
|
return svc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with a mock SessionService")
|
|
def step_wired_facade_session(context: Context) -> None:
|
|
context.wired_facade = A2aLocalFacade(
|
|
services={"session_service": _build_mock_session_service()}
|
|
)
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with a mock PlanLifecycleService")
|
|
def step_wired_facade_plan(context: Context) -> None:
|
|
context.wired_facade = A2aLocalFacade(
|
|
services={"plan_lifecycle_service": _build_mock_plan_lifecycle_service()}
|
|
)
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with a mock ToolRegistry")
|
|
def step_wired_facade_tool_registry(context: Context) -> None:
|
|
context.wired_facade = A2aLocalFacade(
|
|
services={"tool_registry": _build_mock_tool_registry()}
|
|
)
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with a mock ResourceRegistryService")
|
|
def step_wired_facade_resource_registry(context: Context) -> None:
|
|
context.wired_facade = A2aLocalFacade(
|
|
services={
|
|
"resource_registry_service": (_build_mock_resource_registry_service())
|
|
}
|
|
)
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with a mock A2aEventQueue")
|
|
def step_wired_facade_event_queue(context: Context) -> None:
|
|
context.wired_facade = A2aLocalFacade(services={"event_queue": A2aEventQueue()})
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with no services")
|
|
def step_wired_facade_no_services(context: Context) -> None:
|
|
context.wired_facade = A2aLocalFacade()
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with a raising SessionService for not-found")
|
|
def step_wired_facade_not_found(context: Context) -> None:
|
|
svc = MagicMock()
|
|
svc.delete.side_effect = ResourceNotFoundError(
|
|
resource_type="session", resource_id="nonexistent"
|
|
)
|
|
context.wired_facade = A2aLocalFacade(services={"session_service": svc})
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with a raising service for validation-error")
|
|
def step_wired_facade_validation_error(context: Context) -> None:
|
|
svc = MagicMock()
|
|
svc.use_action.side_effect = ValidationError("Invalid action args")
|
|
context.wired_facade = A2aLocalFacade(services={"plan_lifecycle_service": svc})
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with a raising service for plan-error")
|
|
def step_wired_facade_plan_error(context: Context) -> None:
|
|
svc = MagicMock()
|
|
svc.execute_plan.side_effect = PlanError("Plan execution failed")
|
|
context.wired_facade = A2aLocalFacade(services={"plan_lifecycle_service": svc})
|
|
|
|
|
|
@given(r"a wired A2aLocalFacade with a raising service for invalid-state")
|
|
def step_wired_facade_invalid_state(context: Context) -> None:
|
|
svc = MagicMock()
|
|
svc.apply_plan.side_effect = BusinessRuleViolation("Cannot apply in current state")
|
|
context.wired_facade = A2aLocalFacade(services={"plan_lifecycle_service": svc})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when(
|
|
r'I dispatch wired operation "(?P<operation>[^"]+)" '
|
|
r"with params (?P<params_json>.+)"
|
|
)
|
|
def step_dispatch_wired(context: Context, operation: str, params_json: str) -> None:
|
|
params: dict[str, Any] = json.loads(params_json)
|
|
request = A2aRequest(method=operation, params=params)
|
|
context.wired_response = context.wired_facade.dispatch(request)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then(r'the wired response status should be "(?P<status>[^"]+)"')
|
|
def step_wired_status(context: Context, status: str) -> None:
|
|
if status == "ok":
|
|
assert context.wired_response.result is not None, (
|
|
f"Expected ok response but got error: {context.wired_response.error}"
|
|
)
|
|
else:
|
|
assert context.wired_response.error is not None, (
|
|
f"Expected error response but got result: {context.wired_response.result}"
|
|
)
|
|
|
|
|
|
@then(r'wired response data key "(?P<key>[^"]+)" equals "(?P<value>[^"]+)"')
|
|
def step_wired_data_key_value(context: Context, key: str, value: str) -> None:
|
|
data = context.wired_response.result or {}
|
|
actual = data.get(key)
|
|
assert str(actual) == value, f"Expected '{value}', got '{actual}'"
|
|
|
|
|
|
@then(
|
|
r"wired response data should contain tools list "
|
|
r"with (?P<count>\d+) items"
|
|
)
|
|
def step_wired_tools_count(context: Context, count: str) -> None:
|
|
data = context.wired_response.result or {}
|
|
tools = data.get("tools", [])
|
|
assert len(tools) == int(count), f"Expected {count} tools, got {len(tools)}"
|
|
|
|
|
|
@then(
|
|
r"wired response data should contain resources list "
|
|
r"with (?P<count>\d+) items"
|
|
)
|
|
def step_wired_resources_count(context: Context, count: str) -> None:
|
|
data = context.wired_response.result or {}
|
|
resources = data.get("resources", [])
|
|
assert len(resources) == int(count), (
|
|
f"Expected {count} resources, got {len(resources)}"
|
|
)
|
|
|
|
|
|
@then(r'wired response data key "(?P<key>[^"]+)" should not be empty')
|
|
def step_wired_data_key_not_empty(context: Context, key: str) -> None:
|
|
data = context.wired_response.result or {}
|
|
actual = data.get(key)
|
|
assert actual, f"Key '{key}' is empty or missing"
|
|
|
|
|
|
@then(r'wired response error code should be "(?P<code>[^"]+)"')
|
|
def step_wired_error_code(context: Context, code: str) -> None:
|
|
assert context.wired_response.error is not None, "No error in response"
|
|
assert context.wired_response.error.code == code, (
|
|
f"Expected error code '{code}', got '{context.wired_response.error.code}'"
|
|
)
|
|
|
|
|
|
@then(r"the mock SessionService create should not have been called")
|
|
def step_mock_session_create_not_called(context: Context) -> None:
|
|
"""Verify the session service create was NOT invoked (idempotent path)."""
|
|
svc = context.wired_facade._services["session_service"]
|
|
svc.create.assert_not_called()
|
|
|
|
|
|
# Reset step matcher to parse (default) so subsequent step files are not affected
|
|
use_step_matcher("parse")
|