"""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()