"""MemoirImagePromptService 与 image_prompt_fallback_disabled(story 主图 / 章节封面)。""" import json import pytest from app.features.memoir.memoir_images.prompting import MemoirImagePromptService from app.features.memoir.memoir_images.settings import MemoirImageSettings def _svc(llm=None) -> MemoirImagePromptService: return MemoirImagePromptService(llm=llm, settings=MemoirImageSettings()) def test_story_primary_fallback_uses_placeholder_when_llm_disabled(): from app.features.memoir.memoir_images.image_placeholder_template import ( IMAGE_PLACEHOLDER_TEMPLATE, ) out = _svc(llm=None).build_story_primary_prompt( "My Story", "career", "rainy street", None ) assert IMAGE_PLACEHOLDER_TEMPLATE in out["prompt"] assert "rainy street" in out["prompt"] def test_story_primary_fallback_disabled_requires_brief(monkeypatch): monkeypatch.setattr( "app.features.memoir.memoir_images.prompting.settings.image_prompt_fallback_disabled", True, ) with pytest.raises(RuntimeError, match="prompt_brief"): _svc(llm=None).build_story_primary_prompt("T", "career", "", None) def test_story_primary_style_from_chapter_category_when_no_intent_style(): """Story.stage 存章节 category 时,未指定 style_profile 应走 CATEGORY_STYLE_MAP。""" out = _svc(llm=None).build_story_primary_prompt("T", "career_early", "brief", None) assert out["style"] == "realistic" def test_story_primary_style_from_chat_stage_when_no_intent_style(): out = _svc(llm=None).build_story_primary_prompt("T", "career", "brief", None) assert out["style"] == "realistic" def test_story_primary_fallback_disabled_requires_llm(monkeypatch): monkeypatch.setattr( "app.features.memoir.memoir_images.prompting.settings.image_prompt_fallback_disabled", True, ) with pytest.raises(RuntimeError, match="requires LLM"): _svc(llm=None).build_story_primary_prompt("T", "career", "brief", None) def test_story_primary_llm_path(monkeypatch): class _FakeLlm: def invoke(self, *_a, **_kw): # noqa: ANN001 raise AssertionError("invoke should not be used") captured: dict[str, str] = {} def fake_invoke_json(llm, prompt_text, **_kw): # noqa: ANN001 captured["prompt_text"] = prompt_text return json.dumps( { "prompt": "A hero scene at dawn", "style": "watercolor", "size": "1280x720", } ) monkeypatch.setattr( "app.features.memoir.memoir_images.prompting.invoke_json_object", fake_invoke_json, ) out = _svc(llm=_FakeLlm()).build_story_primary_prompt( "Title", "childhood", "memo", "realistic" ) assert out["prompt"].lower().startswith("watercolor") assert "dawn" in out["prompt"].lower() assert out["style"] == "watercolor" raw = captured.get("prompt_text", "") assert "Title" in raw and "memo" in raw def test_cover_fallback_uses_service_template_when_llm_disabled(): out = _svc(llm=None).build_cover_prompt( chapter_title="Ch1", chapter_category="family", context_excerpt="mountain lake", ) assert "mountain lake" in out["prompt"].lower() assert ( "chapter cover" in out["prompt"].lower() or "illustration" in out["prompt"].lower() ) def test_cover_fallback_when_excerpt_empty(monkeypatch): monkeypatch.setattr( "app.features.memoir.memoir_images.prompting.settings.image_prompt_fallback_disabled", False, ) out = _svc(llm=None).build_cover_prompt( chapter_title="", chapter_category="family", context_excerpt="", ) assert len(out["prompt"]) > 20 def test_cover_fallback_disabled_requires_excerpt(monkeypatch): monkeypatch.setattr( "app.features.memoir.memoir_images.prompting.settings.image_prompt_fallback_disabled", True, ) with pytest.raises(RuntimeError, match="context_excerpt"): _svc(llm=None).build_cover_prompt( chapter_title="T", chapter_category="family", context_excerpt="", ) def test_image_prompt_orchestrator_provider_failure_uses_fallback(monkeypatch): from app.agents.image_prompt.orchestrator import get_image_prompt_orchestrator class BoomGateway: def langchain_llm_for(self, *_a, **_kw): # noqa: ANN001 raise RuntimeError("provider missing") monkeypatch.setattr( "app.agents.image_prompt.orchestrator.settings.image_prompt_fallback_disabled", False, ) monkeypatch.setattr("app.core.llm_gateway.LlmGateway", lambda: BoomGateway()) orch = get_image_prompt_orchestrator() out = orch.build_cover_prompt( chapter_title="T", chapter_category="family", context_excerpt="mountain lake", ) assert "mountain lake" in out["prompt"].lower() def test_image_prompt_orchestrator_provider_failure_raises_when_disabled( monkeypatch, ): from app.agents.image_prompt.orchestrator import get_image_prompt_orchestrator class BoomGateway: def langchain_llm_for(self, *_a, **_kw): # noqa: ANN001 raise RuntimeError("provider missing") monkeypatch.setattr( "app.agents.image_prompt.orchestrator.settings.image_prompt_fallback_disabled", True, ) monkeypatch.setattr("app.core.llm_gateway.LlmGateway", lambda: BoomGateway()) with pytest.raises(RuntimeError, match="provider missing"): get_image_prompt_orchestrator()