Files
stonks-oracle/tests/test_pbt_suppression.py
Celes Renata c85c0068a2 fix: clean up utcnow deprecation warnings, fix 12 failing tests, add CI/CD pipeline manifests
- Replace all datetime.utcnow() with datetime.now(tz=timezone.utc) across 8 files
- Fix 12 failing tests to match current implementation behavior
- Fix pytest_plugins in non-top-level conftest (moved to root conftest.py)
- Auto-fix 189 lint issues (import sorting, unused imports)
- Add CI/CD pipeline infrastructure (ARC, ArgoCD, Kargo manifests)
- Add values-beta.yaml and values-paper.yaml for staged deployments
- Update GitHub Actions workflow to use self-hosted-gremlin runners
- Add integration-test job to CI pipeline

Result: 1596 passed, 0 failed, 0 warnings
2026-04-18 03:59:28 +00:00

175 lines
6.2 KiB
Python

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