feat: competitive intelligence & historical pattern matching layer
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
"""Property-based tests for pattern-only suppression.
|
||||
|
||||
Feature: competitive-historical-patterns
|
||||
|
||||
Uses Hypothesis to validate correctness properties of the pattern-only
|
||||
suppression logic in the recommendation service.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from services.recommendation.suppression import (
|
||||
PATTERN_ONLY_CAVEAT,
|
||||
evaluate_pattern_only_suppression,
|
||||
)
|
||||
from services.shared.schemas import TrendDirection, TrendSummary, TrendWindow
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hypothesis strategies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _minimal_trend_summary() -> st.SearchStrategy[TrendSummary]:
|
||||
"""Generate a minimal TrendSummary with random direction and window."""
|
||||
return st.builds(
|
||||
TrendSummary,
|
||||
entity_id=st.text(
|
||||
alphabet=st.characters(whitelist_categories=("Lu",)),
|
||||
min_size=1,
|
||||
max_size=5,
|
||||
),
|
||||
window=st.sampled_from(list(TrendWindow)),
|
||||
trend_direction=st.sampled_from(list(TrendDirection)),
|
||||
confidence=st.floats(min_value=0.0, max_value=1.0, allow_nan=False),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 18: Pattern-only suppression
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestProperty18PatternOnlySuppression:
|
||||
"""Feature: competitive-historical-patterns, Property 18: Pattern-only suppression
|
||||
|
||||
For any trend summary where the trend direction is driven solely by
|
||||
pattern-based and competitive signals (no company-specific or macro
|
||||
signals support the direction), the resulting recommendation SHALL have
|
||||
mode = 'informational' and the thesis SHALL contain a pattern-only caveat.
|
||||
|
||||
**Validates: Requirements 9.3**
|
||||
"""
|
||||
|
||||
@given(
|
||||
summary=_minimal_trend_summary(),
|
||||
pattern_signal_count=st.integers(min_value=1, max_value=100),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_pattern_only_signals_trigger_suppression(
|
||||
self,
|
||||
summary: TrendSummary,
|
||||
pattern_signal_count: int,
|
||||
):
|
||||
"""**Validates: Requirements 9.3**
|
||||
|
||||
When pattern_signal_count > 0 AND company_signal_count == 0 AND
|
||||
macro_signal_count == 0, suppression must be triggered (returns True).
|
||||
"""
|
||||
result = evaluate_pattern_only_suppression(
|
||||
summary=summary,
|
||||
pattern_signal_count=pattern_signal_count,
|
||||
company_signal_count=0,
|
||||
macro_signal_count=0,
|
||||
)
|
||||
assert result is True, (
|
||||
f"Expected suppression for pattern_only scenario "
|
||||
f"(pattern={pattern_signal_count}, company=0, macro=0), got False"
|
||||
)
|
||||
|
||||
@given(
|
||||
summary=_minimal_trend_summary(),
|
||||
pattern_signal_count=st.integers(min_value=0, max_value=100),
|
||||
company_signal_count=st.integers(min_value=1, max_value=100),
|
||||
macro_signal_count=st.integers(min_value=0, max_value=100),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_company_signals_prevent_suppression(
|
||||
self,
|
||||
summary: TrendSummary,
|
||||
pattern_signal_count: int,
|
||||
company_signal_count: int,
|
||||
macro_signal_count: int,
|
||||
):
|
||||
"""**Validates: Requirements 9.3**
|
||||
|
||||
When company_signal_count > 0, suppression must NOT be triggered
|
||||
regardless of pattern or macro signal counts.
|
||||
"""
|
||||
result = evaluate_pattern_only_suppression(
|
||||
summary=summary,
|
||||
pattern_signal_count=pattern_signal_count,
|
||||
company_signal_count=company_signal_count,
|
||||
macro_signal_count=macro_signal_count,
|
||||
)
|
||||
assert result is False, (
|
||||
f"Expected no suppression when company_signal_count={company_signal_count} > 0, "
|
||||
f"got True"
|
||||
)
|
||||
|
||||
@given(
|
||||
summary=_minimal_trend_summary(),
|
||||
pattern_signal_count=st.integers(min_value=0, max_value=100),
|
||||
macro_signal_count=st.integers(min_value=1, max_value=100),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_macro_signals_prevent_suppression(
|
||||
self,
|
||||
summary: TrendSummary,
|
||||
pattern_signal_count: int,
|
||||
macro_signal_count: int,
|
||||
):
|
||||
"""**Validates: Requirements 9.3**
|
||||
|
||||
When macro_signal_count > 0 (and company_signal_count == 0),
|
||||
suppression must NOT be triggered regardless of pattern count.
|
||||
"""
|
||||
result = evaluate_pattern_only_suppression(
|
||||
summary=summary,
|
||||
pattern_signal_count=pattern_signal_count,
|
||||
company_signal_count=0,
|
||||
macro_signal_count=macro_signal_count,
|
||||
)
|
||||
assert result is False, (
|
||||
f"Expected no suppression when macro_signal_count={macro_signal_count} > 0, "
|
||||
f"got True"
|
||||
)
|
||||
|
||||
@given(
|
||||
summary=_minimal_trend_summary(),
|
||||
company_signal_count=st.integers(min_value=0, max_value=100),
|
||||
macro_signal_count=st.integers(min_value=0, max_value=100),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_zero_pattern_signals_no_suppression(
|
||||
self,
|
||||
summary: TrendSummary,
|
||||
company_signal_count: int,
|
||||
macro_signal_count: int,
|
||||
):
|
||||
"""**Validates: Requirements 9.3**
|
||||
|
||||
When pattern_signal_count == 0, suppression must NOT be triggered
|
||||
regardless of other signal counts.
|
||||
"""
|
||||
result = evaluate_pattern_only_suppression(
|
||||
summary=summary,
|
||||
pattern_signal_count=0,
|
||||
company_signal_count=company_signal_count,
|
||||
macro_signal_count=macro_signal_count,
|
||||
)
|
||||
assert result is False, (
|
||||
f"Expected no suppression when pattern_signal_count=0, got True"
|
||||
)
|
||||
|
||||
def test_pattern_only_caveat_constant_exists(self):
|
||||
"""**Validates: Requirements 9.3**
|
||||
|
||||
The PATTERN_ONLY_CAVEAT constant must exist and contain expected
|
||||
key phrases for informational-mode recommendations.
|
||||
"""
|
||||
assert isinstance(PATTERN_ONLY_CAVEAT, str)
|
||||
assert len(PATTERN_ONLY_CAVEAT) > 0
|
||||
assert "pattern" in PATTERN_ONLY_CAVEAT.lower()
|
||||
assert "informational" in PATTERN_ONLY_CAVEAT.lower()
|
||||
Reference in New Issue
Block a user