Files
placeholder/features/steps/predictive_error_prevention_steps.py
freemo 583e6b7ea2 feat(correcting-plans): implement Predictive Error Prevention (Layer 4) with Error Pattern Database
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
2026-03-08 21:53:21 -04:00

173 lines
6.6 KiB
Python

"""Step definitions for predictive_error_prevention.feature.
Exercises the Error Pattern Database (Layer 4: Predictive Error Prevention)
including recording, matching, formatting, statistics, and keyword extraction.
"""
from __future__ import annotations
from typing import Any
from behave import given, then, when
from cleveragents.application.services.error_pattern_service import (
ErrorPatternService,
)
from cleveragents.domain.models.core.error_pattern import (
ErrorPattern,
PreventiveGuidance,
)
from cleveragents.infrastructure.database.error_pattern_repository import (
ErrorPatternRepository,
)
@given("a fresh error pattern service")
def step_pep_fresh_service(context: Any) -> None:
context.pep_service = ErrorPatternService()
context.pep_last_pattern = None
context.pep_guidance: PreventiveGuidance | None = None
context.pep_statistics: dict[str, int | float] | None = None
@given(
'a recorded pattern "{pattern}" with preventive check "{check}" and keywords "{keywords}"'
)
def step_pep_record_pattern_with_check_and_keywords(
context: Any, pattern: str, check: str, keywords: str
) -> None:
kw_tuple = tuple(k.strip() for k in keywords.split(","))
context.pep_last_pattern = context.pep_service.record_failure(
pattern_text=pattern,
failure_description=f"Historical failure for {pattern}",
preventive_checks=(check,),
keywords=kw_tuple,
)
@given('a recorded pattern "{pattern}" with preventive check "{check}"')
def step_pep_record_pattern_with_check(context: Any, pattern: str, check: str) -> None:
context.pep_last_pattern = context.pep_service.record_failure(
pattern_text=pattern,
failure_description=f"Historical failure for {pattern}",
preventive_checks=(check,),
)
@when('I record a failure with pattern "{pattern}" and failure "{failure}"')
def step_pep_record_failure(context: Any, pattern: str, failure: str) -> None:
context.pep_last_pattern = context.pep_service.record_failure(
pattern_text=pattern,
failure_description=failure,
)
@when('I match context "{text}"')
def step_pep_match_context(context: Any, text: str) -> None:
context.pep_guidance = context.pep_service.match_patterns(text)
@when("I request error pattern statistics")
def step_pep_request_statistics(context: Any) -> None:
context.pep_statistics = context.pep_service.get_statistics()
@then("the error pattern database should contain {count:d} pattern")
def step_pep_assert_pattern_count(context: Any, count: int) -> None:
patterns = context.pep_service.list_patterns()
assert len(patterns) == count, f"Expected {count} patterns, got {len(patterns)}"
@then("the pattern should have frequency {freq:d}")
def step_pep_assert_frequency(context: Any, freq: int) -> None:
patterns = context.pep_service.list_patterns()
assert len(patterns) > 0, "No patterns found"
actual = patterns[0].frequency
assert actual == freq, f"Expected frequency {freq}, got {actual}"
@then("the pattern should have {count:d} historical failures")
def step_pep_assert_historical_failures_count(context: Any, count: int) -> None:
patterns = context.pep_service.list_patterns()
assert len(patterns) > 0, "No patterns found"
actual = len(patterns[0].historical_failures)
assert actual == count, f"Expected {count} historical failures, got {actual}"
@then('preventive guidance should contain "{text}"')
def step_pep_assert_guidance_contains(context: Any, text: str) -> None:
assert context.pep_guidance is not None, "No guidance available"
all_checks = context.pep_guidance.preventive_checks
assert any(text in c for c in all_checks), (
f"Expected guidance to contain '{text}', got {all_checks}"
)
@then("preventive guidance should be empty")
def step_pep_assert_guidance_empty(context: Any) -> None:
assert context.pep_guidance is not None, "No guidance available"
assert not context.pep_guidance.has_guidance, (
f"Expected empty guidance, got {context.pep_guidance.preventive_checks}"
)
@then('the formatted guidance should contain "{text}"')
def step_pep_assert_formatted_contains(context: Any, text: str) -> None:
assert context.pep_guidance is not None, "No guidance available"
formatted = context.pep_guidance.format_for_context()
assert text in formatted, (
f"Expected formatted guidance to contain '{text}', got: {formatted}"
)
@then("the statistics should show {count:d} total patterns")
def step_pep_assert_stats_total(context: Any, count: int) -> None:
assert context.pep_statistics is not None, "No statistics available"
actual = context.pep_statistics["total_patterns"]
assert actual == count, f"Expected {count} total patterns, got {actual}"
@then('the recorded pattern should have keywords including "{keyword}"')
def step_pep_assert_keyword(context: Any, keyword: str) -> None:
assert context.pep_last_pattern is not None, "No pattern recorded"
keywords_lower = [k.lower() for k in context.pep_last_pattern.keywords]
assert keyword.lower() in keywords_lower, (
f"Expected keyword '{keyword}' in {context.pep_last_pattern.keywords}"
)
@then("the recorded pattern can be retrieved by its ID")
def step_pep_retrieve_by_id(context: Any) -> None:
assert context.pep_last_pattern is not None, "No pattern recorded"
repo = ErrorPatternRepository()
repo.create(context.pep_last_pattern)
retrieved = repo.get(context.pep_last_pattern.pattern_id)
assert retrieved is not None, "Pattern not found by ID"
assert retrieved.pattern_id == context.pep_last_pattern.pattern_id
@when("I delete the last recorded pattern")
def step_pep_delete_last_pattern(context: Any) -> None:
assert context.pep_last_pattern is not None, "No pattern recorded"
# Access the internal repo to delete
deleted = context.pep_service._repo.delete(context.pep_last_pattern.pattern_id)
assert deleted, "Delete returned False"
@then("the error pattern database should contain {count:d} patterns")
def step_pep_assert_pattern_count_plural(context: Any, count: int) -> None:
patterns = context.pep_service.list_patterns()
assert len(patterns) == count, f"Expected {count} patterns, got {len(patterns)}"
@given('a recorded pattern with no keywords for "{pattern}" with check "{check}"')
def step_pep_record_pattern_no_keywords(context: Any, pattern: str, check: str) -> None:
ep = ErrorPattern(
pattern=pattern,
historical_failures=("Initial failure",),
preventive_checks=(check,),
keywords=(), # No keywords — forces fallback matching
)
context.pep_service._repo.create(ep)
context.pep_last_pattern = ep