phase 14-15: docker build validation and helm deployment
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
"""Tests for recommendation worker — generating recommendations from trend data.
|
||||
|
||||
Tests the pure logic functions (no DB required). Async DB functions
|
||||
are covered by integration tests.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from services.recommendation.eligibility import evaluate_eligibility
|
||||
from services.recommendation.worker import (
|
||||
_extract_risk_classification,
|
||||
build_recommendation,
|
||||
build_thesis,
|
||||
classify_risk,
|
||||
)
|
||||
from services.shared.schemas import (
|
||||
ActionType,
|
||||
RecommendationMode,
|
||||
TrendDirection,
|
||||
TrendSummary,
|
||||
TrendWindow,
|
||||
)
|
||||
|
||||
NOW = datetime(2026, 4, 11, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _make_summary(
|
||||
ticker: str = "AAPL",
|
||||
direction: TrendDirection = TrendDirection.BULLISH,
|
||||
strength: float = 0.5,
|
||||
confidence: float = 0.65,
|
||||
contradiction: float = 0.1,
|
||||
supporting: list[str] | None = None,
|
||||
opposing: list[str] | None = None,
|
||||
catalysts: list[str] | None = None,
|
||||
risks: list[str] | None = None,
|
||||
window: TrendWindow = TrendWindow.SEVEN_DAY,
|
||||
) -> TrendSummary:
|
||||
return TrendSummary(
|
||||
entity_type="company",
|
||||
entity_id=ticker,
|
||||
window=window,
|
||||
trend_direction=direction,
|
||||
trend_strength=strength,
|
||||
confidence=confidence,
|
||||
top_supporting_evidence=supporting or ["doc1", "doc2", "doc3"],
|
||||
top_opposing_evidence=opposing or [],
|
||||
dominant_catalysts=catalysts or ["earnings"],
|
||||
material_risks=risks or ["regulatory scrutiny"],
|
||||
contradiction_score=contradiction,
|
||||
generated_at=NOW,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build_thesis
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_thesis_contains_ticker_and_direction():
|
||||
summary = _make_summary()
|
||||
result = evaluate_eligibility(summary)
|
||||
thesis = build_thesis(summary, result)
|
||||
assert "AAPL" in thesis
|
||||
assert "bullish" in thesis
|
||||
|
||||
|
||||
def test_thesis_includes_catalysts():
|
||||
summary = _make_summary(catalysts=["product", "m_and_a"])
|
||||
result = evaluate_eligibility(summary)
|
||||
thesis = build_thesis(summary, result)
|
||||
assert "product" in thesis
|
||||
|
||||
|
||||
def test_thesis_includes_contradiction_note():
|
||||
summary = _make_summary(contradiction=0.3)
|
||||
result = evaluate_eligibility(summary)
|
||||
thesis = build_thesis(summary, result)
|
||||
assert "disagreement" in thesis
|
||||
|
||||
|
||||
def test_thesis_includes_risks():
|
||||
summary = _make_summary(risks=["supply chain disruption"])
|
||||
result = evaluate_eligibility(summary)
|
||||
thesis = build_thesis(summary, result)
|
||||
assert "supply chain disruption" in thesis
|
||||
|
||||
|
||||
def test_thesis_includes_evidence_counts():
|
||||
summary = _make_summary(
|
||||
supporting=["d1", "d2"],
|
||||
opposing=["d3"],
|
||||
)
|
||||
result = evaluate_eligibility(summary)
|
||||
thesis = build_thesis(summary, result)
|
||||
assert "2 supporting" in thesis
|
||||
assert "1 opposing" in thesis
|
||||
|
||||
|
||||
def test_thesis_includes_action():
|
||||
summary = _make_summary()
|
||||
result = evaluate_eligibility(summary)
|
||||
thesis = build_thesis(summary, result)
|
||||
assert "BUY" in thesis
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# classify_risk
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_risk_low_for_strong_signal():
|
||||
summary = _make_summary(
|
||||
confidence=0.8,
|
||||
contradiction=0.05,
|
||||
supporting=["d1", "d2", "d3", "d4", "d5"],
|
||||
)
|
||||
result = evaluate_eligibility(summary)
|
||||
assert classify_risk(summary, result) == "low"
|
||||
|
||||
|
||||
def test_risk_high_for_weak_signal():
|
||||
summary = _make_summary(
|
||||
confidence=0.36,
|
||||
contradiction=0.55,
|
||||
supporting=["d1"],
|
||||
opposing=[],
|
||||
)
|
||||
result = evaluate_eligibility(summary)
|
||||
risk = classify_risk(summary, result)
|
||||
assert risk in ("high", "very_high")
|
||||
|
||||
|
||||
def test_risk_moderate_for_mixed():
|
||||
summary = _make_summary(
|
||||
confidence=0.5,
|
||||
contradiction=0.2,
|
||||
supporting=["d1", "d2"],
|
||||
opposing=["d3"],
|
||||
)
|
||||
result = evaluate_eligibility(summary)
|
||||
assert classify_risk(summary, result) == "moderate"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build_recommendation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_build_recommendation_basic():
|
||||
summary = _make_summary()
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result, reference_time=NOW)
|
||||
|
||||
assert rec.ticker == "AAPL"
|
||||
assert rec.action == ActionType.BUY
|
||||
assert rec.confidence == summary.confidence
|
||||
assert rec.time_horizon == "swing_1d_10d"
|
||||
assert rec.generated_at == NOW
|
||||
assert len(rec.evidence_refs) == 3 # 3 supporting + 0 opposing
|
||||
assert rec.model_metadata.provider == "deterministic"
|
||||
|
||||
|
||||
def test_build_recommendation_includes_risk_in_thesis():
|
||||
summary = _make_summary()
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result)
|
||||
assert rec.thesis.startswith("[risk:")
|
||||
|
||||
|
||||
def test_build_recommendation_with_llm_thesis():
|
||||
"""When llm_thesis is provided, it replaces the deterministic body."""
|
||||
summary = _make_summary()
|
||||
result = evaluate_eligibility(summary)
|
||||
llm_text = "Apple exhibits a bullish posture driven by strong earnings."
|
||||
rec = build_recommendation(summary, result, llm_thesis=llm_text)
|
||||
assert llm_text in rec.thesis
|
||||
assert rec.thesis.startswith("[risk:")
|
||||
assert rec.model_metadata.provider == "ollama"
|
||||
assert rec.model_metadata.model_name == "thesis-rewrite"
|
||||
|
||||
|
||||
def test_build_recommendation_without_llm_thesis_uses_deterministic():
|
||||
"""When llm_thesis is None, the deterministic thesis is used."""
|
||||
summary = _make_summary()
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result)
|
||||
assert rec.model_metadata.provider == "deterministic"
|
||||
assert rec.model_metadata.model_name == "eligibility-v1"
|
||||
|
||||
|
||||
def test_build_recommendation_combines_evidence():
|
||||
summary = _make_summary(
|
||||
supporting=["s1", "s2"],
|
||||
opposing=["o1"],
|
||||
)
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result)
|
||||
assert rec.evidence_refs == ["s1", "s2", "o1"]
|
||||
|
||||
|
||||
def test_build_recommendation_position_sizing():
|
||||
summary = _make_summary(confidence=0.7)
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result)
|
||||
assert rec.position_sizing.portfolio_pct == result.position_sizing.portfolio_pct
|
||||
assert rec.position_sizing.max_loss_pct == result.position_sizing.max_loss_pct
|
||||
|
||||
|
||||
def test_build_recommendation_invalidation_conditions():
|
||||
summary = _make_summary()
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result)
|
||||
assert len(rec.invalidation_conditions) > 0
|
||||
|
||||
|
||||
def test_build_recommendation_ineligible_is_informational():
|
||||
"""When eligibility fails, mode should be informational (Req 7.4)."""
|
||||
summary = _make_summary(confidence=0.2)
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result)
|
||||
assert rec.mode == RecommendationMode.INFORMATIONAL
|
||||
|
||||
|
||||
def test_build_recommendation_sell_action():
|
||||
summary = _make_summary(direction=TrendDirection.BEARISH, strength=0.5)
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result)
|
||||
assert rec.action == ActionType.SELL
|
||||
assert "SELL" in rec.thesis
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _extract_risk_classification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_extract_risk_classification_from_thesis():
|
||||
assert _extract_risk_classification("[risk:low] Some thesis text") == "low"
|
||||
assert _extract_risk_classification("[risk:very_high] Bad signal") == "very_high"
|
||||
|
||||
|
||||
def test_extract_risk_classification_missing_prefix():
|
||||
assert _extract_risk_classification("No risk prefix here") == "moderate"
|
||||
|
||||
|
||||
def test_extract_risk_classification_empty():
|
||||
assert _extract_risk_classification("") == "moderate"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build_recommendation stores full model metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_build_recommendation_model_metadata_deterministic():
|
||||
summary = _make_summary()
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result, reference_time=NOW)
|
||||
assert rec.model_metadata.provider == "deterministic"
|
||||
assert rec.model_metadata.model_name == "eligibility-v1"
|
||||
assert rec.model_metadata.schema_version == "1.0.0"
|
||||
|
||||
|
||||
def test_build_recommendation_model_metadata_llm():
|
||||
summary = _make_summary()
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(
|
||||
summary, result, reference_time=NOW,
|
||||
llm_thesis="Rewritten thesis text.",
|
||||
)
|
||||
assert rec.model_metadata.provider == "ollama"
|
||||
assert rec.model_metadata.model_name == "thesis-rewrite"
|
||||
assert rec.model_metadata.prompt_version != ""
|
||||
|
||||
|
||||
def test_build_recommendation_risk_classification_in_thesis():
|
||||
"""The risk classification should be embedded in the thesis prefix."""
|
||||
summary = _make_summary(confidence=0.8, contradiction=0.05,
|
||||
supporting=["d1", "d2", "d3", "d4", "d5"])
|
||||
result = evaluate_eligibility(summary)
|
||||
rec = build_recommendation(summary, result, reference_time=NOW)
|
||||
risk = _extract_risk_classification(rec.thesis)
|
||||
assert risk == classify_risk(summary, result)
|
||||
Reference in New Issue
Block a user