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Начать новую арку"