Files
life-echo/api/tests/test_image_prompt_policy.py
Kevin 59d4b19d7d feat(api): 回忆录管线简化、路由延迟池与相关加固
- Phase1/2:移除 MemoirOrchestrator.run 与 process_memoir_segments 别名;文档改为 process_memoir_phase1。
- 槽位校验集中到 stage_constants(filter_stage_slots),批处理与顺序路径及 state_service 写库一致。
- StoryRoute:no_llm/parse_error/invalid_target 保守 new_story;短篇护栏不覆盖这些 fallback。
- Phase2 低置信单路径可选延迟(StoryPipelineResult.deferred):不写 Chapter/Story,Segment 记录 defer 元数据,冷却内不重复消费;上限后停自动重试,Phase1 同类目新段唤醒池内段。
- Alembic 0017:segments 表 narrative_defer_* 列。
- ProfileAgent:经 LlmGateway/注入 Provider 统一聊天与 JSON,新增测试。
- ImagePromptOrchestrator:LLM 初始化失败可依配置降级或硬失败;补充策略测试。
- 配套单测与 README/本地开发文档表述更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 13:18:02 +08:00

165 lines
5.5 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.
"""MemoirImagePromptService 与 image_prompt_fallback_disabledstory 主图 / 章节封面)。"""
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()