122 lines
5.1 KiB
Python
122 lines
5.1 KiB
Python
"""Предгенерация офлайн-таблиц: разбор ответа и планирование работы.
|
||
|
||
Сеть здесь не нужна и не используется: проверяется то, что ломается тихо —
|
||
разбор JSON от рассуждающей модели и решение «что ещё осталось сгенерировать».
|
||
Сама генерация запускается руками там, где есть провайдер (tasks/lct-19).
|
||
"""
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from app.scenarios.loader import load_file
|
||
from scripts.pregenerate import parse, tasks_for
|
||
|
||
LIBRARY = Path(__file__).resolve().parents[2] / "scenarios"
|
||
|
||
|
||
# ── разбор ответа модели ──
|
||
|
||
|
||
def test_plain_json_is_parsed():
|
||
assert parse('{"panic": "Горим!", "calm": "У нас пожар."}') == {
|
||
"panic": "Горим!",
|
||
"calm": "У нас пожар.",
|
||
}
|
||
|
||
|
||
def test_json_in_markdown_fence_is_parsed():
|
||
"""Рассуждающие модели любят обернуть ответ в ограду."""
|
||
answer = 'Вот реплики:\n```json\n{"panic": "Горим!"}\n```\nГотово.'
|
||
assert parse(answer) == {"panic": "Горим!"}
|
||
|
||
|
||
def test_broken_json_gives_nothing_not_a_crash():
|
||
"""Пустой разбор — это повтор запроса, а не падение прогона на середине."""
|
||
assert parse("извините, не могу") == {}
|
||
assert parse('{"panic": ') == {}
|
||
|
||
|
||
def test_empty_lines_are_dropped():
|
||
assert parse('{"panic": " ", "calm": "Пожар."}') == {"calm": "Пожар."}
|
||
|
||
|
||
def test_non_string_values_are_dropped():
|
||
assert parse('{"panic": ["а", "б"], "calm": "Пожар."}') == {"calm": "Пожар."}
|
||
|
||
|
||
# ── планирование ──
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def scenario():
|
||
return load_file(LIBRARY / "tickets" / "t03-1-fire-private-house.yaml", LIBRARY)
|
||
|
||
|
||
def test_empty_table_needs_every_fact(scenario):
|
||
todo = tasks_for(scenario, {}, force=False)
|
||
sections = {section for section, _, _ in todo}
|
||
assert "reveal" in sections and "repeat" in sections and "fillers" in sections
|
||
|
||
|
||
def test_refinable_fact_gets_its_own_section(scenario):
|
||
"""Без секции `refine` офлайн-звонящий на уточняющий вопрос ответит
|
||
заготовкой и уточнённого адреса не назовёт (lct-35)."""
|
||
todo = tasks_for(scenario, {}, force=False)
|
||
refine = [key for section, key, _ in todo if section == "refine"]
|
||
assert refine == ["f_address"]
|
||
|
||
|
||
def test_fact_without_refinement_has_no_refine_task(scenario):
|
||
todo = tasks_for(scenario, {}, force=False)
|
||
refine = {key for section, key, _ in todo if section == "refine"}
|
||
assert "f_what_burns" not in refine
|
||
|
||
|
||
def test_filled_table_needs_nothing(scenario):
|
||
"""Прерванный прогон продолжается с места остановки, а не с начала."""
|
||
from app.dialog.caller import MOOD_WORDS
|
||
|
||
moods = {mood.value: "реплика" for mood in MOOD_WORDS}
|
||
table = {
|
||
"reveal": {fact.id: dict(moods) for fact in scenario.facts},
|
||
"repeat": {fact.id: dict(moods) for fact in scenario.facts},
|
||
"refine": {fact.id: dict(moods) for fact in scenario.facts if fact.refined},
|
||
"fillers": {mood.value: ["раз", "два", "три"] for mood in MOOD_WORDS},
|
||
}
|
||
assert tasks_for(scenario, table, force=False) == []
|
||
|
||
|
||
def test_partially_filled_table_asks_only_for_the_rest(scenario):
|
||
from app.dialog.caller import MOOD_WORDS
|
||
|
||
moods = {mood.value: "реплика" for mood in MOOD_WORDS}
|
||
table = {"reveal": {fact.id: dict(moods) for fact in scenario.facts}}
|
||
sections = {section for section, _, _ in tasks_for(scenario, table, force=False)}
|
||
assert "reveal" not in sections
|
||
assert "repeat" in sections
|
||
|
||
|
||
def test_missing_single_mood_reopens_the_block(scenario):
|
||
"""Блок считается готовым только когда есть все настроения: иначе
|
||
звонящий молчит ровно в том состоянии, которого не хватает."""
|
||
from app.dialog.caller import MOOD_WORDS
|
||
|
||
moods = {mood.value: "реплика" for mood in MOOD_WORDS}
|
||
moods.pop("aggressive")
|
||
table = {"reveal": {fact.id: dict(moods) for fact in scenario.facts}}
|
||
reveal = [key for section, key, _ in tasks_for(scenario, table, force=False) if section == "reveal"]
|
||
assert reveal, "неполный блок не попал в работу"
|
||
|
||
|
||
def test_force_regenerates_everything(scenario):
|
||
from app.dialog.caller import MOOD_WORDS
|
||
|
||
moods = {mood.value: "реплика" for mood in MOOD_WORDS}
|
||
table = {
|
||
"reveal": {fact.id: dict(moods) for fact in scenario.facts},
|
||
"repeat": {fact.id: dict(moods) for fact in scenario.facts},
|
||
"refine": {fact.id: dict(moods) for fact in scenario.facts if fact.refined},
|
||
"fillers": {mood.value: ["раз", "два", "три"] for mood in MOOD_WORDS},
|
||
}
|
||
assert tasks_for(scenario, table, force=True)
|