196 lines
6.9 KiB
Python
196 lines
6.9 KiB
Python
import asyncio
|
|
import json
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from services.rpg_story import (
|
|
migrate_beats_to_steps,
|
|
normalize_story_arc,
|
|
get_current_step,
|
|
step_progress,
|
|
is_arc_completed,
|
|
sync_quest_to_current_step,
|
|
apply_step_advance,
|
|
apply_story_post,
|
|
is_new_arc_request,
|
|
normalize_new_arc_first,
|
|
new_arc_roll_choice,
|
|
append_new_arc_roll_choice,
|
|
format_step_guidance_for_character,
|
|
format_new_arc_opening,
|
|
normalize_step,
|
|
resolve_step_injection,
|
|
roll_next_arc,
|
|
)
|
|
|
|
|
|
def test_migrate_beats_to_steps():
|
|
arc = {
|
|
"beats": [
|
|
{"id": "b1", "title": "Meet Yuki", "injection": "Door opens.", "choices": []},
|
|
]
|
|
}
|
|
out = migrate_beats_to_steps(arc)
|
|
assert len(out["steps"]) == 1
|
|
assert "beats" not in out
|
|
assert out["steps"][0]["title"] == "Meet Yuki"
|
|
|
|
|
|
def test_step_progress():
|
|
arc = normalize_story_arc({
|
|
"steps": [{"id": "s1", "title": "A"}, {"id": "s2", "title": "B"}],
|
|
"current_step_index": 1,
|
|
})
|
|
assert step_progress(arc) == (2, 2)
|
|
|
|
|
|
def test_is_arc_completed():
|
|
arc = normalize_story_arc({
|
|
"steps": [{"id": "s1", "title": "A"}],
|
|
"current_step_index": 1,
|
|
"status": "active",
|
|
})
|
|
assert is_arc_completed(arc)
|
|
|
|
|
|
def test_new_arc_request_detected():
|
|
assert is_new_arc_request("Начать новую арку")
|
|
assert is_new_arc_request("Начать новый квест: 'Тайна плазменных шаров'")
|
|
assert is_new_arc_request("Start new quest")
|
|
assert not is_new_arc_request("привет")
|
|
|
|
|
|
def test_normalize_new_arc_first():
|
|
assert normalize_new_arc_first("user") == "user"
|
|
assert normalize_new_arc_first("Character") == "character"
|
|
assert normalize_new_arc_first("other") is None
|
|
|
|
|
|
def test_new_arc_roll_choice_and_filter():
|
|
base = new_arc_roll_choice()
|
|
assert base["type"] == "new_arc_roll"
|
|
merged = append_new_arc_roll_choice([
|
|
{"id": "x", "label": "Начать новый квест: Foo", "source": "narrator"},
|
|
{"id": "y", "label": "Пойти спать", "source": "narrator"},
|
|
])
|
|
assert len(merged) == 2
|
|
assert merged[-1]["type"] == "new_arc_roll"
|
|
assert not any("новый квест" in (c.get("label") or "").lower() for c in merged[:-1])
|
|
|
|
|
|
def test_normalize_step_repairs_legacy_beat_shape():
|
|
step = normalize_step({
|
|
"status_quo": "Сара и Григо прибывают к лаборатории.",
|
|
"choices": [{"label": "Войти", "injection": "*Дверь скрипит*"}],
|
|
}, 0)
|
|
assert step["title"]
|
|
assert step["completion_criteria"]
|
|
assert resolve_step_injection(step) == "*Дверь скрипит*"
|
|
|
|
|
|
def test_format_new_arc_opening():
|
|
arc = {"title": "Тайна", "global_story": "Расследование шаров."}
|
|
step = {"status_quo": "У ворот лаборатории тихо.", "choices": []}
|
|
text = format_new_arc_opening(arc, step, lang="ru")
|
|
assert "Тайна" in text
|
|
assert "лаборатории" in text or "Расследование" in text
|
|
|
|
|
|
def test_guidance_block_ru():
|
|
step = {"title": "Приютить", "goal": "Впустить внутрь", "character_guidance": "Робость"}
|
|
block = format_step_guidance_for_character(step, {"global_story": "x", "steps": [step]}, lang="ru")
|
|
assert "Приютить" in block
|
|
assert "ОБЯЗАТЕЛЬНОЕ" in block
|
|
|
|
|
|
def test_apply_step_advance():
|
|
arc = normalize_story_arc({
|
|
"title": "Test",
|
|
"steps": [
|
|
{"id": "s1", "title": "Step One", "choices": []},
|
|
{"id": "s2", "title": "Step Two", "injection": "Next scene.", "choices": []},
|
|
],
|
|
"current_step_index": 0,
|
|
})
|
|
|
|
async def run():
|
|
with patch("services.memory.upsert_quest", new_callable=AsyncMock) as uq:
|
|
with patch("services.memory.update_session_plot_arc", new_callable=AsyncMock):
|
|
with patch(
|
|
"services.rpg_story.sync_quest_to_current_step",
|
|
new_callable=AsyncMock,
|
|
):
|
|
return await apply_step_advance("sess", arc), uq
|
|
|
|
result, mock_uq = asyncio.run(run())
|
|
assert result["advanced"] is True
|
|
assert result["injection"] == "Next scene."
|
|
mock_uq.assert_called()
|
|
|
|
|
|
def test_apply_story_post_step_complete_fallback_via_note():
|
|
arc = normalize_story_arc({
|
|
"title": "Test",
|
|
"steps": [
|
|
{"id": "s1", "title": "Step One", "choices": []},
|
|
{"id": "s2", "title": "Step Two", "choices": []},
|
|
],
|
|
"current_step_index": 0,
|
|
})
|
|
|
|
post = {
|
|
"step_complete": False,
|
|
"step_completion_note": "The scene clearly satisfies the completion criteria.",
|
|
}
|
|
|
|
async def run():
|
|
with patch("services.memory.get_session", new_callable=AsyncMock) as gs:
|
|
gs.return_value = {"plot_arc_json": json.dumps(arc, ensure_ascii=False)}
|
|
with patch(
|
|
"services.rpg_story.apply_step_advance",
|
|
new_callable=AsyncMock,
|
|
) as asa:
|
|
asa.return_value = {"advanced": True, "arc_completed": False, "new_step": arc["steps"][1], "injection": ""}
|
|
r = await apply_story_post("sess", post, arc, {"quests": True})
|
|
return r, asa
|
|
|
|
r, asa = asyncio.run(run())
|
|
assert r["step_advanced"] is True
|
|
assert asa.await_count == 1
|
|
|
|
|
|
def test_roll_next_arc_combines_context_for_generate_next_arc():
|
|
old_arc = normalize_story_arc({
|
|
"title": "Arc One",
|
|
"steps": [{"id": "s1", "title": "Done"}],
|
|
"current_step_index": 1,
|
|
"status": "completed",
|
|
})
|
|
new_arc = normalize_story_arc({
|
|
"title": "Arc Two",
|
|
"steps": [{"id": "s1", "title": "Start"}],
|
|
"current_step_index": 0,
|
|
})
|
|
|
|
async def run():
|
|
with patch("services.memory.get_session", new_callable=AsyncMock) as gs:
|
|
gs.return_value = {"plot_arc_json": json.dumps(old_arc, ensure_ascii=False)}
|
|
with patch("services.rpg_plot.generate_next_arc", new_callable=AsyncMock) as gna:
|
|
gna.return_value = new_arc
|
|
with patch("services.memory.update_session_plot_arc", new_callable=AsyncMock):
|
|
with patch(
|
|
"services.rpg_story.sync_quest_to_current_step",
|
|
new_callable=AsyncMock,
|
|
):
|
|
return await roll_next_arc(
|
|
"sess",
|
|
{"name": "Sarah", "description": "", "scenario": ""},
|
|
"Начать новую арку",
|
|
"adventure",
|
|
recent_context="assistant: goodbye",
|
|
), gna
|
|
|
|
result, gna = asyncio.run(run())
|
|
assert result["title"] == "Arc Two"
|
|
gna.assert_awaited_once()
|
|
assert gna.await_args.args[3] == "assistant: goodbye\nНачать новую арку"
|