File size: 6,583 Bytes
5ccdeee
639b641
 
 
 
5ccdeee
639b641
5ccdeee
639b641
fea7356
639b641
 
 
5ccdeee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fea7356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7414383
 
 
 
 
 
 
 
 
 
 
639b641
7414383
 
 
 
5a386b9
 
 
 
639b641
 
5a386b9
 
 
 
 
 
639b641
 
 
 
 
 
 
 
5a386b9
639b641
 
 
 
5a386b9
 
639b641
5a386b9
 
639b641
 
 
 
 
 
5a386b9
 
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
import json
import os
import subprocess
import sys

import pytest
from unittest.mock import patch

from viral_script_engine.agents.critic import CritiqueClaim, CritiqueOutput, CriticAgent, CriticParseError
from viral_script_engine.evaluation.critic_evaluator import CriticEvaluator, EvaluationResult
from tests.conftest import MOCK_VALID_RESPONSE

MOCK_INVALID_RESPONSE = "Here is my feedback: The hook is weak and the CTA is missing."


# ── Task 2: Model parsing tests ───────────────────────────────────────────────

def test_critique_claim_parses_valid_json():
    data = {
        "claim_id": "C1",
        "critique_class": "hook_weakness",
        "claim_text": "The hook is too slow.",
        "timestamp_range": "0:00-0:03",
        "evidence": "Let me tell you a secret",
        "is_falsifiable": True,
        "severity": "high",
    }
    claim = CritiqueClaim(**data)
    assert claim.claim_id == "C1"
    assert claim.severity == "high"


def test_critique_output_parses_valid_json():
    data = {
        "claims": [
            {
                "claim_id": "C1",
                "critique_class": "hook_weakness",
                "claim_text": "Weak hook.",
                "timestamp_range": "0:00-0:03",
                "evidence": "Let me tell you",
                "is_falsifiable": True,
                "severity": "high",
            }
        ],
        "overall_severity": "high",
        "raw_response": '{"claims": []}',
    }
    output = CritiqueOutput(**data)
    assert len(output.claims) == 1


# ── Task 4: CriticEvaluator tests ────────────────────────────────────────────

SCRIPT_TEXT = "Let me tell you a secret about money. First, save twenty percent. Second, invest in index funds. Finally, avoid lifestyle inflation. That is all."


def _make_output(claims_data, overall="medium"):
    claims = [CritiqueClaim(**c) for c in claims_data]
    return CritiqueOutput(claims=claims, overall_severity=overall, raw_response="{}")


def test_evaluator_passes_good_critique():
    output = _make_output([
        {"claim_id": "C1", "critique_class": "hook_weakness", "claim_text": "x", "timestamp_range": "0:00-0:03", "evidence": "Let me tell you a secret", "is_falsifiable": True, "severity": "high"},
        {"claim_id": "C2", "critique_class": "cta_buried", "claim_text": "x", "timestamp_range": "0:10-0:15", "evidence": "That is all", "is_falsifiable": True, "severity": "medium"},
        {"claim_id": "C3", "critique_class": "pacing_issue", "claim_text": "x", "timestamp_range": "N/A", "evidence": "save twenty percent", "is_falsifiable": True, "severity": "low"},
    ])
    evaluator = CriticEvaluator()
    result = evaluator.evaluate(output, SCRIPT_TEXT)
    assert result.passes_gate is True
    assert result.claim_count == 3
    assert result.specificity_score >= 0.6
    assert result.falsifiability_score >= 0.7


def test_evaluator_fails_too_few_claims():
    output = _make_output([
        {"claim_id": "C1", "critique_class": "hook_weakness", "claim_text": "x", "timestamp_range": "0:00-0:03", "evidence": "Let me tell you", "is_falsifiable": True, "severity": "high"},
        {"claim_id": "C2", "critique_class": "cta_buried", "claim_text": "x", "timestamp_range": "0:10", "evidence": "invest", "is_falsifiable": True, "severity": "medium"},
    ])
    evaluator = CriticEvaluator()
    result = evaluator.evaluate(output, SCRIPT_TEXT)
    assert result.passes_gate is False
    assert result.claim_count == 2


def test_evaluator_fails_low_specificity():
    output = _make_output([
        {"claim_id": "C1", "critique_class": "hook_weakness", "claim_text": "x", "timestamp_range": "0:00-0:03", "evidence": "this quote is not in the script", "is_falsifiable": True, "severity": "high"},
        {"claim_id": "C2", "critique_class": "cta_buried", "claim_text": "x", "timestamp_range": "0:10", "evidence": "also not in script", "is_falsifiable": False, "severity": "medium"},
        {"claim_id": "C3", "critique_class": "pacing_issue", "claim_text": "x", "timestamp_range": "N/A", "evidence": "still not in script", "is_falsifiable": False, "severity": "low"},
    ])
    evaluator = CriticEvaluator()
    result = evaluator.evaluate(output, SCRIPT_TEXT)
    assert result.passes_gate is False


# ── Task 5: CLI exit-code test ────────────────────────────────────────────────

def test_cli_dry_run_exits_zero_or_one():
    """CLI must exit 0 (pass) or 1 (fail) β€” never crash with unhandled exception."""
    result = subprocess.run(
        [sys.executable, "scripts/run_critic_gate.py", "--dry-run"],
        capture_output=True,
        text=True,
        cwd=str(__import__("pathlib").Path(__file__).parent.parent),
        env={**os.environ},
    )
    assert result.returncode in (0, 1), (
        f"Unexpected exit code: {result.returncode}\nSTDERR: {result.stderr}"
    )


# ── Task 6: Mocked CriticAgent tests ─────────────────────────────────────────

def test_critic_agent_returns_critique_output(mock_llm):
    agent = CriticAgent(backend="qwen")
    result = agent.critique("Some script text here", "Mumbai", "Reels", "finance")
    assert len(result.claims) == 3
    assert result.claims[0].claim_id == "C1"
    assert result.overall_severity == "high"


def test_critic_agent_retries_on_bad_json(monkeypatch):
    calls = []

    def fake_generate(self, sys_prompt, usr_prompt, **kw):
        calls.append(usr_prompt)
        if len(calls) == 1:
            return MOCK_INVALID_RESPONSE
        return MOCK_VALID_RESPONSE

    monkeypatch.setattr(
        "viral_script_engine.agents.llm_backend.LLMBackend.generate", fake_generate
    )
    agent = CriticAgent(backend="qwen")
    result = agent.critique("Some script text here", "Mumbai", "Reels", "finance")
    assert len(result.claims) == 3
    assert len(calls) == 2


def test_critic_agent_raises_parse_error_after_two_failures(monkeypatch):
    monkeypatch.setattr(
        "viral_script_engine.agents.llm_backend.LLMBackend.generate",
        lambda self, sys_prompt, usr_prompt, **kw: MOCK_INVALID_RESPONSE,
    )
    agent = CriticAgent(backend="qwen")
    with pytest.raises(CriticParseError):
        agent.critique("Some script text here", "Mumbai", "Reels", "finance")