| from underdog_lab.scenarios.factory import FallbackExtractor |
| from underdog_lab.scenarios.mock_extractor import MockScenarioExtractor |
| from underdog_lab.scenarios.schemas import ExtractedFactor, ScenarioExtraction |
| from underdog_lab.scenarios.taxonomy import FactorType |
|
|
|
|
| class BrokenExtractor: |
| name = "broken local model" |
|
|
| def extract(self, text, match): |
| raise RuntimeError("model could not load") |
|
|
|
|
| def test_fallback_exposes_backend_and_error(neutral_match): |
| extractor = FallbackExtractor() |
| extractor.primary = BrokenExtractor() |
| extractor.fallback = MockScenarioExtractor() |
|
|
| result = extractor.extract( |
| f"{neutral_match.home_team}'s striker is out.", |
| neutral_match, |
| ) |
|
|
| assert result.factors |
| assert extractor.last_backend == "deterministic fallback" |
| assert "RuntimeError" in extractor.last_error |
|
|
|
|
| class EmptyExtractor: |
| name = "empty local model" |
|
|
| def extract(self, text, match): |
| return ScenarioExtraction() |
|
|
|
|
| def test_empty_primary_result_uses_honest_semantic_recovery(neutral_match): |
| extractor = FallbackExtractor() |
| extractor.primary = EmptyExtractor() |
| extractor.fallback = MockScenarioExtractor() |
|
|
| result = extractor.extract( |
| f"{neutral_match.home_team}'s striker is confirmed out.", |
| neutral_match, |
| ) |
|
|
| assert len(result.factors) == 1 |
| assert extractor.last_backend == "deterministic fallback" |
| assert "deterministic recovery" in extractor.last_error |
|
|
|
|
| class ZeroWeightExtractor: |
| name = "zero-weight local model" |
|
|
| def extract(self, text, match): |
| return ScenarioExtraction( |
| factors=[ |
| ExtractedFactor( |
| factor_type=FactorType.HOME_ADVANTAGE, |
| team="both", |
| severity=0.0, |
| certainty=0.0, |
| evidence="irrelevant", |
| ) |
| ] |
| ) |
|
|
|
|
| def test_zero_weight_hallucination_uses_semantic_recovery(neutral_match): |
| extractor = FallbackExtractor() |
| extractor.primary = ZeroWeightExtractor() |
| extractor.fallback = MockScenarioExtractor() |
|
|
| result = extractor.extract( |
| f"{neutral_match.home_team}'s striker is confirmed out.", |
| neutral_match, |
| ) |
|
|
| assert result.factors[0].factor_type == FactorType.KEY_ATTACKER_UNAVAILABLE |
| assert extractor.last_backend == "deterministic fallback" |
|
|
|
|
| class CountingBrokenExtractor: |
| name = "broken local model" |
|
|
| def __init__(self) -> None: |
| self.calls = 0 |
|
|
| def extract(self, text, match): |
| self.calls += 1 |
| raise RuntimeError("model could not load") |
|
|
| def warmup(self) -> None: |
| raise RuntimeError("model could not load") |
|
|
|
|
| def test_primary_failure_is_remembered_and_not_retried(neutral_match): |
| extractor = FallbackExtractor() |
| broken = CountingBrokenExtractor() |
| extractor.primary = broken |
| extractor.fallback = MockScenarioExtractor() |
|
|
| text = f"{neutral_match.home_team}'s striker is out." |
|
|
| first = extractor.extract(text, neutral_match) |
| assert extractor.primary_unavailable is True |
| assert extractor.last_backend == "deterministic fallback" |
| assert "RuntimeError" in extractor.last_error |
| assert broken.calls == 1 |
|
|
| second = extractor.extract(text, neutral_match) |
| assert broken.calls == 1 |
| assert extractor.last_backend == "deterministic fallback" |
| assert second.factors == first.factors |
|
|
|
|
| def test_warmup_failure_is_remembered_and_skips_primary(neutral_match): |
| extractor = FallbackExtractor() |
| broken = CountingBrokenExtractor() |
| extractor.primary = broken |
| extractor.fallback = MockScenarioExtractor() |
|
|
| extractor.warmup() |
| assert extractor.primary_unavailable is True |
| assert extractor.last_backend == "deterministic fallback" |
| assert "RuntimeError" in extractor.last_error |
|
|
| extractor.extract(f"{neutral_match.home_team}'s striker is out.", neutral_match) |
| assert broken.calls == 0 |
|
|