CI / test (push) Failing after 39s
Hjernen (party/brain) skriver kommentarer til løgnene, dømmer løgne (sandheden eller upassende), personlige drillerier og en slut-anmeldelse, alt i baggrunden med faste replikker som fallback. Udbyderen vælges med BRAIN_PROVIDER: anthropic (Claude), openai (OpenAI-kompatibel, fx Codex) eller ollama (fx Gemma). Rummets mikrofon er eksperimentel: storskærmen sender korte lydstumper til talegenkendelse (STT_PROVIDER), og kun et kort resumé af latter og udsagn huskes til hjernen. Musikken er nu lavet med en musikmodel. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
126 lines
5.8 KiB
Python
126 lines
5.8 KiB
Python
import asyncio
|
|
from unittest import mock
|
|
|
|
from asgiref.sync import sync_to_async
|
|
from channels.testing.websocket import WebsocketCommunicator
|
|
from django.test import SimpleTestCase, TransactionTestCase, override_settings
|
|
|
|
from party import rooms, stt, tts
|
|
from party.brain import GameBrain
|
|
from party.brain.providers import OllamaProvider, OpenAICompatibleProvider, build_provider
|
|
from partyhub.asgi import application
|
|
|
|
from .test_narration import fake_synth
|
|
from .test_ws import HOST_HEADERS, make_bank, recv_until
|
|
|
|
|
|
class FakeProvider:
|
|
"""Svarer som en sprogmodel ville, ud fra hvilket skema der bedes om."""
|
|
name = "fake"
|
|
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
async def complete_json(self, system, user, schema, **kw):
|
|
self.calls.append(user)
|
|
props = schema["properties"]
|
|
if "is_truth" in props:
|
|
bad = '"løgn": "grim løgn"' in user
|
|
return {"is_truth": False, "inappropriate": bad, "message": "Prøv en pænere løgn." if bad else ""}
|
|
if "comments" in props:
|
|
import json, re
|
|
data = json.loads(user.split("Data (JSON):\n", 1)[1])
|
|
return {"comments": [{"id": a["id"], "line": f"KOMMENTAR {a['text']}"} for a in data["svar"]]}
|
|
if "teases" in props:
|
|
return {"teases": []}
|
|
if "lines" in props:
|
|
return {"lines": []}
|
|
return None
|
|
|
|
|
|
class ProviderFactoryTests(SimpleTestCase):
|
|
def test_build(self):
|
|
self.assertIsNone(build_provider("", "x"))
|
|
self.assertIsNone(build_provider("ollama", ""))
|
|
self.assertIsInstance(build_provider("ollama", "gemma"), OllamaProvider)
|
|
self.assertIsInstance(build_provider("openai", "gpt"), OpenAICompatibleProvider)
|
|
|
|
def test_timeout_and_errors_return_none(self):
|
|
class Slow(OllamaProvider):
|
|
async def _complete(self, *a):
|
|
await asyncio.sleep(5)
|
|
|
|
class Broken(OllamaProvider):
|
|
async def _complete(self, *a):
|
|
raise RuntimeError("nede")
|
|
self.assertIsNone(asyncio.run(Slow("m").complete_json("s", "u", {}, timeout=0.05)))
|
|
self.assertIsNone(asyncio.run(Broken("m").complete_json("s", "u", {})))
|
|
|
|
|
|
@override_settings(TRUSTED_PROXIES=["127.0.0.1"], DEV_HOST_USER=None)
|
|
class BrainInGameTests(TransactionTestCase):
|
|
def setUp(self):
|
|
rooms.all_rooms().clear()
|
|
make_bank()
|
|
from fupogfakta import engine as _engine
|
|
self._intro, _engine.INTRO_SECONDS = _engine.INTRO_SECONDS, 0.2
|
|
self.addCleanup(setattr, _engine, "INTRO_SECONDS", self._intro)
|
|
self.fake = FakeProvider()
|
|
|
|
async def _room(self):
|
|
from fupogfakta.models import load_game_questions
|
|
with mock.patch.object(rooms, "get_brain", return_value=GameBrain(self.fake)):
|
|
return rooms.create_room(await sync_to_async(load_game_questions)(), "agw", "http://testserver")
|
|
|
|
async def test_judge_rejects_lie_and_comments_reach_reveal(self):
|
|
room = await self._room()
|
|
with mock.patch.object(tts, "enabled", return_value=True), mock.patch.object(tts, "synth", fake_synth):
|
|
host = WebsocketCommunicator(application, f"/ws/host/{room.code}/", headers=HOST_HEADERS)
|
|
host.scope["client"] = ("127.0.0.1", 5000)
|
|
await host.connect()
|
|
phones = []
|
|
for name in ["Asger", "Sus"]:
|
|
c = WebsocketCommunicator(application, f"/ws/play/{room.code}/")
|
|
await c.connect()
|
|
await c.send_json_to({"action": "join", "name": name})
|
|
await recv_until(c, "joined")
|
|
phones.append(c)
|
|
await phones[0].send_json_to({"action": "start"})
|
|
st = (await recv_until(host, "state", "category"))["state"]
|
|
chooser = phones[0] if st["category"]["chooser_id"] == "p1" else phones[1]
|
|
await chooser.send_json_to({"action": "choose_category", "category_id": st["category"]["options"][0]["id"]})
|
|
for c in phones:
|
|
await recv_until(c, "state", "lie")
|
|
await phones[0].send_json_to({"action": "submit_lie", "text": "grim løgn"})
|
|
err = await recv_until(phones[0], "error")
|
|
self.assertEqual(err["message"], "Prøv en pænere løgn.")
|
|
self.assertNotIn("p1", room.game.lies) # afvist og fjernet
|
|
await phones[0].send_json_to({"action": "submit_lie", "text": "pæn løgn"})
|
|
await phones[1].send_json_to({"action": "submit_lie", "text": "anden løgn"})
|
|
for c in phones:
|
|
mine = (await recv_until(c, "state", "guess"))["state"]
|
|
await c.send_json_to({"action": "guess", "answer_id": mine["answers"][0]["id"]})
|
|
for _ in range(40):
|
|
st = (await recv_until(host, "state", "reveal"))["state"]
|
|
if st["reveal"]["started_at"]:
|
|
break
|
|
texts = [s["audio"]["text"] for s in st["reveal"]["steps"] if s.get("audio")]
|
|
self.assertTrue(any("KOMMENTAR" in t for t in texts), texts) # hjernens kommentar blev læst op
|
|
self.assertTrue(room.memory) # afsløringen blev husket
|
|
for c in phones:
|
|
await c.disconnect()
|
|
await host.disconnect()
|
|
|
|
async def test_mic_feeds_memory(self):
|
|
room = await self._room()
|
|
|
|
async def fake_transcribe(audio, ctype):
|
|
return {"text": "det er helt sikkert ugler", "laughter": True}
|
|
with mock.patch.object(stt, "transcribe", fake_transcribe):
|
|
res = await room.hear(b"x" * 5000, "audio/webm")
|
|
self.assertTrue(res["laughter"])
|
|
self.assertEqual(room.mic_laughs, 1)
|
|
self.assertTrue(any("grinede" in m for m in room.memory))
|
|
self.assertTrue(any("helt sikkert ugler" in m for m in room.memory))
|
|
self.assertTrue(room.mic_on())
|