test_scheduler_filament_deficit.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """Scheduler pre-dispatch filament-deficit guard tests (#1496).
  2. ``PrintScheduler._block_on_filament_deficit`` is the gate that keeps an
  3. auto_dispatch=True VP intake (or any other scheduler-driven dispatch) from
  4. sending a print onto a spool that can't satisfy it. On a deficit it
  5. promotes the item to manual_start; when a previously-flagged item's spool
  6. is now adequate it clears the flag so the next tick dispatches.
  7. """
  8. from __future__ import annotations
  9. from unittest.mock import AsyncMock, patch
  10. import pytest
  11. from backend.app.models.print_queue import PrintQueueItem
  12. from backend.app.services.filament_deficit import FilamentDeficit
  13. from backend.app.services.print_scheduler import PrintScheduler
  14. @pytest.fixture
  15. def scheduler():
  16. """A fresh scheduler instance — internal state is not exercised."""
  17. return PrintScheduler()
  18. @pytest.fixture
  19. def queue_item(db_session, printer_factory):
  20. """Helper to drop a queue item the helper can mutate."""
  21. async def _make(**overrides):
  22. printer = await printer_factory()
  23. defaults = {
  24. "printer_id": printer.id,
  25. "status": "pending",
  26. "manual_start": False,
  27. "filament_short": False,
  28. }
  29. defaults.update(overrides)
  30. item = PrintQueueItem(**defaults)
  31. db_session.add(item)
  32. await db_session.commit()
  33. await db_session.refresh(item)
  34. return item
  35. return _make
  36. @pytest.mark.asyncio
  37. async def test_blocks_on_deficit_promotes_to_manual_start(scheduler, db_session, queue_item):
  38. item = await queue_item()
  39. with patch(
  40. "backend.app.services.print_scheduler.compute_deficit_for_queue_item",
  41. AsyncMock(
  42. return_value=[
  43. FilamentDeficit(
  44. slot_id=1,
  45. ams_id=0,
  46. tray_id=0,
  47. filament_type="PLA",
  48. required_grams=270.0,
  49. remaining_grams=200.0,
  50. ),
  51. ]
  52. ),
  53. ):
  54. blocked = await scheduler._block_on_filament_deficit(db_session, item)
  55. assert blocked is True
  56. await db_session.refresh(item)
  57. assert item.manual_start is True
  58. assert item.filament_short is True
  59. @pytest.mark.asyncio
  60. async def test_clears_stale_flag_when_deficit_resolves(scheduler, db_session, queue_item):
  61. """Previously-flagged item whose spool was swapped is unblocked."""
  62. item = await queue_item(filament_short=True, manual_start=False)
  63. with patch(
  64. "backend.app.services.print_scheduler.compute_deficit_for_queue_item",
  65. AsyncMock(return_value=[]),
  66. ):
  67. blocked = await scheduler._block_on_filament_deficit(db_session, item)
  68. assert blocked is False
  69. await db_session.refresh(item)
  70. assert item.filament_short is False
  71. assert item.manual_start is False
  72. @pytest.mark.asyncio
  73. async def test_no_deficit_no_op(scheduler, db_session, queue_item):
  74. """Happy path — no deficit, no flag changes, dispatch proceeds."""
  75. item = await queue_item()
  76. with patch(
  77. "backend.app.services.print_scheduler.compute_deficit_for_queue_item",
  78. AsyncMock(return_value=[]),
  79. ):
  80. blocked = await scheduler._block_on_filament_deficit(db_session, item)
  81. assert blocked is False
  82. await db_session.refresh(item)
  83. assert item.filament_short is False
  84. assert item.manual_start is False
  85. @pytest.mark.asyncio
  86. async def test_helper_exception_does_not_wedge_dispatch(scheduler, db_session, queue_item):
  87. """A flaky deficit check (e.g. Spoolman timeout) must not block dispatch."""
  88. item = await queue_item()
  89. with patch(
  90. "backend.app.services.print_scheduler.compute_deficit_for_queue_item",
  91. AsyncMock(side_effect=RuntimeError("network down")),
  92. ):
  93. blocked = await scheduler._block_on_filament_deficit(db_session, item)
  94. assert blocked is False
  95. await db_session.refresh(item)
  96. assert item.filament_short is False