lct-hack/backend/tests/test_tree.py
2026-09-20 10:05:43 +03:00

146 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Офлайн-звонящий по предгенерированной таблице. Сети не требует."""
from pathlib import Path
import pytest
import yaml
from app.dialog.persona import PersonaState
from app.dialog.slots import SlotMachine
from app.domain.events import Mood
from tests.test_slots import SCENARIO, StemEmbedder
TABLE = {
"scenario": SCENARIO.id,
"first_line": "Алло! Горим!",
"reveal": {
"f_address": {"panic": "Ленина четырнадцать, сорок седьмая квартира! Быстрее!",
"aggressive": "ЛЕНИНА ЧЕТЫРНАДЦАТЬ! Записали?!"},
"f_people": {"panic": "Жена с ребёнком там, не выходят!"},
},
"repeat": {
"f_address": {"panic": "Я же сказал — Ленина четырнадцать!",
"aggressive": "СКОЛЬКО МОЖНО! ЛЕНИНА ЧЕТЫРНАДЦАТЬ!"},
},
"fillers": {"panic": ["Что?! Не слышу!", "Алло! Вы там?!"]},
}
@pytest.fixture
def caller(tmp_path, monkeypatch):
from app.dialog import tree
monkeypatch.setattr(tree, "LIBRARY", tmp_path)
(tmp_path / f"{SCENARIO.id}.yaml").write_text(
yaml.safe_dump(TABLE, allow_unicode=True), encoding="utf-8"
)
return tree.TreeCaller(SCENARIO.id)
@pytest.fixture
def slots():
return SlotMachine(SCENARIO, StemEmbedder(), floor=0.5)
async def test_reply_comes_from_the_table(caller, slots):
persona = PersonaState(SCENARIO.persona)
line = await caller.reply(slots.hear("Какой адрес?"), persona, slots)
assert line.text == TABLE["reveal"]["f_address"]["panic"]
assert caller.misses == 0
async def test_repeat_uses_the_irritated_line(caller, slots):
persona = PersonaState(SCENARIO.persona)
await caller.reply(slots.hear("Какой адрес?"), persona, slots)
line = await caller.reply(slots.hear("Повторите адрес"), persona, slots)
assert line.text == TABLE["repeat"]["f_address"]["panic"]
async def test_mood_picks_another_line(caller, slots):
"""Дуга и директивы переключают вариант, а не текст."""
persona = PersonaState(SCENARIO.persona)
persona.directive = "turns_aggressive"
line = await caller.reply(slots.hear("Какой адрес?"), persona, slots)
assert line.mood is Mood.AGGRESSIVE
assert line.text == TABLE["reveal"]["f_address"]["aggressive"]
async def test_unknown_question_gets_a_filler(caller, slots):
persona = PersonaState(SCENARIO.persona)
line = await caller.reply(slots.hear("Вы в безопасности?"), persona, slots)
assert line.text in TABLE["fillers"]["panic"]
async def test_hole_in_the_table_falls_back_and_is_counted(caller, slots):
"""Пропуск не оставляет звонящего без голоса, но виден по счётчику:
предгенерацию пора повторить."""
persona = PersonaState(SCENARIO.persona)
line = await caller.reply(slots.hear("Что именно горит?"), persona, slots)
assert line.text, "звонящий промолчал"
assert caller.misses == 1
async def test_caller_never_speaks_an_unrevealed_fact(caller, slots):
persona = PersonaState(SCENARIO.persona)
for question in ("Служба 112, слушаю", "Успокойтесь", "Вы одна дома?"):
text = (await caller.reply(slots.hear(question), persona, slots)).text
revealed = {fact.id for fact in slots.revealed_facts()}
for fact in SCENARIO.facts:
if fact.id not in revealed:
assert fact.value not in text, f"выдал «{fact.value}» на «{question}»"
def test_refined_line_comes_from_the_table(tmp_path, monkeypatch):
"""Офлайн-звонящий должен поправляться уточнённым адресом, а не отвечать
заготовкой: секцию `refine` пишет `make pregen` (tasks/lct-19, lct-35)."""
import yaml as _yaml
from app.dialog import tree as tree_module
from app.scenarios.schema import Scenario
scenario = Scenario.model_validate(
{
"id": "refine-offline",
"title": "Проверка уточнения офлайн",
"type": "fire",
"level": "L3",
"persona": {"base": "calm", "arc": [{"stage": "registration", "mood": "calm"}]},
"first_line": "Горим!",
"facts": [
{
"id": "f_address",
"value": "улица Станционная, дом 28",
"refined": "Королёв, улица Станционная, дом 28",
"reveal_on": {"question": "q_address"},
"refine_on": "q_city",
}
],
"checklist": [
{"id": "q_address", "question": "Назовите адрес", "fact": "f_address"},
{"id": "q_city", "question": "Это точно Москва город?"},
],
}
)
table = {
"scenario": scenario.id,
"first_line": "Горим!",
"reveal": {"f_address": {"calm": "Станционная, двадцать восемь."}},
"refine": {"f_address": {"calm": "Нет, это Королёв, Станционная, двадцать восемь."}},
"repeat": {},
"fillers": {"calm": ["Не расслышал."]},
}
monkeypatch.setattr(tree_module, "LIBRARY", tmp_path)
(tmp_path / f"{scenario.id}.yaml").write_text(
_yaml.safe_dump(table, allow_unicode=True), encoding="utf-8"
)
caller = tree_module.TreeCaller(scenario.id)
slots = SlotMachine(scenario, StemEmbedder(), floor=0.5)
persona = PersonaState(scenario.persona)
import asyncio
asyncio.run(caller.reply(slots.hear("Назовите адрес"), persona, slots))
line = asyncio.run(caller.reply(slots.hear("Это точно Москва город?"), persona, slots))
assert "Королёв" in line.text, f"звонящий не поправился: {line.text}"
assert caller.misses == 0, "реплика ушла в заготовки, хотя в таблице она есть"