"""Tests for chapter cover Celery enqueue deduplication gate.""" from unittest.mock import MagicMock, patch from app.tasks.chapter_cover_enqueue import ( _enqueue_dedup_key, try_enqueue_generate_chapter_cover, ) def _md_with_n_inline_images(n: int) -> str: lines = [f"![c](asset://a{i})" for i in range(n)] return "\n\n".join(lines) + "\n\n正文" def _eligible_pipeline_chapter(): ch = MagicMock() ch.cover_asset_id = None # 章节封面:正文内 asset 插图需 >3 才入队 ch.canonical_markdown = _md_with_n_inline_images(4) ch.images = [] return ch @patch("app.tasks.chapter_cover_tasks.generate_chapter_cover") @patch("app.tasks.chapter_cover_enqueue.redis.from_url") @patch("app.tasks.chapter_cover_enqueue._load_chapter_for_enqueue_sync") def test_try_enqueue_false_when_chapter_missing(mock_load, mock_redis, mock_gen_task): mock_load.return_value = None assert try_enqueue_generate_chapter_cover("missing-id", "pipeline") is False mock_gen_task.delay.assert_not_called() mock_redis.assert_not_called() @patch("app.tasks.chapter_cover_tasks.generate_chapter_cover") @patch("app.tasks.chapter_cover_enqueue.redis.from_url") @patch("app.tasks.chapter_cover_enqueue._load_chapter_for_enqueue_sync") def test_try_enqueue_false_when_cover_asset_exists( mock_load, mock_redis, mock_gen_task ): ch = _eligible_pipeline_chapter() ch.cover_asset_id = "asset-1" mock_load.return_value = ch assert try_enqueue_generate_chapter_cover("ch-1", "pipeline") is False mock_gen_task.delay.assert_not_called() mock_redis.assert_not_called() @patch("app.tasks.chapter_cover_tasks.generate_chapter_cover") @patch("app.tasks.chapter_cover_enqueue.redis.from_url") @patch("app.tasks.chapter_cover_enqueue._load_chapter_for_enqueue_sync") def test_try_enqueue_false_when_redis_nx_not_acquired( mock_load, mock_redis, mock_gen_task ): mock_load.return_value = _eligible_pipeline_chapter() r = MagicMock() r.set.return_value = False mock_redis.return_value = r assert try_enqueue_generate_chapter_cover("ch-1", "pipeline") is False mock_gen_task.delay.assert_not_called() r.set.assert_called_once() @patch("app.tasks.chapter_cover_tasks.generate_chapter_cover") @patch("app.tasks.chapter_cover_enqueue.redis.from_url") @patch("app.tasks.chapter_cover_enqueue._load_chapter_for_enqueue_sync") def test_try_enqueue_true_when_eligible_and_nx_ok(mock_load, mock_redis, mock_gen_task): mock_load.return_value = _eligible_pipeline_chapter() r = MagicMock() r.set.return_value = True mock_redis.return_value = r mock_gen_task.delay = MagicMock() assert try_enqueue_generate_chapter_cover("ch-1", "pipeline") is True mock_gen_task.delay.assert_called_once_with("ch-1") r.set.assert_called_once() @patch("app.tasks.chapter_cover_tasks.generate_chapter_cover") @patch("app.tasks.chapter_cover_enqueue.redis.from_url") @patch("app.tasks.chapter_cover_enqueue._load_chapter_for_enqueue_sync") def test_try_enqueue_false_when_inline_images_not_enough( mock_load, mock_redis, mock_gen_task ): ch = _eligible_pipeline_chapter() ch.canonical_markdown = _md_with_n_inline_images(3) mock_load.return_value = ch assert try_enqueue_generate_chapter_cover("ch-1", "pipeline") is False mock_gen_task.delay.assert_not_called() mock_redis.assert_not_called() @patch("app.tasks.chapter_cover_tasks.generate_chapter_cover") @patch("app.tasks.chapter_cover_enqueue.redis.from_url") @patch("app.tasks.chapter_cover_enqueue._load_chapter_for_enqueue_sync") def test_try_enqueue_deletes_dedup_key_when_delay_fails( mock_load, mock_redis, mock_gen_task ): mock_load.return_value = _eligible_pipeline_chapter() r = MagicMock() r.set.return_value = True mock_redis.return_value = r mock_gen_task.delay = MagicMock(side_effect=RuntimeError("broker down")) assert try_enqueue_generate_chapter_cover("ch-1", "pipeline") is False mock_gen_task.delay.assert_called_once_with("ch-1") r.delete.assert_called_once_with(_enqueue_dedup_key("ch-1")) @patch("app.tasks.chapter_cover_tasks.generate_chapter_cover") @patch("app.tasks.chapter_cover_enqueue.redis.from_url") @patch("app.tasks.chapter_cover_enqueue._load_chapter_for_enqueue_sync") def test_try_enqueue_http_skips_empty_category(mock_load, mock_redis, mock_gen_task): ch = _eligible_pipeline_chapter() ch.category = None ch.status = "completed" mock_load.return_value = ch assert try_enqueue_generate_chapter_cover("ch-1", "http") is False mock_gen_task.delay.assert_not_called() mock_redis.assert_not_called()