| """HUD rendering - pure functions, no Gradio runtime or model needed.""" |
|
|
| import pytest |
|
|
| pytest.importorskip("gradio") |
|
|
| from jailbreak_dojo.levels import load_levels |
| from jailbreak_dojo.ui import ( |
| FORGE_PRESETS, |
| _meter, |
| _parse_forge_query, |
| _share_link, |
| _shields_panel, |
| _trail, |
| make_custom_level, |
| render_hud, |
| shield_card, |
| ) |
|
|
| LEVELS = load_levels() |
|
|
|
|
| def test_trail_marks_done_current_and_locked(): |
| html = _trail(LEVELS, current_id=3, won_levels=[1, 2]) |
| assert html.count("node done") == 2 |
| assert html.count("node current") == 1 |
| assert html.count("node locked") == 2 |
|
|
|
|
| def test_shields_panel_shows_open_gate_at_l1(): |
| assert "gate stands open" in _shields_panel(LEVELS[0]) |
|
|
|
|
| def test_shields_panel_lights_all_five_at_l5(): |
| |
| assert _shields_panel(LEVELS[4]).count("chip on") == 5 |
|
|
|
|
| def test_shields_panel_is_honest_when_ml_did_not_load(): |
| assert "degraded to regex" in _shields_panel(LEVELS[4], ml_ok=False) |
|
|
|
|
| def test_meter_flips_to_over_budget_past_1000(): |
| assert 'class="fill"' in _meter(300) |
| assert "fill over" in _meter(1200) |
|
|
|
|
| def test_render_hud_includes_level_palette_and_panels(): |
| html = render_hud(LEVELS, LEVELS[4], won_levels=[1, 2, 3, 4], tokens=1180) |
| assert "level-5" in html and 'class="trail"' in html |
| assert 'class="meter"' in html and 'class="shields"' in html |
|
|
|
|
| def test_shield_card_names_the_ml_sentinel(): |
| class _S: |
| stage, subcategory, reason = "model", "span_model", "looks like an injection" |
|
|
| assert "Sentinel (ML)" in shield_card(_S()) |
|
|
|
|
| def test_shield_card_escapes_html_in_evidence(): |
| class _S: |
| stage, subcategory, reason = "regex", "<script>", '<img src=x onerror=alert(1)>' |
|
|
| html_out = shield_card(_S()) |
| assert "<script>" not in html_out and "<img src=x" not in html_out |
| assert "<script>" in html_out |
|
|
|
|
| def test_message_too_long_rejects(): |
| from jailbreak_dojo.ui import MAX_MESSAGE_LEN, _message_too_long |
|
|
| assert _message_too_long("x" * MAX_MESSAGE_LEN) is None |
| assert _message_too_long("x" * (MAX_MESSAGE_LEN + 1)) is not None |
|
|
|
|
| def test_forge_presets_map_to_escalating_defenses(): |
| pushover = make_custom_level("A", "p", "sk-", "Pushover - no shields") |
| fortress = make_custom_level("B", "p", "sk-", "Fortress - every Unplug shield") |
| assert not (pushover.input_scan or pushover.guardrails or pushover.input_ml) |
| assert fortress.input_scan and fortress.guardrails and fortress.output_san and fortress.input_ml |
|
|
|
|
| def test_forge_inputs_are_html_escaped_no_xss(): |
| from jailbreak_dojo.ui import _portrait, render_custom_hud |
|
|
| lvl = make_custom_level('<img src=x onerror=alert(1)>', "persona", '"><svg>', list(FORGE_PRESETS)[0]) |
| html_out = render_custom_hud(lvl, 0) + _portrait(lvl) |
| assert "<img src=x onerror" not in html_out |
| assert "<img" in html_out |
| assert "<svg>" not in html_out and "<svg>" in html_out |
|
|
|
|
| def test_forge_share_link_round_trips_config_but_not_secret(): |
| class _Req: |
| headers = {"host": "x.hf.space", "x-forwarded-proto": "https"} |
|
|
| link = _share_link(_Req(), "Mossbeard", "You are Mossbeard.", "tk-", list(FORGE_PRESETS)[1]) |
| assert "Mossbeard" in link and "sk-" not in link |
|
|
| from urllib.parse import parse_qs, urlparse |
|
|
| q = parse_qs(urlparse(link).query) |
|
|
| class _Req2: |
| query_params = {k: v[0] for k, v in q.items()} |
|
|
| name, persona, prefix, diff = _parse_forge_query(_Req2()) |
| assert name == "Mossbeard" and prefix == "tk-" and diff == list(FORGE_PRESETS)[1] |
|
|