Files
temp/features/steps/plan_cli_coverage_r3_steps.py
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

544 lines
22 KiB
Python

"""Step definitions for plan_cli_coverage_r3.feature.
Targets remaining uncovered lines in cleveragents/cli/commands/plan.py:
- Lines 104, 107-108: validate_namespaced_actor invalid / valid
- Line 184: _plan_spec_dict with execution_environment
- Line 790: build command PlanError handler
- Lines 799-858: apply command (v3 and legacy paths)
- Lines 1205-1224: continue_plan command body
- Lines 1529-1542, 1631-1633, 1751-1752: use_action with actor overrides
- Lines 1789-1792, 1794: use_action with execution_env_priority
- Lines 2740-2741, 2743: plan_diff command body
- Lines 3047-3048, 3051: correct_decision error handlers
"""
from __future__ import annotations
import os
from datetime import datetime
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from cleveragents.cli.commands.plan import (
_plan_spec_dict,
validate_namespaced_actor,
)
from cleveragents.cli.commands.plan import (
app as plan_app,
)
from cleveragents.core.exceptions import (
CleverAgentsError,
PlanError,
ValidationError,
)
from cleveragents.domain.models.core.plan import (
ExecutionEnvPriority,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
)
# ──────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────
_ULID_A = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
_PATCH_CONTAINER = "cleveragents.application.container.get_container"
_PATCH_GET_LIFECYCLE = "cleveragents.cli.commands.plan._get_lifecycle_service"
_PATCH_GET_APPLY_SVC = "cleveragents.cli.commands.plan._get_apply_service"
_PATCH_GET_PROJECT = "cleveragents.cli.commands.plan._get_current_project"
_PATCH_NOTIFY_FACADE = "cleveragents.cli.commands.plan._notify_facade"
# ──────────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────────
def _make_plan(
*,
plan_id: str = _ULID_A,
name: str = "local/test-plan",
phase: PlanPhase = PlanPhase.EXECUTE,
processing_state: ProcessingState = ProcessingState.COMPLETE,
read_only: bool = False,
execution_environment: str | None = None,
execution_env_priority: ExecutionEnvPriority | None = None,
) -> Plan:
"""Build a real Plan object for testing."""
plan = Plan(
identity=PlanIdentity(plan_id=plan_id),
namespaced_name=NamespacedName.parse(name),
action_name="local/test-action",
description="Coverage test plan",
phase=phase,
processing_state=processing_state,
project_links=[],
timestamps=PlanTimestamps(
created_at=datetime(2025, 6, 15, 10, 0, 0),
updated_at=datetime(2025, 6, 15, 11, 0, 0),
),
reusable=True,
read_only=read_only,
)
if execution_environment is not None:
plan.execution_environment = execution_environment
if execution_env_priority is not None:
plan.execution_env_priority = execution_env_priority
return plan
def _start_patch(context: Context, target: str, **kwargs) -> MagicMock:
"""Start a mock patch and register cleanup on context."""
patcher = patch(target, **kwargs)
mock_obj = patcher.start()
context.add_cleanup(patcher.stop)
return mock_obj
# ──────────────────────────────────────────────────────────────
# Given — CLI runner
# ──────────────────────────────────────────────────────────────
@given("a plcov3 CLI runner")
def step_plcov3_cli_runner(context: Context) -> None:
context.plcov3_runner = CliRunner()
context.plcov3_result = None
context.plcov3_error = None
context.plcov3_return_value = None
# ══════════════════════════════════════════════════════════════
# validate_namespaced_actor (lines 104, 107-108)
# ══════════════════════════════════════════════════════════════
@when('I plcov3 call validate_namespaced_actor with value "{value}" and flag "{flag}"')
def step_plcov3_call_validate_actor(context: Context, value: str, flag: str) -> None:
try:
context.plcov3_return_value = validate_namespaced_actor(value, flag)
except Exception as exc:
context.plcov3_error = exc
@then(
'the plcov3 error should be a ValidationError with message containing "{fragment}"'
)
def step_plcov3_error_is_validation(context: Context, fragment: str) -> None:
assert context.plcov3_error is not None, "Expected an error but none was raised"
assert isinstance(context.plcov3_error, ValidationError), (
f"Expected ValidationError, got {type(context.plcov3_error).__name__}"
)
assert fragment in str(context.plcov3_error), (
f"Expected '{fragment}' in error message, got: {context.plcov3_error}"
)
@then('the plcov3 result should equal "{expected}"')
def step_plcov3_result_equals(context: Context, expected: str) -> None:
assert context.plcov3_error is None, f"Unexpected error: {context.plcov3_error}"
assert context.plcov3_return_value == expected, (
f"Expected '{expected}', got '{context.plcov3_return_value}'"
)
# ══════════════════════════════════════════════════════════════
# _plan_spec_dict with execution_environment (line 184)
# ══════════════════════════════════════════════════════════════
@given('a plcov3 v3 plan with execution_environment "{env}"')
def step_plcov3_plan_with_exec_env(context: Context, env: str) -> None:
context.plcov3_plan = _make_plan(
execution_environment=env,
execution_env_priority=ExecutionEnvPriority.FALLBACK,
)
@when("I plcov3 call _plan_spec_dict")
def step_plcov3_call_plan_spec_dict(context: Context) -> None:
context.plcov3_spec_dict = _plan_spec_dict(context.plcov3_plan)
@then('the plcov3 spec dict should contain key "{key}" with value "{value}"')
def step_plcov3_spec_dict_contains(context: Context, key: str, value: str) -> None:
assert key in context.plcov3_spec_dict, (
f"Key '{key}' not in spec dict: {list(context.plcov3_spec_dict.keys())}"
)
assert str(context.plcov3_spec_dict[key]) == value, (
f"Expected '{value}', got '{context.plcov3_spec_dict[key]}'"
)
# ══════════════════════════════════════════════════════════════
# build command PlanError handler (line 790)
# ══════════════════════════════════════════════════════════════
@given("a plcov3 mocked build environment that raises PlanError")
def step_plcov3_mock_build_plan_error(context: Context) -> None:
container = MagicMock()
plan_service = MagicMock()
plan_service.build_plan.side_effect = PlanError("Build failed internally")
container.plan_service.return_value = plan_service
container.actor_registry.return_value = MagicMock()
project = MagicMock()
project.name = "test-project"
_start_patch(context, _PATCH_CONTAINER, return_value=container)
_start_patch(context, _PATCH_GET_PROJECT, return_value=project)
@when("I plcov3 invoke the build command")
def step_plcov3_invoke_build(context: Context) -> None:
context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["build"])
# ══════════════════════════════════════════════════════════════
# apply command — v3 lifecycle path (lines 799-824)
# ══════════════════════════════════════════════════════════════
@given("a plcov3 mocked lifecycle service for apply happy path")
def step_plcov3_mock_apply_v3(context: Context) -> None:
service = MagicMock()
# Walk through the full apply flow
pre_plan = _make_plan(
phase=PlanPhase.EXECUTE,
processing_state=ProcessingState.COMPLETE,
)
apply_queued = _make_plan(
phase=PlanPhase.APPLY,
processing_state=ProcessingState.QUEUED,
)
apply_processing = _make_plan(
phase=PlanPhase.APPLY,
processing_state=ProcessingState.PROCESSING,
)
apply_applied = _make_plan(
phase=PlanPhase.APPLY,
processing_state=ProcessingState.APPLIED,
)
service.get_plan.side_effect = [
pre_plan,
apply_queued,
apply_processing,
apply_applied,
]
service.apply_plan.return_value = None
service.start_apply.return_value = None
service.complete_apply.return_value = None
_start_patch(context, _PATCH_GET_LIFECYCLE, return_value=service)
_start_patch(context, _PATCH_NOTIFY_FACADE)
@when('I plcov3 invoke apply with plan_id "{plan_id}"')
def step_plcov3_invoke_apply_with_id(context: Context, plan_id: str) -> None:
context.plcov3_result = context.plcov3_runner.invoke(
plan_app, ["apply", plan_id, "--yes"]
)
# ══════════════════════════════════════════════════════════════
# apply command — legacy path with changes (lines 826-858)
# ══════════════════════════════════════════════════════════════
def _make_mock_change(file_path: str = "src/main.py", operation: str = "modify"):
"""Create a mock Change object."""
change = MagicMock()
change.file_path = file_path
change.operation = operation
return change
@given("a plcov3 mocked lifecycle service with no eligible apply plans")
def step_plcov3_mock_lifecycle_no_eligible_apply(context: Context) -> None:
"""Mock lifecycle service that returns no plans eligible for apply."""
service = MagicMock()
service.list_plans.return_value = []
_start_patch(context, _PATCH_GET_LIFECYCLE, return_value=service)
@given("a plcov3 mocked legacy apply environment with pending changes")
def step_plcov3_mock_legacy_apply_with_changes(context: Context) -> None:
container = MagicMock()
plan_service = MagicMock()
changes = [_make_mock_change("src/main.py", "modify")]
plan_service.get_pending_changes.return_value = changes
plan_service.apply_changes.return_value = 1
container.plan_service.return_value = plan_service
project = MagicMock()
project.name = "test-project"
_start_patch(context, _PATCH_CONTAINER, return_value=container)
_start_patch(context, _PATCH_GET_PROJECT, return_value=project)
# Set testing env var so confirmation is skipped
old_val = os.environ.get("CLEVERAGENTS_TESTING_USE_MOCK_AI", "")
os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = "true"
context.add_cleanup(
lambda: (
os.environ.__setitem__("CLEVERAGENTS_TESTING_USE_MOCK_AI", old_val)
if old_val
else os.environ.pop("CLEVERAGENTS_TESTING_USE_MOCK_AI", None)
)
)
@given("a plcov3 mocked legacy apply environment with no pending changes")
def step_plcov3_mock_legacy_apply_no_changes(context: Context) -> None:
container = MagicMock()
plan_service = MagicMock()
plan_service.get_pending_changes.return_value = []
container.plan_service.return_value = plan_service
project = MagicMock()
project.name = "test-project"
_start_patch(context, _PATCH_CONTAINER, return_value=container)
_start_patch(context, _PATCH_GET_PROJECT, return_value=project)
@when("I plcov3 invoke apply without plan_id")
def step_plcov3_invoke_apply_no_id(context: Context) -> None:
context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["apply"])
# ══════════════════════════════════════════════════════════════
# continue_plan command (lines 1205-1224)
# ══════════════════════════════════════════════════════════════
@given("a plcov3 mocked continue environment")
def step_plcov3_mock_continue(context: Context) -> None:
container = MagicMock()
plan_service = MagicMock()
plan_service.continue_plan.return_value = None
container.plan_service.return_value = plan_service
project = MagicMock()
project.name = "test-project"
_start_patch(context, _PATCH_CONTAINER, return_value=container)
_start_patch(context, _PATCH_GET_PROJECT, return_value=project)
@given("a plcov3 mocked continue environment with current plan")
def step_plcov3_mock_continue_with_plan(context: Context) -> None:
container = MagicMock()
plan_service = MagicMock()
current_plan = MagicMock()
current_plan.name = "my-existing-plan"
plan_service.get_current_plan.return_value = current_plan
container.plan_service.return_value = plan_service
project = MagicMock()
project.name = "test-project"
_start_patch(context, _PATCH_CONTAINER, return_value=container)
_start_patch(context, _PATCH_GET_PROJECT, return_value=project)
@when('I plcov3 invoke continue with prompt "{prompt}"')
def step_plcov3_invoke_continue_with_prompt(context: Context, prompt: str) -> None:
context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["continue", prompt])
@when("I plcov3 invoke continue without prompt")
def step_plcov3_invoke_continue_no_prompt(context: Context) -> None:
context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["continue"])
# ══════════════════════════════════════════════════════════════
# use_action with overrides (lines 1529-1542, 1631-1633, 1751-1752,
# 1789-1792, 1794)
# ══════════════════════════════════════════════════════════════
def _make_mock_action():
"""Create a mock Action with a namespaced_name."""
action = MagicMock()
action.namespaced_name = NamespacedName.parse("local/test")
return action
@given("a plcov3 mocked use_action environment")
def step_plcov3_mock_use_action(context: Context) -> None:
service = MagicMock()
action = _make_mock_action()
service.get_action_by_name.return_value = action
plan = _make_plan(
phase=PlanPhase.STRATEGIZE,
processing_state=ProcessingState.QUEUED,
)
service.use_action.return_value = plan
service.save_plan.return_value = None
service._commit_plan.return_value = None
_start_patch(context, _PATCH_GET_LIFECYCLE, return_value=service)
_start_patch(context, _PATCH_NOTIFY_FACADE)
@when('I plcov3 invoke use with action "{action}" and strategy-actor "{actor}"')
def step_plcov3_invoke_use_with_strategy_actor(
context: Context, action: str, actor: str
) -> None:
context.plcov3_result = context.plcov3_runner.invoke(
plan_app,
["use", action, "--strategy-actor", actor],
)
@when(
'I plcov3 invoke use with action "{action}" and execution-environment "{env}" and priority "{priority}"'
)
def step_plcov3_invoke_use_with_env_priority(
context: Context, action: str, env: str, priority: str
) -> None:
context.plcov3_result = context.plcov3_runner.invoke(
plan_app,
[
"use",
action,
"--execution-environment",
env,
"--execution-env-priority",
priority,
],
)
# ══════════════════════════════════════════════════════════════
# plan diff command (lines 2740-2741, 2743)
# ══════════════════════════════════════════════════════════════
@given("a plcov3 mocked apply service for diff")
def step_plcov3_mock_apply_svc_diff(context: Context) -> None:
apply_svc = MagicMock()
apply_svc.diff.return_value = "--- a/file.py\n+++ b/file.py\n@@ unified diff @@"
_start_patch(context, _PATCH_GET_APPLY_SVC, return_value=apply_svc)
@when('I plcov3 invoke diff with plan_id "{plan_id}"')
def step_plcov3_invoke_diff(context: Context, plan_id: str) -> None:
context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["diff", plan_id])
# ══════════════════════════════════════════════════════════════
# correct_decision error handlers (lines 3047-3048, 3051)
# ══════════════════════════════════════════════════════════════
@given("a plcov3 mocked correction environment that raises ValidationError")
def step_plcov3_mock_correct_validation_error(context: Context) -> None:
container = MagicMock()
# Make the correction service raise ValidationError during request_correction
correction_svc = MagicMock()
correction_svc.request_correction.side_effect = ValidationError(
"Bad correction input"
)
container.correction_service.return_value = correction_svc
# Decision service to get past identifier resolution
decision_svc = MagicMock()
decision_svc.list_decisions.return_value = []
decision_svc.get_influence_edges.return_value = []
container.decision_service.return_value = decision_svc
# Lifecycle service to resolve identifier as non-plan
from cleveragents.core.exceptions import ResourceNotFoundError
lifecycle_svc = MagicMock()
lifecycle_svc.get_plan.side_effect = ResourceNotFoundError(
"Not found", resource_type="plan", resource_id="DEC-001"
)
container.plan_lifecycle_service.return_value = lifecycle_svc
_start_patch(context, _PATCH_CONTAINER, return_value=container)
# Mock _resolve_active_plan_id to return a plan ID
_start_patch(
context,
"cleveragents.cli.commands.plan._resolve_active_plan_id",
return_value="PLAN-ACTIVE",
)
@given("a plcov3 mocked correction environment that raises CleverAgentsError")
def step_plcov3_mock_correct_clever_error(context: Context) -> None:
container = MagicMock()
correction_svc = MagicMock()
correction_svc.request_correction.side_effect = CleverAgentsError(
"Internal correction error"
)
container.correction_service.return_value = correction_svc
decision_svc = MagicMock()
decision_svc.list_decisions.return_value = []
decision_svc.get_influence_edges.return_value = []
container.decision_service.return_value = decision_svc
from cleveragents.core.exceptions import ResourceNotFoundError
lifecycle_svc = MagicMock()
lifecycle_svc.get_plan.side_effect = ResourceNotFoundError(
"Not found", resource_type="plan", resource_id="DEC-001"
)
container.plan_lifecycle_service.return_value = lifecycle_svc
_start_patch(context, _PATCH_CONTAINER, return_value=container)
_start_patch(
context,
"cleveragents.cli.commands.plan._resolve_active_plan_id",
return_value="PLAN-ACTIVE",
)
@when(
'I plcov3 invoke correct with identifier "{ident}" mode "{mode}" guidance "{guidance}"'
)
def step_plcov3_invoke_correct(
context: Context, ident: str, mode: str, guidance: str
) -> None:
context.plcov3_result = context.plcov3_runner.invoke(
plan_app,
["correct", ident, "--mode", mode, "--guidance", guidance, "--yes"],
)
# ══════════════════════════════════════════════════════════════
# Shared Then steps
# ══════════════════════════════════════════════════════════════
@then('the plcov3 CLI output should contain "{fragment}"')
def step_plcov3_output_contains(context: Context, fragment: str) -> None:
result = context.plcov3_result
assert result is not None, "No CLI result captured"
combined = (result.output or "") + (getattr(result, "stderr", "") or "")
assert fragment in combined, f"Expected '{fragment}' in output, got:\n{combined}"
@then("the plcov3 CLI exit code should be {code:d}")
def step_plcov3_exit_code(context: Context, code: int) -> None:
result = context.plcov3_result
assert result is not None, "No CLI result captured"
assert result.exit_code == code, (
f"Expected exit code {code}, got {result.exit_code}.\n"
f"Output: {result.output}\n"
f"Exception: {result.exception}"
)