File size: 5,670 Bytes
5743bc2
 
 
 
 
 
 
52cef2a
5743bc2
 
52cef2a
 
5743bc2
 
f18b8fb
5743bc2
f18b8fb
 
52cef2a
 
5743bc2
 
 
 
52cef2a
 
f18b8fb
52cef2a
f18b8fb
52cef2a
 
 
 
 
 
 
f18b8fb
 
52cef2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f18b8fb
52cef2a
 
 
 
 
 
5743bc2
 
f18b8fb
5743bc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52cef2a
 
 
 
 
 
f18b8fb
52cef2a
 
 
 
 
 
 
f18b8fb
52cef2a
 
 
 
 
 
 
 
 
f18b8fb
52cef2a
 
 
 
 
 
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
from guardian_openenv.environment import GuardianReviewEnvironment
from guardian_openenv.models import (
    ActionType,
    GuardianAction,
    PatternLabel,
    RecommendationDecision,
)
from guardian_openenv.tasks import TASKS, TASKS_BY_ID


# ── Basic API contract ────────────────────────────────────────────────────

def test_reset_returns_expected_task():
    env = GuardianReviewEnvironment()
    observation = env.reset("value_hotel_budget_guard")

    task = TASKS_BY_ID["value_hotel_budget_guard"]
    assert observation.task_id == "value_hotel_budget_guard"
    # remaining_steps equals the task's max_steps right after reset
    assert observation.remaining_steps == task.max_steps
    assert observation.current_decision.flagged_patterns == []
    assert observation.shopper_context.budget == 200.0


def test_state_reflects_task_after_reset():
    env = GuardianReviewEnvironment()
    env.reset("airline_seat_upsell_gauntlet")
    state = env.state()
    assert state.task_id == "airline_seat_upsell_gauntlet"
    assert state.difficulty == "medium"
    assert state.step_count == 0
    assert state.done is False


def test_step_increases_step_count():
    env = GuardianReviewEnvironment()
    obs = env.reset("value_hotel_budget_guard")
    task = TASKS_BY_ID["value_hotel_budget_guard"]

    result = env.step(GuardianAction(action_type=ActionType.INSPECT_SECTION, section_id=task.sections[0].section_id))
    assert env.state().step_count == 1
    assert result.reward.score > 0.0
    assert result.reward.score < 1.0


def test_reward_score_always_in_open_range():
    """Reward scores must be strictly > 0 and < 1 at every step."""
    env = GuardianReviewEnvironment()
    for task in TASKS:
        obs = env.reset(task.task_id)
        for section in task.sections:
            if obs.done:
                break
            result = env.step(GuardianAction(action_type=ActionType.INSPECT_SECTION, section_id=section.section_id))
            assert 0.0 < result.reward.score < 1.0, (
                f"[{task.task_id}] score out of range: {result.reward.score}"
            )


def test_invalid_section_penalised():
    env = GuardianReviewEnvironment()
    env.reset("value_hotel_budget_guard")
    result = env.step(GuardianAction(action_type=ActionType.INSPECT_SECTION, section_id="NONEXISTENT"))
    assert result.reward.value < 0.0


# ── Full perfect episode β€” easy task ──────────────────────────────────────

def test_dense_progress_and_final_grade():
    env = GuardianReviewEnvironment()
    env.reset("value_hotel_budget_guard")

    env.step(GuardianAction(action_type=ActionType.INSPECT_SECTION, section_id="listing-banner"))
    env.step(GuardianAction(action_type=ActionType.INSPECT_SECTION, section_id="price-summary"))
    env.step(GuardianAction(action_type=ActionType.INSPECT_SECTION, section_id="addons"))
    env.step(GuardianAction(action_type=ActionType.INSPECT_SECTION, section_id="timer-proof"))
    env.step(GuardianAction(action_type=ActionType.FLAG_PATTERN, pattern=PatternLabel.FALSE_URGENCY))
    env.step(GuardianAction(action_type=ActionType.FLAG_PATTERN, pattern=PatternLabel.FALSE_SCARCITY))
    env.step(GuardianAction(action_type=ActionType.FLAG_PATTERN, pattern=PatternLabel.HIDDEN_FEES))
    env.step(GuardianAction(action_type=ActionType.FLAG_PATTERN, pattern=PatternLabel.PRECHECKED_ADDONS))
    env.step(GuardianAction(action_type=ActionType.REMOVE_ADDON, addon_id="insurance"))
    env.step(GuardianAction(action_type=ActionType.REMOVE_ADDON, addon_id="newsletter"))
    env.step(GuardianAction(action_type=ActionType.VERIFY_TIMER, timer_id="timer-checkout", timer_is_fake=True))
    env.step(GuardianAction(action_type=ActionType.SET_TRUE_TOTAL, estimated_true_total=212.39))
    env.step(GuardianAction(action_type=ActionType.SET_RECOMMENDATION, recommendation=RecommendationDecision.AVOID))
    env.step(
        GuardianAction(
            action_type=ActionType.WRITE_SUMMARY,
            summary="Hidden fees, a fake timer, and pre-checked add-ons push the stay over budget.",
        )
    )
    result = env.step(GuardianAction(action_type=ActionType.SUBMIT_DECISION))

    assert result.done is True
    assert result.info["final_score"] >= 0.95


# ── Minimum smoke test β€” medium and hard tasks ────────────────────────────

def test_medium_task_completes():
    env = GuardianReviewEnvironment()
    env.reset("airline_seat_upsell_gauntlet")
    result = env.step(GuardianAction(action_type=ActionType.SUBMIT_DECISION))
    assert result.done is True
    assert 0.0 < result.reward.score < 1.0


def test_hard_task_completes():
    env = GuardianReviewEnvironment()
    env.reset("marketplace_ghost_checkout")
    result = env.step(GuardianAction(action_type=ActionType.SUBMIT_DECISION))
    assert result.done is True
    assert 0.0 < result.reward.score < 1.0


# ── Post-done behaviour ───────────────────────────────────────────────────

def test_actions_after_done_return_reward_zero():
    env = GuardianReviewEnvironment()
    env.reset("value_hotel_budget_guard")
    env.step(GuardianAction(action_type=ActionType.SUBMIT_DECISION))
    # Further steps should be no-ops with value=0
    noop = env.step(GuardianAction(action_type=ActionType.SUBMIT_DECISION))
    assert noop.done is True
    assert noop.reward.value == 0.0