Files
pony/web/tests/test_voice.py
T
alex c19c8afc6d refactor: replace pony configurator with pixel art sprites from zip
- Removed PonyConfiguratorPage, PonyAvatar, ponyCustomization
- Flow is now: Home → Theme → Pony Select → Game (no configurator step)
- Added pixel art pony sprite sheets from /tmp/Pixel Ponies.zip
- GameScenePage uses pony image directly instead of SVG avatar
- 48 frontend + 67 backend tests passing
2026-08-09 11:09:25 +00:00

145 lines
5.0 KiB
Python

"""Voice normalization, matching, and API integration tests."""
import os
import sys
import tempfile
from unittest.mock import patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.config import create_app
from app.game.voice_matcher import match_intent
from app.game.voice_normalizer import normalize_danish
from app.services import persistence
from app.services.speech_to_text import LocalFasterWhisperProvider, get_speech_to_text_provider
INTENTS = [
{"id": "forest", "keywords": ["skov", "skoven", "træer"], "synonyms": ["mellem træerne"]},
{"id": "garden", "keywords": ["have", "haven", "blomster"]},
]
def make_client():
persistence.DB_PATH = tempfile.mktemp()
app = create_app()
app.config["TESTING"] = True
return app.test_client()
def test_normalizer_preserves_danish_letters():
assert normalize_danish(" SKOVEN!! ÆØÅ ") == "skoven æøå"
def test_local_stt_provider_can_be_selected(monkeypatch):
monkeypatch.setenv("STT_PROVIDER", "local")
monkeypatch.setenv("STT_MODEL", "test-model")
provider = get_speech_to_text_provider()
assert isinstance(provider, LocalFasterWhisperProvider)
assert provider.model_name == "test-model"
def test_matches_whole_word_in_child_sentence():
result = match_intent("Jeg tror den er ved træer!", INTENTS)
assert result.matched
assert result.intent == "forest"
assert result.method == "keyword"
def test_does_not_use_naive_substring_matching():
result = match_intent("Vi skal behøve noget", [{"id": "sea", "keywords": ["sø"]}])
assert not result.matched
def test_low_fuzzy_match_requests_clarification():
result = match_intent("skåven", INTENTS)
assert not result.matched
assert result.response_type == "clarify"
def test_conflicting_intents_are_not_guessed():
result = match_intent("skoven og haven", INTENTS)
assert not result.matched
assert result.response_type == "clarify"
def test_text_endpoint_can_progress_the_current_scene():
client = make_client()
started = client.post("/api/start", json={"type": 0, "tema": 0}).get_json()
response = client.post(
f"/api/v1/games/{started['gameId']}/voice/text",
json={
"scene_id": "0",
"question_id": started["voice"]["question"]["id"],
"text": "Ja, jeg er klar!",
},
)
assert response.status_code == 200
data = response.get_json()["data"]
assert data["matched"]
assert data["intent"] == "ready"
assert data["next_action"]["choice_id"] == "roll_scene"
assert len(data["gameState"]["history"]) == 1
def test_text_endpoint_rejects_a_stale_scene():
client = make_client()
started = client.post("/api/start", json={"type": 0, "tema": 0}).get_json()
response = client.post(
f"/api/v1/games/{started['gameId']}/voice/text",
json={"scene_id": "4", "text": "ja"},
)
assert response.status_code == 409
assert response.get_json()["error"] == "stale_scene"
def test_no_match_never_blocks_the_dice_fallback():
client = make_client()
started = client.post("/api/start", json={"type": 0, "tema": 0}).get_json()
response = client.post(
f"/api/v1/games/{started['gameId']}/voice/text",
json={"scene_id": "0", "text": "en kæmpe lilla drage"},
)
assert response.status_code == 200
assert not response.get_json()["data"]["matched"]
assert client.post("/api/kast").status_code == 200
@patch("app.routes.api.classify_with_hermes")
def test_hermes_can_answer_about_scene_without_changing_game(hermes):
hermes.return_value = {
"scope": "game", "action": "answer", "choice_id": None, "intent": None,
"confidence": 0.93, "reply": "Angel er Fluttershys lille hvide kanin.",
"method": "hermes",
}
client = make_client()
started = client.post("/api/start", json={"type": 0, "tema": 1}).get_json()
response = client.post(
f"/api/v1/games/{started['gameId']}/voice/text",
json={"scene_id": "0", "text": "Hvem er Angel?"},
)
data = response.get_json()["data"]
assert data["matched"]
assert data["match_method"] == "hermes"
assert data["next_action"] is None
assert client.get("/api/scene").get_json()["history"] == []
@patch("app.routes.api.classify_with_hermes")
def test_hermes_story_comment_returns_a_spoken_response_without_rolling(hermes):
hermes.return_value = {
"scope": "game", "action": "answer", "choice_id": None, "intent": None,
"confidence": 0.91, "reply": "Ja, de røde æbler ser lækre og sprøde ud!",
"method": "hermes",
}
client = make_client()
started = client.post("/api/start", json={"type": 0, "tema": 2}).get_json()
response = client.post(
f"/api/v1/games/{started['gameId']}/voice/text",
json={"scene_id": "0", "text": "De æbler ser lækre ud"},
)
data = response.get_json()["data"]
assert data["child_response"] == "Ja, de røde æbler ser lækre og sprøde ud!"
assert data["gameState"] is None
assert client.get("/api/scene").get_json()["history"] == []