583e6b7ea2
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 17s
CI / security (pull_request) Successful in 36s
CI / typecheck (pull_request) Successful in 54s
CI / unit_tests (pull_request) Successful in 2m39s
CI / integration_tests (pull_request) Successful in 3m10s
CI / docker (pull_request) Successful in 40s
CI / coverage (pull_request) Successful in 5m0s
CI / lint (push) Successful in 14s
CI / build (push) Successful in 15s
CI / quality (push) Successful in 16s
CI / security (push) Successful in 34s
CI / typecheck (push) Successful in 35s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m23s
CI / docker (push) Successful in 38s
CI / integration_tests (push) Successful in 3m8s
CI / coverage (push) Successful in 5m10s
CI / benchmark-publish (push) Successful in 16m26s
CI / benchmark-regression (pull_request) Successful in 30m12s
Implement spec-mandated Layer 4 Predictive Error Prevention system: - ErrorPattern domain model with pattern text, historical failures, preventive checks, frequency tracking, and keyword-based matching. - ErrorPatternRepository with in-memory CRUD + context-matching query. - ErrorPatternService with record_failure(), match_patterns(), and get_statistics() methods. - Wire into plan execution via plan_lifecycle_service pre-execution hook. - Add error pattern statistics to CLI diagnostics output. Behave BDD: 11 scenarios covering recording, matching, formatting, stats. Robot Framework: 3 integration smoke tests. ASV benchmarks: pattern matching performance. ISSUES CLOSED: #571
237 lines
8.4 KiB
Python
237 lines
8.4 KiB
Python
"""Step definitions for system_diagnostics_coverage.feature.
|
|
|
|
Covers uncovered paths in cli/commands/system.py:
|
|
- _check_file_permissions: writable-but-not-readable (line 344)
|
|
- _check_stale_locks: locks table exists paths (lines 367-388)
|
|
- _check_async_worker_health: async enabled path (lines 405-417)
|
|
- _check_error_patterns: exception path (lines 438-442)
|
|
|
|
All step patterns use the 'sysdiag_' prefix to avoid AmbiguousStep collisions
|
|
with existing steps in cli_core_steps.py and coverage_security_template_boost_steps.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
__all__: list[str] = []
|
|
|
|
|
|
# ===========================================================================
|
|
# Helpers
|
|
# ===========================================================================
|
|
|
|
|
|
def _sysdiag_mock_settings(**overrides: Any) -> Any:
|
|
"""Create mock settings with sensible defaults for diagnostic tests."""
|
|
tmpdir = tempfile.mkdtemp()
|
|
s = MagicMock()
|
|
s.database_url = overrides.get("database_url", f"sqlite:///{tmpdir}/test.db")
|
|
s.data_dir = overrides.get("data_dir", Path(tmpdir))
|
|
s.storage_path = overrides.get("storage_path", Path(tmpdir))
|
|
s.log_dir = overrides.get("log_dir", Path(tmpdir) / "logs")
|
|
s.default_automation_profile = "auto"
|
|
s.debug_enabled = False
|
|
s.async_enabled = overrides.get("async_enabled", False)
|
|
s.async_max_workers = overrides.get("async_max_workers", 4)
|
|
s.async_poll_interval = overrides.get("async_poll_interval", 1.0)
|
|
s.has_provider_configured = MagicMock(return_value=False)
|
|
s.configured_providers = []
|
|
return s
|
|
|
|
|
|
# ===========================================================================
|
|
# Given steps
|
|
# ===========================================================================
|
|
|
|
|
|
@given("a sysdiag test environment")
|
|
def step_sysdiag_env(context: Context) -> None:
|
|
context.sysdiag_result = {}
|
|
context.sysdiag_tmpdir = tempfile.mkdtemp()
|
|
|
|
|
|
@given("a data dir that exists")
|
|
def step_sysdiag_data_dir_exists(context: Context) -> None:
|
|
data_dir = Path(context.sysdiag_tmpdir) / "data"
|
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
context.sysdiag_data_dir = data_dir
|
|
|
|
|
|
# ===========================================================================
|
|
# When steps — _check_file_permissions edge cases
|
|
# ===========================================================================
|
|
|
|
|
|
@when("I run the file permissions check with read access denied")
|
|
def step_sysdiag_perms_no_read(context: Context) -> None:
|
|
from cleveragents.cli.commands.system import _check_file_permissions
|
|
|
|
ms = _sysdiag_mock_settings(data_dir=context.sysdiag_data_dir)
|
|
|
|
def fake_access(path: Any, mode: int) -> bool:
|
|
"""Simulate writable-but-not-readable directory."""
|
|
import os as _os
|
|
|
|
if mode == _os.R_OK:
|
|
return False
|
|
return mode == _os.W_OK
|
|
|
|
with (
|
|
patch("cleveragents.config.settings.get_settings", return_value=ms),
|
|
patch("cleveragents.cli.commands.system.os.access", side_effect=fake_access),
|
|
):
|
|
context.sysdiag_result = _check_file_permissions()
|
|
|
|
|
|
@when("I run the file permissions check with no access at all")
|
|
def step_sysdiag_perms_no_access(context: Context) -> None:
|
|
from cleveragents.cli.commands.system import _check_file_permissions
|
|
|
|
ms = _sysdiag_mock_settings(data_dir=context.sysdiag_data_dir)
|
|
|
|
with (
|
|
patch("cleveragents.config.settings.get_settings", return_value=ms),
|
|
patch("cleveragents.cli.commands.system.os.access", return_value=False),
|
|
):
|
|
context.sysdiag_result = _check_file_permissions()
|
|
|
|
|
|
# ===========================================================================
|
|
# When steps — _check_stale_locks edge cases
|
|
# ===========================================================================
|
|
|
|
|
|
@when("I run the stale locks check with {count:d} stale locks in the table")
|
|
def step_sysdiag_stale_locks_count(context: Context, count: int) -> None:
|
|
from cleveragents.cli.commands.system import _check_stale_locks
|
|
|
|
mock_engine = MagicMock()
|
|
mock_inspector = MagicMock()
|
|
mock_inspector.get_table_names.return_value = ["locks", "other_table"]
|
|
|
|
mock_lock_service = MagicMock()
|
|
mock_lock_service.count_stale_locks.return_value = count
|
|
|
|
with (
|
|
patch(
|
|
"cleveragents.cli.commands.system.get_database_url",
|
|
return_value="sqlite:///test.db",
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.system.create_engine",
|
|
return_value=mock_engine,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.system.sa_inspect",
|
|
return_value=mock_inspector,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.system.LockService",
|
|
return_value=mock_lock_service,
|
|
),
|
|
):
|
|
context.sysdiag_result = _check_stale_locks()
|
|
|
|
|
|
@when("I run the stale locks check with a database error")
|
|
def step_sysdiag_stale_locks_error(context: Context) -> None:
|
|
from cleveragents.cli.commands.system import _check_stale_locks
|
|
|
|
with patch(
|
|
"cleveragents.cli.commands.system.get_database_url",
|
|
side_effect=RuntimeError("database unreachable"),
|
|
):
|
|
context.sysdiag_result = _check_stale_locks()
|
|
|
|
|
|
# ===========================================================================
|
|
# When steps — _check_async_worker_health edge cases
|
|
# ===========================================================================
|
|
|
|
|
|
@when("I run the async worker health check with async enabled")
|
|
def step_sysdiag_async_enabled(context: Context) -> None:
|
|
from cleveragents.cli.commands.system import _check_async_worker_health
|
|
|
|
ms = _sysdiag_mock_settings(
|
|
async_enabled=True,
|
|
async_max_workers=8,
|
|
async_poll_interval=2.5,
|
|
)
|
|
with patch("cleveragents.config.settings.get_settings", return_value=ms):
|
|
context.sysdiag_result = _check_async_worker_health()
|
|
|
|
|
|
@when("I run the async worker health check with a settings error")
|
|
def step_sysdiag_async_error(context: Context) -> None:
|
|
from cleveragents.cli.commands.system import _check_async_worker_health
|
|
|
|
with patch(
|
|
"cleveragents.config.settings.get_settings",
|
|
side_effect=RuntimeError("settings unavailable"),
|
|
):
|
|
context.sysdiag_result = _check_async_worker_health()
|
|
|
|
|
|
# ===========================================================================
|
|
# When steps — _check_error_patterns exception path
|
|
# ===========================================================================
|
|
|
|
|
|
@when("I run the error patterns check with a service error")
|
|
def step_sysdiag_error_patterns_error(context: Context) -> None:
|
|
from cleveragents.cli.commands.system import _check_error_patterns
|
|
|
|
with patch(
|
|
"cleveragents.application.services.error_pattern_service.ErrorPatternService",
|
|
side_effect=RuntimeError("service init failed"),
|
|
):
|
|
context.sysdiag_result = _check_error_patterns()
|
|
|
|
|
|
# ===========================================================================
|
|
# Then steps
|
|
# ===========================================================================
|
|
|
|
|
|
@then('the sysdiag check status should be "{expected}"')
|
|
def step_sysdiag_status(context: Context, expected: str) -> None:
|
|
from cleveragents.cli.commands.system import CheckStatus
|
|
|
|
status_map = {
|
|
"ok": CheckStatus.OK,
|
|
"warn": CheckStatus.WARN,
|
|
"error": CheckStatus.ERROR,
|
|
}
|
|
actual = context.sysdiag_result["status"]
|
|
assert actual == status_map[expected], (
|
|
f"Expected status '{expected}', got '{actual}'"
|
|
)
|
|
|
|
|
|
@then('the sysdiag check details should be "{expected}"')
|
|
def step_sysdiag_details_exact(context: Context, expected: str) -> None:
|
|
actual = context.sysdiag_result["details"]
|
|
assert actual == expected, f"Expected details '{expected}', got '{actual}'"
|
|
|
|
|
|
@then('the sysdiag check details should contain "{text}"')
|
|
def step_sysdiag_details_contains(context: Context, text: str) -> None:
|
|
details = context.sysdiag_result["details"]
|
|
assert text in details, f"Expected '{text}' in details: '{details}'"
|
|
|
|
|
|
@then("the sysdiag check should have a recommendation")
|
|
def step_sysdiag_has_recommendation(context: Context) -> None:
|
|
rec = context.sysdiag_result.get("recommendation")
|
|
assert rec is not None and len(rec) > 0, (
|
|
f"Expected a non-empty recommendation, got: {rec!r}"
|
|
)
|