tests/test_exec_voice.py
Rama master · réplica de lectura del repositorio original.
"""Exec-voice wiring smoke tests (HTTP — no browser).
The GLaDOS voice is delivered by a set of scripts injected into a page's bubble
by `_build_nav` (api/pages.py). These tests pin the WIRING — which pages load
the voice and which deliberately don't — against the live container, so a
refactor / version-bump that drops a script tag or leaks voice onto an excluded
page fails the commit. They do NOT test audio (that's the browser suite); they
assert the right `<script src>` tags are present/absent per auth + page tier.
- planning pages (/rd, /hq): full panel voice (exec-voice.js +
exec-bubble.js).
- other non-planning pages (/debug, /UI): the listener (exec-voice-listener.js
+ exec-voice.js) on the link-bubble, no panel.
- /tarot + /hosaka: link-bubble only, NO exec voice (tarot has its own reader
voice; /hosaka IS the TTS page).
Every page that loads exec voice must also load its deps — hosaka-audio.js,
voice-util.js, and the shared voice-narrator.js + voice-ui.js. exec-voice.js is
undefined without them.
"""
import pytest
PLANNING = ["/rd", "/hq"]
SPEAKING = ["/rd", "/hq"] # pages that narrate their own replies
LISTENER = ["/debug", "/UI"] # non-planning, voice via listener
EXCLUDED = ["/tarot", "/hosaka"] # link-bubble, no voice
def _body(client, path, headers):
r = client.get(path, headers=headers)
assert r.status_code == 200, f"{path} -> {r.status_code}"
return r.text
@pytest.mark.parametrize("path", PLANNING)
def test_planning_pages_load_panel_voice(client, admin_cookie, path):
html = _body(client, path, admin_cookie)
assert "/exec-voice.js" in html
assert "/exec-bubble.js" in html # the chat panel (speaks replies)
assert "/hosaka-audio.js" in html and "/voice-util.js" in html
assert "/exec-voice-listener.js" not in html # panel handles it, not the listener
@pytest.mark.parametrize("path", LISTENER)
def test_other_protected_pages_load_listener_voice(client, admin_cookie, path):
html = _body(client, path, admin_cookie)
assert "/exec-voice-listener.js" in html
assert "/exec-voice.js" in html
assert "/hosaka-audio.js" in html and "/voice-util.js" in html
assert "/exec-link.js" in html # link-bubble, not the chat panel
assert "/exec-bubble.js" not in html
@pytest.mark.parametrize("path", EXCLUDED)
def test_tarot_and_hosaka_have_no_exec_voice(client, admin_cookie, path):
html = _body(client, path, admin_cookie)
assert "/exec-voice.js" not in html
assert "/exec-voice-listener.js" not in html
assert "/exec-link.js" in html # the link-bubble still stays
def test_voice_deps_never_appear_without_exec_voice(client, admin_cookie):
"""A page loading exec-voice.js must also load its deps, and vice-versa the
listener implies the player — guards the load-order contract."""
for path in PLANNING + LISTENER:
html = _body(client, path, admin_cookie)
if "/exec-voice.js" in html:
assert "/hosaka-audio.js" in html, f"{path}: exec-voice without hosaka-audio"
assert "/voice-util.js" in html, f"{path}: exec-voice without voice-util"
# ── the shared stack ────────────────────────────────────────────────────────
@pytest.mark.parametrize("path", SPEAKING)
def test_speaking_pages_load_the_shared_narrator(client, admin_cookie, path):
"""One narrator core and one control, not a copy per surface."""
html = _body(client, path, admin_cookie)
assert "/voice-narrator.js" in html
assert "/voice-ui.js" in html
assert "/exec-voice.js" in html
@pytest.mark.parametrize("path", ["/tarot"])
def test_pages_with_an_inline_control_link_its_stylesheet(client, admin_cookie, path):
"""/tarot places the toggle in its own markup, so it links the shared
stylesheet. The PANEL injects it at runtime instead
(exec-bubble-assets.js), because the panel itself is built there."""
assert "/voice-ui.css" in _body(client, path, admin_cookie)
@pytest.mark.parametrize("path", PLANNING)
def test_the_panel_injects_the_control_stylesheet(client, admin_cookie, path):
assert "/exec-bubble-assets.js" in _body(client, path, admin_cookie)
def test_the_panel_loads_the_voice_exactly_once(client, admin_cookie):
"""A second copy re-evaluates exec-voice.js and throws `duplicate variable`,
taking the page with it. (/cc used to load its own copy; it is gone.)"""
for path in PLANNING:
html = _body(client, path, admin_cookie)
assert html.count("/exec-voice.js") == 1
assert html.count("/voice-narrator.js") == 1
def test_the_panel_speaks_and_paces_with_the_shared_engine(client, admin_cookie):
html = _body(client, "/hq", admin_cookie)
assert "/exec-stream.js" in html # the typer that picks guessed vs audio
assert "/typewriter.js" in html # twGuess + twAudio live here
def test_tarot_loads_the_shared_stack_not_execs_voice(client, admin_cookie):
"""/tarot narrates with the same core and the same control, but its own
voice: nicole on the home box, never glados."""
html = _body(client, "/tarot", admin_cookie)
assert "/voice-narrator.js" in html
assert "/voice-ui.js" in html and "/voice-ui.css" in html
assert "/tarot-voice.js" in html
assert "/exec-voice.js" not in html