| """Tests for the Strategy Selector — 100% branch coverage per E-2.6 spec.""" |
|
|
| from __future__ import annotations |
|
|
| import time |
|
|
| import pytest |
|
|
| from orchestrator.strategy import ( |
| StrategyDecision, |
| StrategyInputs, |
| select_strategy, |
| ) |
|
|
|
|
| def _inputs(**overrides: object) -> StrategyInputs: |
| """Default-balanced, no-escalation, trusted-user inputs. Override what each test needs.""" |
| base = { |
| "reporter_count": 1, |
| "velocity_zscore": 0.0, |
| "user_risk_tier": "trusted", |
| "rule_match_score": 0.5, |
| "personality": "balanced", |
| "tier_override": "auto", |
| "cold_start": False, |
| } |
| base.update(overrides) |
| return StrategyInputs(**base) |
|
|
|
|
| |
|
|
|
|
| def test_default_is_standard() -> None: |
| d = select_strategy(_inputs()) |
| assert d.tier == "STANDARD" |
| assert d.tool_budget == 4 |
| assert d.time_budget_ms == 3_000 |
| assert d.cost_budget_usd == 0.012 |
| assert d.reasoner_required is True |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.parametrize( |
| ("override", "tier"), |
| [("fast", "FAST"), ("standard", "STANDARD"), ("deep", "DEEP")], |
| ) |
| def test_override_forces_tier(override: str, tier: str) -> None: |
| d = select_strategy(_inputs(tier_override=override)) |
| assert d.tier == tier |
| assert "override" in d.rationale |
|
|
|
|
| def test_cold_start_floors_fast_override() -> None: |
| d = select_strategy(_inputs(tier_override="fast", cold_start=True)) |
| assert d.tier == "STANDARD" |
| assert "cold-start" in d.rationale |
|
|
|
|
| def test_cold_start_does_not_floor_deep_override() -> None: |
| d = select_strategy(_inputs(tier_override="deep", cold_start=True)) |
| assert d.tier == "DEEP" |
|
|
|
|
| |
|
|
|
|
| def test_deep_when_many_reporters() -> None: |
| d = select_strategy(_inputs(reporter_count=4)) |
| assert d.tier == "DEEP" |
| assert "reporter_count" in d.rationale |
|
|
|
|
| def test_deep_when_high_velocity() -> None: |
| d = select_strategy(_inputs(velocity_zscore=3.5)) |
| assert d.tier == "DEEP" |
| assert "velocity_z" in d.rationale |
|
|
|
|
| def test_deep_when_user_watched() -> None: |
| d = select_strategy(_inputs(user_risk_tier="watched")) |
| assert d.tier == "DEEP" |
| assert "watched" in d.rationale |
|
|
|
|
| def test_deep_signals_combine_in_rationale() -> None: |
| d = select_strategy( |
| _inputs(reporter_count=5, velocity_zscore=4.0, user_risk_tier="watched") |
| ) |
| assert d.tier == "DEEP" |
| assert "reporter_count" in d.rationale |
| assert "velocity_z" in d.rationale |
| assert "watched" in d.rationale |
|
|
|
|
| |
|
|
|
|
| def test_fast_eligible_when_clear_cut() -> None: |
| d = select_strategy( |
| _inputs(reporter_count=1, velocity_zscore=0.0, rule_match_score=0.95, user_risk_tier="new") |
| ) |
| assert d.tier == "FAST" |
| assert d.tool_budget == 2 |
| assert d.time_budget_ms == 800 |
| assert d.cost_budget_usd == 0.003 |
| assert d.reasoner_required is False |
|
|
|
|
| def test_fast_blocked_by_cold_start() -> None: |
| """Cold-start mode raises thresholds; FAST is never selected (Specs §12.1).""" |
| d = select_strategy( |
| _inputs(rule_match_score=0.95, user_risk_tier="trusted", cold_start=True) |
| ) |
| assert d.tier == "STANDARD" |
|
|
|
|
| def test_fast_blocked_by_two_reporters() -> None: |
| d = select_strategy(_inputs(reporter_count=2, rule_match_score=0.95)) |
| assert d.tier == "STANDARD" |
|
|
|
|
| def test_fast_blocked_by_velocity_above_threshold() -> None: |
| d = select_strategy(_inputs(velocity_zscore=0.6, rule_match_score=0.95)) |
| assert d.tier == "STANDARD" |
|
|
|
|
| def test_fast_blocked_by_weak_rule_match() -> None: |
| d = select_strategy(_inputs(rule_match_score=0.85)) |
| assert d.tier == "STANDARD" |
|
|
|
|
| def test_fast_blocked_by_neutral_user() -> None: |
| d = select_strategy(_inputs(rule_match_score=0.95, user_risk_tier="neutral")) |
| assert d.tier == "STANDARD" |
|
|
|
|
| |
|
|
|
|
| def test_strict_personality_lowers_reporter_threshold() -> None: |
| """In a strict sub, 3 reporters is enough to trip DEEP.""" |
| d = select_strategy(_inputs(reporter_count=3, personality="strict")) |
| assert d.tier == "DEEP" |
|
|
|
|
| def test_lenient_personality_raises_reporter_threshold() -> None: |
| """In a lenient sub, 4 reporters is *not* enough — needs 5.""" |
| d = select_strategy(_inputs(reporter_count=4, personality="lenient")) |
| assert d.tier == "STANDARD" |
| d2 = select_strategy(_inputs(reporter_count=5, personality="lenient")) |
| assert d2.tier == "DEEP" |
|
|
|
|
| def test_strict_personality_lowers_velocity_threshold() -> None: |
| d = select_strategy(_inputs(velocity_zscore=2.5, personality="strict")) |
| assert d.tier == "DEEP" |
|
|
|
|
| def test_lenient_personality_raises_velocity_threshold() -> None: |
| d = select_strategy(_inputs(velocity_zscore=3.5, personality="lenient")) |
| assert d.tier == "STANDARD" |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.parametrize( |
| ("tier", "tool_budget", "time_budget_ms", "cost_budget_usd", "reasoner"), |
| [ |
| ("FAST", 2, 800, 0.003, False), |
| ("STANDARD", 4, 3_000, 0.012, True), |
| ("DEEP", 6, 6_000, 0.030, True), |
| ], |
| ) |
| def test_budget_table_matches_specs( |
| tier: str, tool_budget: int, time_budget_ms: int, cost_budget_usd: float, reasoner: bool |
| ) -> None: |
| """Locks the Specs §7.1 numbers into a test so casual edits get caught.""" |
| d = select_strategy(_inputs(tier_override=tier.lower())) |
| assert d.tier == tier |
| assert d.tool_budget == tool_budget |
| assert d.time_budget_ms == time_budget_ms |
| assert d.cost_budget_usd == cost_budget_usd |
| assert d.reasoner_required is reasoner |
|
|
|
|
| |
|
|
|
|
| def test_select_strategy_under_50ms() -> None: |
| """Spec: pure function, <50ms — 1000 calls should fit well under that bound.""" |
| inputs = _inputs(reporter_count=2, velocity_zscore=1.0) |
| started = time.perf_counter() |
| for _ in range(1000): |
| select_strategy(inputs) |
| elapsed_ms = (time.perf_counter() - started) * 1000 |
| |
| assert elapsed_ms < 50.0, f"too slow: {elapsed_ms:.2f}ms for 1000 calls" |
|
|
|
|
| |
|
|
|
|
| def test_decision_is_frozen() -> None: |
| from dataclasses import FrozenInstanceError |
|
|
| d = select_strategy(_inputs()) |
| with pytest.raises(FrozenInstanceError): |
| d.tier = "DEEP" |
| assert isinstance(d, StrategyDecision) |
|
|
|
|
| def test_override_unknown_value_raises() -> None: |
| """Defensive: type-checker forbids bad override strings, but the runtime |
| guard catches them anyway if e.g. settings somehow get a stale value.""" |
| from orchestrator.strategy import _override_tier |
|
|
| with pytest.raises(ValueError, match="unsupported override"): |
| _override_tier("nonsense") |
|
|
|
|
| |
|
|
|
|
| def test_thread_escalated_lowers_reporter_threshold() -> None: |
| """On a balanced sub, 3 reporters is below DEEP — *unless* the thread |
| has known prior escalation, which drops the threshold from 4 to 3.""" |
| not_escalated = select_strategy(_inputs(reporter_count=3, thread_escalated=False)) |
| assert not_escalated.tier == "STANDARD" |
| escalated = select_strategy(_inputs(reporter_count=3, thread_escalated=True)) |
| assert escalated.tier == "DEEP" |
| assert "reporter_count" in escalated.rationale |
|
|
|
|
| def test_thread_escalated_lowers_velocity_threshold() -> None: |
| not_escalated = select_strategy(_inputs(velocity_zscore=2.5, thread_escalated=False)) |
| assert not_escalated.tier == "STANDARD" |
| escalated = select_strategy(_inputs(velocity_zscore=2.5, thread_escalated=True)) |
| assert escalated.tier == "DEEP" |
| assert "velocity_z" in escalated.rationale |
|
|
|
|
| def test_thread_escalated_stacks_with_strict_personality() -> None: |
| """strict (-1) + thread_escalated (-1) = reporter threshold 2.""" |
| d = select_strategy( |
| _inputs(reporter_count=2, personality="strict", thread_escalated=True) |
| ) |
| assert d.tier == "DEEP" |
|
|
|
|
| def test_thread_escalated_plus_user_risk_triggers_deep_alone() -> None: |
| """Even at quiet baseline (1 reporter, z=0), a watched/neutral user on |
| a known-escalating thread is DEEP. Captures the 'context-aware' |
| investigation thesis from docs/Specs.md §1.2.""" |
| for tier in ("neutral", "watched"): |
| d = select_strategy( |
| _inputs( |
| reporter_count=1, |
| velocity_zscore=0.0, |
| user_risk_tier=tier, |
| thread_escalated=True, |
| ) |
| ) |
| assert d.tier == "DEEP", f"user_risk_tier={tier}" |
| assert "thread_escalated+user_risk" in d.rationale |
|
|
|
|
| def test_thread_escalated_plus_new_user_does_not_force_deep() -> None: |
| """A new user on an escalating thread isn't automatically DEEP — needs |
| a confirming velocity / reporter signal. Avoids false-positives on |
| newcomers who happened to land in a contentious thread.""" |
| d = select_strategy( |
| _inputs(reporter_count=1, velocity_zscore=0.0, user_risk_tier="new", thread_escalated=True) |
| ) |
| |
| assert d.tier == "STANDARD" |
|
|
|
|
| def test_thread_escalated_vetoes_fast() -> None: |
| """A target that would otherwise hit FAST (single report, clear rule |
| match, trusted user) is bumped to STANDARD when the thread is known |
| to be escalating — we want the Reasoner's review, not a shortcut.""" |
| base_inputs: dict[str, object] = { |
| "reporter_count": 1, |
| "velocity_zscore": 0.0, |
| "rule_match_score": 0.95, |
| "user_risk_tier": "trusted", |
| } |
| fast = select_strategy(_inputs(**base_inputs, thread_escalated=False)) |
| assert fast.tier == "FAST" |
| no_fast = select_strategy(_inputs(**base_inputs, thread_escalated=True)) |
| assert no_fast.tier == "STANDARD" |
|
|
|
|
| def test_thread_escalated_default_is_false() -> None: |
| """Backwards-compat: existing callers that don't set thread_escalated |
| keep working as before.""" |
| d = select_strategy(_inputs()) |
| assert d.tier == "STANDARD" |
|
|