test_dispatch_force_timelapse.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. """Tests for _resolve_effective_timelapse (#1397).
  2. Bambuddy forces timelapse recording on at dispatch time when the
  3. capture_finish_photo setting is enabled and the user did not opt in
  4. to timelapse for the specific print. The forced bit is recorded on
  5. the archive so the post-extraction cleanup path can delete the file.
  6. These tests exercise the four decision shapes the helper has to handle:
  7. 1. capture_finish_photo OFF → no override regardless of user choice
  8. 2. capture_finish_photo ON, user chose timelapse → no override (the
  9. user's choice already covers the photo path)
  10. 3. capture_finish_photo ON, user chose NO timelapse → override to ON,
  11. mark archive.bambuddy_forced_timelapse=True
  12. 4. capture_finish_photo unset (None / missing) → defaults to ON, so
  13. the same override applies as case 3
  14. """
  15. from types import SimpleNamespace
  16. from unittest.mock import AsyncMock, patch
  17. import pytest
  18. from backend.app.services.background_dispatch import (
  19. BackgroundDispatchService,
  20. PrintDispatchJob,
  21. )
  22. def _make_job(timelapse: bool | None) -> PrintDispatchJob:
  23. """Mint a job with the smallest valid shape — the only field
  24. _resolve_effective_timelapse reads from job is `options`."""
  25. return PrintDispatchJob(
  26. id=1,
  27. kind="print_library_file",
  28. source_id=42,
  29. source_name="test.gcode.3mf",
  30. printer_id=10,
  31. printer_name="Printer A",
  32. options={"timelapse": timelapse} if timelapse is not None else {},
  33. )
  34. def _make_archive() -> SimpleNamespace:
  35. """Stand-in archive object; the helper only touches .id and
  36. .bambuddy_forced_timelapse."""
  37. return SimpleNamespace(id=99, bambuddy_forced_timelapse=False)
  38. def _make_db() -> AsyncMock:
  39. """Fake db with a no-op .commit()."""
  40. db = AsyncMock()
  41. return db
  42. @pytest.mark.asyncio
  43. async def test_capture_finish_photo_off_means_no_override():
  44. """Master toggle off → user's timelapse=False stays False, no flag set."""
  45. service = BackgroundDispatchService()
  46. archive = _make_archive()
  47. db = _make_db()
  48. job = _make_job(timelapse=False)
  49. with patch(
  50. "backend.app.api.routes.settings.get_setting",
  51. new=AsyncMock(return_value="false"),
  52. ):
  53. effective = await service._resolve_effective_timelapse(db, archive, job)
  54. assert effective is False
  55. assert archive.bambuddy_forced_timelapse is False
  56. db.commit.assert_not_awaited()
  57. @pytest.mark.asyncio
  58. async def test_user_opted_in_passes_through_unchanged():
  59. """User asked for a timelapse → no override needed (their normal flow
  60. already records one). bambuddy_forced_timelapse stays False so cleanup
  61. leaves the file alone."""
  62. service = BackgroundDispatchService()
  63. archive = _make_archive()
  64. db = _make_db()
  65. job = _make_job(timelapse=True)
  66. # get_setting shouldn't even be consulted — but if it is, no override
  67. # should still fire.
  68. with patch(
  69. "backend.app.api.routes.settings.get_setting",
  70. new=AsyncMock(return_value="true"),
  71. ):
  72. effective = await service._resolve_effective_timelapse(db, archive, job)
  73. assert effective is True
  74. assert archive.bambuddy_forced_timelapse is False
  75. db.commit.assert_not_awaited()
  76. @pytest.mark.asyncio
  77. async def test_capture_on_user_off_forces_timelapse_and_marks_flag():
  78. """The whole point of the fix: capture_finish_photo=on + user-timelapse=off
  79. flips the MQTT command to timelapse=True and marks the archive for
  80. post-extraction cleanup."""
  81. service = BackgroundDispatchService()
  82. archive = _make_archive()
  83. db = _make_db()
  84. job = _make_job(timelapse=False)
  85. with patch(
  86. "backend.app.api.routes.settings.get_setting",
  87. new=AsyncMock(return_value="true"),
  88. ):
  89. effective = await service._resolve_effective_timelapse(db, archive, job)
  90. assert effective is True
  91. assert archive.bambuddy_forced_timelapse is True
  92. db.commit.assert_awaited_once()
  93. @pytest.mark.asyncio
  94. async def test_capture_finish_photo_unset_defaults_to_enabled():
  95. """Setting absent from DB → default is True (per the Field default in the
  96. schema), so the override fires just like when explicitly enabled."""
  97. service = BackgroundDispatchService()
  98. archive = _make_archive()
  99. db = _make_db()
  100. job = _make_job(timelapse=False)
  101. with patch(
  102. "backend.app.api.routes.settings.get_setting",
  103. new=AsyncMock(return_value=None),
  104. ):
  105. effective = await service._resolve_effective_timelapse(db, archive, job)
  106. assert effective is True
  107. assert archive.bambuddy_forced_timelapse is True
  108. db.commit.assert_awaited_once()
  109. @pytest.mark.asyncio
  110. async def test_user_missing_timelapse_treated_as_false():
  111. """Some queue paths pass options without a timelapse key. Treat absent
  112. as False (matches existing job.options.get('timelapse', False) default
  113. that the caller previously used)."""
  114. service = BackgroundDispatchService()
  115. archive = _make_archive()
  116. db = _make_db()
  117. job = _make_job(timelapse=None) # falls through to {}
  118. with patch(
  119. "backend.app.api.routes.settings.get_setting",
  120. new=AsyncMock(return_value="true"),
  121. ):
  122. effective = await service._resolve_effective_timelapse(db, archive, job)
  123. assert effective is True
  124. assert archive.bambuddy_forced_timelapse is True