forked from HAL9000/cleveragents-core
583e6b7ea2
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
173 lines
6.6 KiB
Python
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
|