mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 06:46:00 +00:00
The frontend rewrites every cover URL to .webp as soon as the heartbeat reports ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP, but existing covers have no .webp sibling until the scheduled cron eventually runs (the inline conversion only covers art fetched after enabling). This produced 404s on all existing covers until the cron fired. Enqueue a one-off backfill run of the conversion task on startup when the feature is enabled, mirroring the recompute-save-hashes pattern. A fixed job_id + Job.exists guard prevents duplicate jobs across restarts, and the task already skips covers that have a .webp sibling so repeated runs are cheap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
"""Tests for startup-time auto-enqueue of the recompute task."""
|
|
|
|
import startup
|
|
from rq.job import JOB_ID_PATTERN
|
|
|
|
|
|
def test_enqueue_recompute_skips_when_no_missing_hashes(mocker):
|
|
"""Saves all have content_hash -> no enqueue."""
|
|
mocker.patch.object(
|
|
startup.db_save_handler, "count_saves_missing_content_hash", return_value=0
|
|
)
|
|
enqueue = mocker.patch.object(startup.low_prio_queue, "enqueue")
|
|
|
|
startup._enqueue_recompute_save_hashes_if_needed()
|
|
|
|
enqueue.assert_not_called()
|
|
|
|
|
|
def test_enqueue_recompute_fires_when_missing_hashes_present(mocker):
|
|
"""At least one Save row has NULL content_hash -> enqueue exactly once."""
|
|
mocker.patch.object(
|
|
startup.db_save_handler, "count_saves_missing_content_hash", return_value=42
|
|
)
|
|
mocker.patch.object(startup.Job, "exists", return_value=False)
|
|
enqueue = mocker.patch.object(startup.low_prio_queue, "enqueue")
|
|
|
|
startup._enqueue_recompute_save_hashes_if_needed()
|
|
|
|
enqueue.assert_called_once()
|
|
args, kwargs = enqueue.call_args
|
|
# First positional arg is the bound task.run method
|
|
assert args[0].__self__ is startup.recompute_save_content_hashes_task
|
|
# Sanity-check the meta payload routes correctly in the task list UI
|
|
assert kwargs["meta"]["task_name"] == (
|
|
startup.recompute_save_content_hashes_task.title
|
|
)
|
|
assert kwargs["meta"]["task_type"] == (
|
|
startup.recompute_save_content_hashes_task.task_type.value
|
|
)
|
|
# Job timeout must be passed; otherwise long-running recomputes get killed
|
|
# by RQ's default short timeout on very large libraries.
|
|
assert kwargs["job_timeout"] == startup.TASK_TIMEOUT
|
|
assert kwargs["job_id"] == startup.RECOMPUTE_SAVE_HASHES_JOB_ID
|
|
|
|
|
|
def test_recompute_job_id_is_valid_rq_id():
|
|
"""RQ rejects any job_id not matching [A-Za-z0-9_-]+ (ValueError in set_id),
|
|
which the broad except here would swallow -> backfill silently never enqueues.
|
|
A colon was the original culprit; assert the full contract, not just that."""
|
|
assert JOB_ID_PATTERN.fullmatch(startup.RECOMPUTE_SAVE_HASHES_JOB_ID)
|
|
|
|
|
|
def test_enqueue_recompute_skips_when_already_queued(mocker):
|
|
"""An in-flight job from a previous restart -> skip enqueue, don't double up."""
|
|
mocker.patch.object(
|
|
startup.db_save_handler, "count_saves_missing_content_hash", return_value=10
|
|
)
|
|
mocker.patch.object(startup.Job, "exists", return_value=True)
|
|
enqueue = mocker.patch.object(startup.low_prio_queue, "enqueue")
|
|
|
|
startup._enqueue_recompute_save_hashes_if_needed()
|
|
|
|
enqueue.assert_not_called()
|
|
|
|
|
|
def test_enqueue_recompute_swallows_count_error(mocker):
|
|
"""A failed COUNT query must not crash startup."""
|
|
mocker.patch.object(
|
|
startup.db_save_handler,
|
|
"count_saves_missing_content_hash",
|
|
side_effect=RuntimeError("db gone"),
|
|
)
|
|
enqueue = mocker.patch.object(startup.low_prio_queue, "enqueue")
|
|
|
|
startup._enqueue_recompute_save_hashes_if_needed()
|
|
|
|
enqueue.assert_not_called()
|
|
|
|
|
|
def test_enqueue_recompute_swallows_enqueue_error(mocker):
|
|
"""A failed enqueue must not crash startup."""
|
|
mocker.patch.object(
|
|
startup.db_save_handler, "count_saves_missing_content_hash", return_value=5
|
|
)
|
|
mocker.patch.object(startup.Job, "exists", return_value=False)
|
|
mocker.patch.object(
|
|
startup.low_prio_queue, "enqueue", side_effect=RuntimeError("redis gone")
|
|
)
|
|
|
|
startup._enqueue_recompute_save_hashes_if_needed()
|
|
|
|
|
|
def test_enqueue_convert_webp_fires_when_not_queued(mocker):
|
|
"""No in-flight bootstrap job -> enqueue the backfill exactly once."""
|
|
mocker.patch.object(startup.Job, "exists", return_value=False)
|
|
enqueue = mocker.patch.object(startup.low_prio_queue, "enqueue")
|
|
|
|
startup._enqueue_convert_images_to_webp()
|
|
|
|
enqueue.assert_called_once()
|
|
args, kwargs = enqueue.call_args
|
|
assert args[0].__self__ is startup.convert_images_to_webp_task
|
|
assert kwargs["meta"]["task_name"] == startup.convert_images_to_webp_task.title
|
|
assert kwargs["meta"]["task_type"] == (
|
|
startup.convert_images_to_webp_task.task_type.value
|
|
)
|
|
assert kwargs["job_timeout"] == startup.TASK_TIMEOUT
|
|
assert kwargs["job_id"] == startup.CONVERT_IMAGES_TO_WEBP_JOB_ID
|
|
|
|
|
|
def test_convert_webp_job_id_is_valid_rq_id():
|
|
"""An invalid job_id raises in set_id, which the broad except swallows ->
|
|
backfill silently never enqueues. Assert the id matches RQ's contract."""
|
|
assert JOB_ID_PATTERN.fullmatch(startup.CONVERT_IMAGES_TO_WEBP_JOB_ID)
|
|
|
|
|
|
def test_enqueue_convert_webp_skips_when_already_queued(mocker):
|
|
"""An in-flight job from a previous restart -> skip enqueue, don't double up."""
|
|
mocker.patch.object(startup.Job, "exists", return_value=True)
|
|
enqueue = mocker.patch.object(startup.low_prio_queue, "enqueue")
|
|
|
|
startup._enqueue_convert_images_to_webp()
|
|
|
|
enqueue.assert_not_called()
|
|
|
|
|
|
def test_enqueue_convert_webp_swallows_enqueue_error(mocker):
|
|
"""A failed enqueue must not crash startup."""
|
|
mocker.patch.object(startup.Job, "exists", return_value=False)
|
|
mocker.patch.object(
|
|
startup.low_prio_queue, "enqueue", side_effect=RuntimeError("redis gone")
|
|
)
|
|
|
|
startup._enqueue_convert_images_to_webp()
|