Spaces:
Sleeping
Sleeping
| """ | |
| Smoke tests: Node 8 (Phoneme Risk Assessor) boundary conditions. | |
| Covers: | |
| 1. Risk scoring formula and filtering thresholds. | |
| 2. Risk type decision tree priority. | |
| 3. Safety level mapping from max risk score. | |
| 4. Ranking sorting and Sub-env 2 hint propagation. | |
| 5. Cluster formation and mitigation generation. | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| from src.envs.subenv3.node8_phoneme_risk import assess_phoneme_risk | |
| from src.schemas.subenv3 import PhonemeRiskObservation, WeightEvidenceDossier | |
| # --------------------------------------------------------------------------- | |
| # Helpers | |
| # --------------------------------------------------------------------------- | |
| def base_obs(**overrides) -> PhonemeRiskObservation: | |
| """Return a PhonemeRiskObservation with safe defaults.""" | |
| defaults = dict( | |
| weight_evidence=WeightEvidenceDossier( | |
| weight_file_id="test_weights", | |
| training_quality="healthy", | |
| rank_utilization_assessment="efficient", | |
| high_entropy_token_flags=[], | |
| layer_anomaly_flags=[], | |
| overall_behavioral_risk="low", | |
| evidence_summary="Baseline healthy weights evidence.", | |
| ), | |
| high_entropy_token_flags=[], | |
| phoneme_vocabulary=["AH", "EE", "OW", "IY", "EY", "ZH", "TH", "NG"], | |
| phoneme_to_token_indices={"EE": [0], "OW": [1], "AH": [2]}, | |
| phoneme_entropy_scores={}, | |
| phoneme_influence_scores={}, | |
| phoneme_cooccurrence_anomalies=[], | |
| behavior_vocabulary=["smile", "jaw_drift", "head_turn", "brow_raise"], | |
| training_data_phoneme_distribution=None, | |
| suspected_anomalous_phonemes_from_subenv2=None, | |
| ) | |
| return PhonemeRiskObservation(**{**defaults, **overrides}) | |
| # =========================================================================== | |
| # Risk Scoring & Type Selection | |
| # =========================================================================== | |
| def test_all_zero_scores_produces_safe(): | |
| """No high entropy/influence scores -> safe, empty ranking.""" | |
| obs = base_obs(phoneme_entropy_scores={}, phoneme_influence_scores={}) | |
| action = assess_phoneme_risk(obs) | |
| assert action.model_behavioral_safety == "safe" | |
| assert action.phoneme_risk_ranking == [] | |
| def test_high_ee_scores_triggers_expression(): | |
| """entropy > 0.7 and influence > 0.5 -> expression_trigger.""" | |
| obs = base_obs( | |
| phoneme_entropy_scores={"EE": 0.9}, | |
| phoneme_influence_scores={"EE": 0.85} | |
| ) | |
| # risk = 0.6*0.9 + 0.4*0.85 = 0.54 + 0.34 = 0.88 | |
| action = assess_phoneme_risk(obs) | |
| ee_entry = next(e for e in action.phoneme_risk_ranking if e.phoneme == "EE") | |
| assert ee_entry.risk_type == "expression_trigger" | |
| assert ee_entry.risk_score > 0.5 | |
| def test_high_influence_alone_triggers_identity(): | |
| """influence > 0.7 and entropy not > 0.7 -> identity_trigger.""" | |
| obs = base_obs( | |
| phoneme_entropy_scores={"OW": 0.3}, | |
| phoneme_influence_scores={"OW": 0.85} | |
| ) | |
| # risk = 0.6*0.3 + 0.4*0.85 = 0.18 + 0.34 = 0.52 | |
| action = assess_phoneme_risk(obs) | |
| ow_entry = next(e for e in action.phoneme_risk_ranking if e.phoneme == "OW") | |
| assert ow_entry.risk_type == "identity_trigger" | |
| def test_threshold_03_filters_low_risk(): | |
| """risk_score <= 0.3 -> filtered out.""" | |
| obs = base_obs( | |
| phoneme_entropy_scores={"AH": 0.2}, | |
| phoneme_influence_scores={"AH": 0.1} | |
| ) | |
| # risk = 0.12 + 0.04 = 0.16 <= 0.3 | |
| action = assess_phoneme_risk(obs) | |
| assert "AH" not in [e.phoneme for e in action.phoneme_risk_ranking] | |
| def test_threshold_03_exact_boundary(): | |
| """risk_score == 0.3 is NOT > 0.3 -> filtered out.""" | |
| # To get 0.3 exactly: (0.3*0.6) + (0.3*0.4) = 0.18 + 0.12 = 0.3 | |
| obs = base_obs( | |
| phoneme_entropy_scores={"AH": 0.3}, | |
| phoneme_influence_scores={"AH": 0.3} | |
| ) | |
| action = assess_phoneme_risk(obs) | |
| assert "AH" not in [e.phoneme for e in action.phoneme_risk_ranking] | |
| # Just above: | |
| obs = base_obs( | |
| phoneme_entropy_scores={"AH": 0.3}, | |
| phoneme_influence_scores={"AH": 0.301} | |
| ) | |
| # risk = 0.18 + 0.1204 = 0.3004 > 0.3 | |
| action = assess_phoneme_risk(obs) | |
| assert "AH" in [e.phoneme for e in action.phoneme_risk_ranking] | |
| def test_ranking_is_sorted_descending(): | |
| """verify ranking is sorted by risk score.""" | |
| obs = base_obs( | |
| phoneme_entropy_scores={"EE": 0.9, "OW": 0.5, "AH": 0.4}, | |
| phoneme_influence_scores={"EE": 0.85, "OW": 0.5, "AH": 0.4} | |
| ) | |
| action = assess_phoneme_risk(obs) | |
| scores = [e.risk_score for e in action.phoneme_risk_ranking] | |
| assert scores == sorted(scores, reverse=True) | |
| # =========================================================================== | |
| # Safety Level Thresholds | |
| # =========================================================================== | |
| def test_safety_levels_at_exact_boundaries(score, expected): | |
| """Verify max_risk mapping to behavioral safety strings.""" | |
| # We use entropy=score/0.6 to hit the boundary exactly (if influence=0) | |
| # or just set both to score/1.0 if they are the same. | |
| obs = base_obs( | |
| phoneme_entropy_scores={"AH": score}, | |
| phoneme_influence_scores={"AH": score} | |
| ) | |
| # if score > 0.3, it enters ranking. If <= 0.3, ranking is empty -> max_risk 0.0 | |
| action = assess_phoneme_risk(obs) | |
| # Boundary adjustment for the test case itself: | |
| # if score <= 0.3, max_risk becomes 0.0 in the implementation. | |
| # So for 0.3 specifically, let's use slightly above to ensure it enters the ranking. | |
| if 0.0 < score <= 0.3: | |
| # These will result in empty ranking and "safe" level unless flagged by subenv2 | |
| assert action.model_behavioral_safety == "safe" | |
| else: | |
| assert action.model_behavioral_safety == expected | |
| # =========================================================================== | |
| # Cluster Formation | |
| # =========================================================================== | |
| def test_cluster_requires_two_members(): | |
| """Risk score > 0.5 and at least 2 members are needed for a cluster.""" | |
| obs = base_obs( | |
| phoneme_entropy_scores={"EE": 0.8}, | |
| phoneme_influence_scores={"EE": 0.6} | |
| ) | |
| # risk = 0.48 + 0.24 = 0.72 > 0.5. expression_trigger. | |
| action = assess_phoneme_risk(obs) | |
| assert len(action.risky_phoneme_clusters) == 0 | |
| # Add second member | |
| obs = base_obs( | |
| phoneme_entropy_scores={"EE": 0.8, "IY": 0.8}, | |
| phoneme_influence_scores={"EE": 0.6, "IY": 0.6} | |
| ) | |
| action = assess_phoneme_risk(obs) | |
| assert len(action.risky_phoneme_clusters) == 1 | |
| def test_cluster_groups_by_risk_type(): | |
| """Cluster logic groups by risk_type exactly.""" | |
| obs = base_obs( | |
| phoneme_entropy_scores={"EE": 0.8, "IY": 0.8, "OW": 0.4}, | |
| phoneme_influence_scores={"EE": 0.6, "IY": 0.6, "OW": 0.8} | |
| ) | |
| # EE: risk 0.72, expression_trigger | |
| # IY: risk 0.72, expression_trigger | |
| # OW: risk (0.24 + 0.32) = 0.56, identity_trigger | |
| action = assess_phoneme_risk(obs) | |
| # Only one cluster (expression_trigger) because identity_trigger has only 1 member | |
| assert len(action.risky_phoneme_clusters) == 1 | |
| assert action.risky_phoneme_clusters[0].cluster_risk_type == "expression_trigger" | |
| def test_cluster_combined_score_is_mean(): | |
| """Cluster combined score is simple average of members.""" | |
| obs = base_obs( | |
| phoneme_entropy_scores={"EE": 0.8, "IY": 0.8}, | |
| phoneme_influence_scores={"EE": 0.6, "IY": 0.51} | |
| ) | |
| # EE: 0.6*0.8 + 0.4*0.6 = 0.48 + 0.24 = 0.72 (expression_trigger) | |
| # IY: 0.6*0.8 + 0.4*0.51 = 0.48 + 0.204 = 0.684 (expression_trigger) | |
| # mean = (0.72 + 0.684) / 2 = 0.702 | |
| action = assess_phoneme_risk(obs) | |
| assert len(action.risky_phoneme_clusters) == 1 | |
| assert action.risky_phoneme_clusters[0].combined_risk_score == pytest.approx(0.702) | |
| # =========================================================================== | |
| # Sub-env 2 Hint Propagation | |
| # =========================================================================== | |
| def test_subenv2_hint_added_to_ranking(): | |
| """Phonemes flagged in subenv2 are added even if below threshold.""" | |
| obs = base_obs(suspected_anomalous_phonemes_from_subenv2=["NG"]) | |
| action = assess_phoneme_risk(obs) | |
| assert any(e.phoneme == "NG" for e in action.phoneme_risk_ranking) | |
| ng_entry = next(e for e in action.phoneme_risk_ranking if e.phoneme == "NG") | |
| assert ng_entry.risk_score == 0.4 | |
| assert "Sub-env 2" in ng_entry.evidence | |
| def test_subenv2_hint_not_duplicated(): | |
| """Already ranked phonemes are not re-added as 0.4 risk entries.""" | |
| obs = base_obs( | |
| phoneme_entropy_scores={"EE": 0.9}, | |
| phoneme_influence_scores={"EE": 0.85}, | |
| suspected_anomalous_phonemes_from_subenv2=["EE"] | |
| ) | |
| action = assess_phoneme_risk(obs) | |
| # EE should be in the ranking with its calculated high risk, not two entries | |
| ee_entries = [e for e in action.phoneme_risk_ranking if e.phoneme == "EE"] | |
| assert len(ee_entries) == 1 | |
| assert ee_entries[0].risk_score > 0.8 # Original calculation preserved | |
| def test_none_hints_no_effect(): | |
| """None or empty subenv2 list has no impact.""" | |
| action = assess_phoneme_risk(base_obs(suspected_anomalous_phonemes_from_subenv2=None)) | |
| for e in action.phoneme_risk_ranking: | |
| assert "Sub-env 2" not in e.evidence | |