2026-03-10 17:02:50 +08:00
|
|
|
|
import os
|
2026-03-10 16:03:49 +08:00
|
|
|
|
import unittest
|
2026-03-10 17:02:50 +08:00
|
|
|
|
import unittest.mock
|
2026-03-10 16:03:49 +08:00
|
|
|
|
|
|
|
|
|
|
from api.tasks.memoir_tasks import initialize_chapter_images
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MemoirImageBootstrapTest(unittest.TestCase):
|
2026-03-11 11:26:42 +08:00
|
|
|
|
def test_initialize_chapter_images_keeps_only_completed_assets_when_disabled(self):
|
|
|
|
|
|
chapter = type(
|
|
|
|
|
|
"ChapterStub",
|
|
|
|
|
|
(),
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "chapter-1",
|
|
|
|
|
|
"title": "童年的夏天",
|
|
|
|
|
|
"category": "childhood",
|
|
|
|
|
|
"content": "那条路我一直记得。",
|
|
|
|
|
|
"images": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"index": 0,
|
|
|
|
|
|
"placeholder": "{{IMAGE:南方小镇的青石板路}}",
|
|
|
|
|
|
"description": "南方小镇的青石板路",
|
|
|
|
|
|
"status": "completed",
|
|
|
|
|
|
"url": "https://cos.example.com/existing.png",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"index": 1,
|
|
|
|
|
|
"placeholder": "{{IMAGE:奶奶坐在院子里的藤椅上}}",
|
|
|
|
|
|
"description": "奶奶坐在院子里的藤椅上",
|
|
|
|
|
|
"status": "pending",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
)()
|
|
|
|
|
|
|
|
|
|
|
|
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "false"}, clear=False):
|
|
|
|
|
|
assets = initialize_chapter_images(chapter)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(assets), 1)
|
|
|
|
|
|
self.assertEqual(assets[0]["status"], "completed")
|
|
|
|
|
|
self.assertEqual(assets[0]["url"], "https://cos.example.com/existing.png")
|
|
|
|
|
|
self.assertEqual(chapter.images, assets)
|
|
|
|
|
|
|
2026-03-10 17:02:50 +08:00
|
|
|
|
def test_initialize_chapter_images_sets_pending_assets_when_enabled(self):
|
2026-03-10 16:03:49 +08:00
|
|
|
|
chapter = type(
|
|
|
|
|
|
"ChapterStub",
|
|
|
|
|
|
(),
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "chapter-1",
|
|
|
|
|
|
"title": "童年的夏天",
|
|
|
|
|
|
"category": "childhood",
|
|
|
|
|
|
"content": "那条路我一直记得。\n\n{{{{IMAGE:南方小镇的青石板路}}}}",
|
|
|
|
|
|
"images": [],
|
|
|
|
|
|
},
|
|
|
|
|
|
)()
|
|
|
|
|
|
|
2026-03-10 17:02:50 +08:00
|
|
|
|
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
|
|
|
|
|
|
assets = initialize_chapter_images(chapter)
|
2026-03-10 16:03:49 +08:00
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(assets), 1)
|
|
|
|
|
|
self.assertEqual(assets[0]["status"], "pending")
|
2026-03-10 17:02:50 +08:00
|
|
|
|
|
|
|
|
|
|
def test_initialize_chapter_images_preserves_completed_assets_and_adds_only_new_placeholders(self):
|
|
|
|
|
|
chapter = type(
|
|
|
|
|
|
"ChapterStub",
|
|
|
|
|
|
(),
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "chapter-1",
|
|
|
|
|
|
"title": "童年的夏天",
|
|
|
|
|
|
"category": "childhood",
|
|
|
|
|
|
"content": (
|
|
|
|
|
|
"那条路我一直记得。\n\n"
|
|
|
|
|
|
"{{{{IMAGE:南方小镇的青石板路}}}}\n\n"
|
|
|
|
|
|
"奶奶总坐在门口。\n\n"
|
|
|
|
|
|
"{{{{IMAGE:奶奶坐在院子里的藤椅上}}}}"
|
|
|
|
|
|
),
|
|
|
|
|
|
"images": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"index": 0,
|
|
|
|
|
|
"placeholder": "{{{{IMAGE:南方小镇的青石板路}}}}",
|
|
|
|
|
|
"description": "南方小镇的青石板路",
|
|
|
|
|
|
"prompt": "A serene southern China town",
|
|
|
|
|
|
"url": "https://cos.example.com/existing.png",
|
|
|
|
|
|
"status": "completed",
|
|
|
|
|
|
"provider": "liblib",
|
|
|
|
|
|
"style": "watercolor",
|
|
|
|
|
|
"size": "1024x1024",
|
|
|
|
|
|
"error": None,
|
|
|
|
|
|
"created_at": "2026-03-10T10:00:00Z",
|
|
|
|
|
|
"updated_at": "2026-03-10T10:00:00Z",
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
)()
|
|
|
|
|
|
|
|
|
|
|
|
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
|
|
|
|
|
|
assets = initialize_chapter_images(chapter)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(assets), 2)
|
|
|
|
|
|
self.assertEqual(assets[0]["status"], "completed")
|
|
|
|
|
|
self.assertEqual(assets[0]["url"], "https://cos.example.com/existing.png")
|
|
|
|
|
|
self.assertEqual(assets[1]["status"], "pending")
|
|
|
|
|
|
self.assertEqual(assets[1]["description"], "奶奶坐在院子里的藤椅上")
|
2026-03-11 10:06:12 +08:00
|
|
|
|
|
|
|
|
|
|
def test_initialize_chapter_images_accepts_double_brace_placeholders(self):
|
|
|
|
|
|
chapter = type(
|
|
|
|
|
|
"ChapterStub",
|
|
|
|
|
|
(),
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "chapter-1",
|
|
|
|
|
|
"title": "童年的夏天",
|
|
|
|
|
|
"category": "childhood",
|
|
|
|
|
|
"content": "开头。\n\n{{IMAGE:1938年初的上海弄堂口,冬日萧瑟}}\n\n结尾。",
|
|
|
|
|
|
"images": [],
|
|
|
|
|
|
},
|
|
|
|
|
|
)()
|
|
|
|
|
|
|
|
|
|
|
|
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
|
|
|
|
|
|
assets = initialize_chapter_images(chapter)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(assets), 1)
|
|
|
|
|
|
self.assertEqual(assets[0]["status"], "pending")
|
|
|
|
|
|
self.assertEqual(assets[0]["placeholder"], "{{IMAGE:1938年初的上海弄堂口,冬日萧瑟}}")
|
2026-03-11 11:26:42 +08:00
|
|
|
|
|
|
|
|
|
|
def test_initialize_chapter_images_normalizes_invalid_existing_asset_status(self):
|
|
|
|
|
|
chapter = type(
|
|
|
|
|
|
"ChapterStub",
|
|
|
|
|
|
(),
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "chapter-1",
|
|
|
|
|
|
"title": "童年的夏天",
|
|
|
|
|
|
"category": "childhood",
|
|
|
|
|
|
"content": "开头。\n\n{{IMAGE:南方小镇的青石板路}}\n\n结尾。",
|
|
|
|
|
|
"images": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"index": 0,
|
|
|
|
|
|
"placeholder": "{{IMAGE:南方小镇的青石板路}}",
|
|
|
|
|
|
"description": "南方小镇的青石板路",
|
|
|
|
|
|
"status": "mystery",
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
)()
|
|
|
|
|
|
|
|
|
|
|
|
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
|
|
|
|
|
|
assets = initialize_chapter_images(chapter)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(assets), 1)
|
|
|
|
|
|
self.assertEqual(assets[0]["status"], "failed")
|
|
|
|
|
|
self.assertEqual(assets[0]["error"], "invalid image status: mystery")
|
2026-03-11 14:07:02 +08:00
|
|
|
|
|
|
|
|
|
|
def test_initialize_chapter_images_preserves_existing_completed_assets_beyond_effective_max(self):
|
|
|
|
|
|
chapter = type(
|
|
|
|
|
|
"ChapterStub",
|
|
|
|
|
|
(),
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "chapter-1",
|
|
|
|
|
|
"title": "童年的夏天",
|
|
|
|
|
|
"category": "childhood",
|
|
|
|
|
|
"content": (
|
|
|
|
|
|
"{{IMAGE:南方小镇的青石板路}}\n"
|
|
|
|
|
|
"{{IMAGE:奶奶坐在院子里的藤椅上}}\n"
|
|
|
|
|
|
"{{IMAGE:门前的老槐树}}"
|
|
|
|
|
|
),
|
|
|
|
|
|
"images": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"index": 0,
|
|
|
|
|
|
"placeholder": "{{IMAGE:南方小镇的青石板路}}",
|
|
|
|
|
|
"description": "南方小镇的青石板路",
|
|
|
|
|
|
"status": "completed",
|
|
|
|
|
|
"url": "https://cos.example.com/1.png",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"index": 1,
|
|
|
|
|
|
"placeholder": "{{IMAGE:奶奶坐在院子里的藤椅上}}",
|
|
|
|
|
|
"description": "奶奶坐在院子里的藤椅上",
|
|
|
|
|
|
"status": "completed",
|
|
|
|
|
|
"url": "https://cos.example.com/2.png",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"index": 2,
|
|
|
|
|
|
"placeholder": "{{IMAGE:门前的老槐树}}",
|
|
|
|
|
|
"description": "门前的老槐树",
|
|
|
|
|
|
"status": "completed",
|
|
|
|
|
|
"url": "https://cos.example.com/3.png",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
)()
|
|
|
|
|
|
|
|
|
|
|
|
with unittest.mock.patch.dict(
|
|
|
|
|
|
os.environ,
|
|
|
|
|
|
{
|
|
|
|
|
|
"MEMOIR_IMAGE_ENABLED": "true",
|
|
|
|
|
|
"MEMOIR_IMAGE_MAX_PER_CHAPTER": "2",
|
|
|
|
|
|
"MEMOIR_IMAGE_CHARS_PER_EXTRA": "99999",
|
|
|
|
|
|
"MEMOIR_IMAGE_MAX_CAP": "8",
|
|
|
|
|
|
},
|
|
|
|
|
|
clear=False,
|
|
|
|
|
|
):
|
|
|
|
|
|
assets = initialize_chapter_images(chapter)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(assets), 3)
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
[asset["placeholder"] for asset in assets],
|
|
|
|
|
|
[
|
|
|
|
|
|
"{{IMAGE:南方小镇的青石板路}}",
|
|
|
|
|
|
"{{IMAGE:奶奶坐在院子里的藤椅上}}",
|
|
|
|
|
|
"{{IMAGE:门前的老槐树}}",
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertTrue(all(asset["status"] == "completed" for asset in assets))
|
|
|
|
|
|
|
|
|
|
|
|
def test_initialize_chapter_images_increases_limit_for_long_content(self):
|
|
|
|
|
|
chapter = type(
|
|
|
|
|
|
"ChapterStub",
|
|
|
|
|
|
(),
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "chapter-1",
|
|
|
|
|
|
"title": "童年的夏天",
|
|
|
|
|
|
"category": "childhood",
|
|
|
|
|
|
"content": (
|
|
|
|
|
|
("很长的正文" * 800)
|
|
|
|
|
|
+ "\n{{IMAGE:南方小镇的青石板路}}"
|
|
|
|
|
|
+ "\n{{IMAGE:奶奶坐在院子里的藤椅上}}"
|
|
|
|
|
|
+ "\n{{IMAGE:门前的老槐树}}"
|
|
|
|
|
|
+ "\n{{IMAGE:夏夜的晒谷场}}"
|
|
|
|
|
|
),
|
|
|
|
|
|
"images": [],
|
|
|
|
|
|
},
|
|
|
|
|
|
)()
|
|
|
|
|
|
|
|
|
|
|
|
with unittest.mock.patch.dict(
|
|
|
|
|
|
os.environ,
|
|
|
|
|
|
{
|
|
|
|
|
|
"MEMOIR_IMAGE_ENABLED": "true",
|
|
|
|
|
|
"MEMOIR_IMAGE_MAX_PER_CHAPTER": "2",
|
|
|
|
|
|
"MEMOIR_IMAGE_CHARS_PER_EXTRA": "1000",
|
|
|
|
|
|
"MEMOIR_IMAGE_MAX_CAP": "8",
|
|
|
|
|
|
},
|
|
|
|
|
|
clear=False,
|
|
|
|
|
|
):
|
|
|
|
|
|
assets = initialize_chapter_images(chapter)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(assets), 4)
|
|
|
|
|
|
self.assertTrue(all(asset["status"] == "pending" for asset in assets))
|
|
|
|
|
|
|
|
|
|
|
|
def test_initialize_chapter_images_caps_dynamic_limit_at_max_images_cap(self):
|
|
|
|
|
|
chapter = type(
|
|
|
|
|
|
"ChapterStub",
|
|
|
|
|
|
(),
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "chapter-1",
|
|
|
|
|
|
"title": "童年的夏天",
|
|
|
|
|
|
"category": "childhood",
|
|
|
|
|
|
"content": (
|
|
|
|
|
|
("很长的正文" * 1600)
|
|
|
|
|
|
+ "\n{{IMAGE:图1}}"
|
|
|
|
|
|
+ "\n{{IMAGE:图2}}"
|
|
|
|
|
|
+ "\n{{IMAGE:图3}}"
|
|
|
|
|
|
+ "\n{{IMAGE:图4}}"
|
|
|
|
|
|
+ "\n{{IMAGE:图5}}"
|
|
|
|
|
|
+ "\n{{IMAGE:图6}}"
|
|
|
|
|
|
),
|
|
|
|
|
|
"images": [],
|
|
|
|
|
|
},
|
|
|
|
|
|
)()
|
|
|
|
|
|
|
|
|
|
|
|
with unittest.mock.patch.dict(
|
|
|
|
|
|
os.environ,
|
|
|
|
|
|
{
|
|
|
|
|
|
"MEMOIR_IMAGE_ENABLED": "true",
|
|
|
|
|
|
"MEMOIR_IMAGE_MAX_PER_CHAPTER": "2",
|
|
|
|
|
|
"MEMOIR_IMAGE_CHARS_PER_EXTRA": "1000",
|
|
|
|
|
|
"MEMOIR_IMAGE_MAX_CAP": "4",
|
|
|
|
|
|
},
|
|
|
|
|
|
clear=False,
|
|
|
|
|
|
):
|
|
|
|
|
|
assets = initialize_chapter_images(chapter)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(assets), 4)
|
|
|
|
|
|
self.assertEqual([asset["description"] for asset in assets], ["图1", "图2", "图3", "图4"])
|