File size: 8,801 Bytes
ebae6ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e6b6793
ebae6ab
 
 
 
 
e6b6793
 
ebae6ab
e6b6793
 
 
 
 
 
 
 
ebae6ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""
Tests for Phase 4 — Critic Escalation Engine.

Run: pytest viral_script_engine/tests/test_escalation.py -v
"""
import json
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

BASE_DIR = Path(__file__).parent.parent


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def tmp_tracker(tmp_path):
    from viral_script_engine.escalation.difficulty_tracker import DifficultyTracker
    return DifficultyTracker(persistence_path=str(tmp_path / "tracker.json"))


@pytest.fixture
def dummy_challenge():
    from viral_script_engine.escalation.critic_escalation_engine import EscalatedChallenge
    return EscalatedChallenge(
        source_class="hook_weakness",
        script_text="This script has a subtle hook problem buried under misdirection.",
        region="Mumbai Gen Z",
        platform="Reels",
        dominant_flaw="hook_weakness",
        conflicting_flaw="pacing_issue",
        why_its_harder="Fixing hook early destroys pacing and lowers total reward.",
        optimal_action_order=["pacing_fix", "hook_rewrite"],
        trap_action="hook_rewrite",
    )


# ---------------------------------------------------------------------------
# Test 1: record_episode tracks consecutive resolutions correctly
# ---------------------------------------------------------------------------

def test_record_episode_tracks_consecutive(tmp_tracker):
    """Consecutive resolutions increment; a failure resets to 0."""
    tmp_tracker.record_episode("hook_weakness", 0.85, "ep1")
    assert tmp_tracker.records["hook_weakness"].consecutive_resolutions == 1

    tmp_tracker.record_episode("hook_weakness", 0.90, "ep2")
    assert tmp_tracker.records["hook_weakness"].consecutive_resolutions == 2

    tmp_tracker.record_episode("hook_weakness", 0.50, "ep3")
    assert tmp_tracker.records["hook_weakness"].consecutive_resolutions == 0


# ---------------------------------------------------------------------------
# Test 2: mastery triggers at exactly 3 consecutive resolutions, not 2
# ---------------------------------------------------------------------------

def test_mastery_triggers_at_3_not_2(tmp_tracker):
    """Mastery is set when consecutive_resolutions == mastery_threshold (3)."""
    tmp_tracker.record_episode("hook_weakness", 0.85, "ep1")
    assert not tmp_tracker.records["hook_weakness"].is_mastered

    tmp_tracker.record_episode("hook_weakness", 0.85, "ep2")
    assert not tmp_tracker.records["hook_weakness"].is_mastered

    tmp_tracker.record_episode("hook_weakness", 0.85, "ep3")
    assert tmp_tracker.records["hook_weakness"].is_mastered
    assert "hook_weakness" in tmp_tracker.get_mastered_classes()


# ---------------------------------------------------------------------------
# Test 3: mastery resets if agent fails after mastery achieved
# ---------------------------------------------------------------------------

def test_mastery_resets_on_failure(tmp_tracker):
    """A failure (r4 < 0.8) after mastery clears is_mastered."""
    for i in range(3):
        tmp_tracker.record_episode("hook_weakness", 0.9, f"ep{i}")
    assert tmp_tracker.records["hook_weakness"].is_mastered

    tmp_tracker.record_episode("hook_weakness", 0.3, "ep_fail")
    assert not tmp_tracker.records["hook_weakness"].is_mastered
    assert tmp_tracker.records["hook_weakness"].consecutive_resolutions == 0


# ---------------------------------------------------------------------------
# Test 4: CriticEscalationEngine.escalate() returns valid EscalatedChallenge
# ---------------------------------------------------------------------------

def test_escalation_engine_returns_valid_challenge():
    """escalate() returns an EscalatedChallenge with all required fields when LLM is mocked."""
    from viral_script_engine.escalation.critic_escalation_engine import CriticEscalationEngine

    mock_response = json.dumps({
        "script_text": "Today I'll teach you the one thing schools never told you about money.",
        "dominant_flaw": "hook_weakness",
        "conflicting_flaw": "pacing_issue",
        "why_its_harder": "Hook fix accelerates pacing and destroys retention.",
        "optimal_action_order": ["pacing_fix", "hook_rewrite"],
        "trap_action": "hook_rewrite",
    })

    engine = CriticEscalationEngine.__new__(CriticEscalationEngine)
    engine.escalated_classes = {}
    engine.llm = MagicMock()
    engine.llm.generate.return_value = mock_response

    challenge = engine.escalate(
        mastered_class="hook_weakness",
        original_script_example="Old script text here.",
        region="Mumbai Gen Z",
        platform="Reels",
    )

    assert challenge.source_class == "hook_weakness"
    assert challenge.script_text
    assert challenge.dominant_flaw == "hook_weakness"
    assert challenge.conflicting_flaw == "pacing_issue"
    assert challenge.difficulty_level == "self_generated"
    assert challenge.generated_at
    assert isinstance(challenge.optimal_action_order, list)
    assert challenge.trap_action


# ---------------------------------------------------------------------------
# Test 5: env.reset() uses escalated script when mastery is achieved
# ---------------------------------------------------------------------------

def test_env_reset_uses_escalated_script_on_mastery(tmp_path, dummy_challenge, monkeypatch):
    """When a class is mastered, env.reset() uses the escalated challenge script."""
    from viral_script_engine.environment.env import ViralScriptEnv
    from viral_script_engine.escalation.difficulty_tracker import DifficultyTracker
    from viral_script_engine.escalation.critic_escalation_engine import CriticEscalationEngine
    from viral_script_engine.rewards import r2_coherence, r5_defender_preservation
    from viral_script_engine.rewards.r2_coherence import CoherenceRewardResult
    from viral_script_engine.rewards.r5_defender_preservation import DefenderPreservationResult

    monkeypatch.setattr(
        r2_coherence.CoherenceReward, "score",
        lambda self, a, b: CoherenceRewardResult(score=0.75, raw_similarity=0.85, interpretation="good_coherence"),
    )
    monkeypatch.setattr(
        r5_defender_preservation.DefenderPreservationReward, "score",
        lambda self, d, s: DefenderPreservationResult(score=0.70, max_similarity=0.80, best_matching_sentence="[mock]"),
    )

    tracker = DifficultyTracker(persistence_path=str(tmp_path / "tracker.json"))
    for i in range(3):
        tracker.record_episode("hook_weakness", 0.9, f"ep{i}")
    assert tracker.records["hook_weakness"].is_mastered

    mock_engine = MagicMock(spec=CriticEscalationEngine)
    mock_engine.get_next_challenge.return_value = dummy_challenge

    env = ViralScriptEnv(
        scripts_path=str(BASE_DIR / "data" / "test_scripts" / "scripts.json"),
        cultural_kb_path=str(BASE_DIR / "data" / "cultural_kb.json"),
        max_steps=2,
        difficulty="easy",
        use_escalation=True,
        difficulty_tracker=tracker,
        escalation_engine=mock_engine,
    )

    obs, info = env.reset()

    assert info.get("escalation_used") is True
    assert obs["current_script"] == dummy_challenge.script_text
    assert obs["difficulty_level"] == "self_generated"


# ---------------------------------------------------------------------------
# Test 6: difficulty progression JSON is saved correctly
# ---------------------------------------------------------------------------

def test_progression_json_saved(tmp_path, tmp_tracker):
    """Progression JSON written by run_escalation_demo matches expected schema."""
    from viral_script_engine.escalation.critic_escalation_engine import CriticEscalationEngine

    engine = CriticEscalationEngine.__new__(CriticEscalationEngine)
    engine.escalated_classes = {}
    engine.llm = MagicMock()

    fake_episodes = [
        {"episode_num": i, "difficulty_level": "easy", "escalation_used": False,
         "total_reward": 0.5, "r4_score": 0.4, "steps": [], "tracker_summary": {}}
        for i in range(1, 6)
    ]

    from viral_script_engine.scripts.run_escalation_demo import _build_progression_report
    progression = _build_progression_report(fake_episodes, tmp_tracker, engine)

    out_path = tmp_path / "escalation_progression.json"
    with open(out_path, "w") as f:
        json.dump({"episodes": fake_episodes, "progression": progression}, f, indent=2)

    assert out_path.exists()
    with open(out_path) as f:
        loaded = json.load(f)

    assert "episodes" in loaded
    assert "progression" in loaded
    assert "mastery_events" in loaded["progression"]
    assert "total_escalated_challenges" in loaded["progression"]