|
@@ -18,9 +18,18 @@ class _FakeAssignmentsResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeSession:
|
|
class _FakeSession:
|
|
|
- def __init__(self, printer_name: str, assignments: list[SimpleNamespace]):
|
|
|
|
|
|
|
+ """Fake DB session that returns legacy vs. Spoolman assignment rows based
|
|
|
|
|
+ on which table the SELECT targets, so tests can exercise either mode."""
|
|
|
|
|
+
|
|
|
|
|
+ def __init__(
|
|
|
|
|
+ self,
|
|
|
|
|
+ printer_name: str,
|
|
|
|
|
+ legacy: list[SimpleNamespace] | None = None,
|
|
|
|
|
+ spoolman: list[SimpleNamespace] | None = None,
|
|
|
|
|
+ ):
|
|
|
self._printer = SimpleNamespace(name=printer_name)
|
|
self._printer = SimpleNamespace(name=printer_name)
|
|
|
- self._assignments = assignments
|
|
|
|
|
|
|
+ self._legacy = legacy or []
|
|
|
|
|
+ self._spoolman = spoolman or []
|
|
|
|
|
|
|
|
async def __aenter__(self):
|
|
async def __aenter__(self):
|
|
|
return self
|
|
return self
|
|
@@ -32,7 +41,10 @@ class _FakeSession:
|
|
|
return self._printer
|
|
return self._printer
|
|
|
|
|
|
|
|
async def execute(self, statement):
|
|
async def execute(self, statement):
|
|
|
- return _FakeAssignmentsResult(self._assignments)
|
|
|
|
|
|
|
+ table = statement.get_final_froms()[0].name
|
|
|
|
|
+ if table == "spoolman_slot_assignments":
|
|
|
|
|
+ return _FakeAssignmentsResult(self._spoolman)
|
|
|
|
|
+ return _FakeAssignmentsResult(self._legacy)
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
@@ -75,3 +87,85 @@ async def test_missing_assignment_broadcasts_websocket_event_and_push_notificati
|
|
|
assert notify_kwargs["printer_id"] == 1
|
|
assert notify_kwargs["printer_id"] == 1
|
|
|
assert notify_kwargs["printer_name"] == "Printer A"
|
|
assert notify_kwargs["printer_name"] == "Printer A"
|
|
|
assert notify_kwargs["missing_slots"] == [{"slot": "A2", "profile": "Unknown", "color": "Unknown"}]
|
|
assert notify_kwargs["missing_slots"] == [{"slot": "A2", "profile": "Unknown", "color": "Unknown"}]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _patches(session):
|
|
|
|
|
+ """Common patch set: the fake session + stubbed printer state / emitters."""
|
|
|
|
|
+ return (
|
|
|
|
|
+ patch(
|
|
|
|
|
+ "backend.app.services.spool_assignment_notifications.async_session",
|
|
|
|
|
+ return_value=session,
|
|
|
|
|
+ ),
|
|
|
|
|
+ patch("backend.app.services.spool_assignment_notifications.printer_manager.get_status", return_value=None),
|
|
|
|
|
+ patch(
|
|
|
|
|
+ "backend.app.services.spool_assignment_notifications.ws_manager.send_missing_spool_assignment",
|
|
|
|
|
+ new_callable=AsyncMock,
|
|
|
|
|
+ ),
|
|
|
|
|
+ patch(
|
|
|
|
|
+ "backend.app.services.spool_assignment_notifications.notification_service.on_print_missing_spool_assignment",
|
|
|
|
|
+ new_callable=AsyncMock,
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_spoolman_only_assignment_suppresses_notification():
|
|
|
|
|
+ """#1473 — trays bound only via spoolman_slot_assignments must NOT be
|
|
|
|
|
+ flagged missing (the legacy spool_assignment table is empty in Spoolman
|
|
|
|
|
+ mode, so checking it alone fired a false positive on every print)."""
|
|
|
|
|
+ logger = logging.getLogger(__name__)
|
|
|
|
|
+ data = {"ams_mapping": [0, 1], "raw_data": {}} # print uses A1 + A2
|
|
|
|
|
+
|
|
|
|
|
+ # Both used trays bound via Spoolman; legacy table empty.
|
|
|
|
|
+ session = _FakeSession(
|
|
|
|
|
+ "Printer A",
|
|
|
|
|
+ legacy=[],
|
|
|
|
|
+ spoolman=[SimpleNamespace(ams_id=0, tray_id=0), SimpleNamespace(ams_id=0, tray_id=1)],
|
|
|
|
|
+ )
|
|
|
|
|
+ p_session, p_status, p_ws, p_notify = _patches(session)
|
|
|
|
|
+ with p_session, p_status, p_ws as mock_ws, p_notify as mock_notify:
|
|
|
|
|
+ await notify_missing_spool_assignments_on_print_start(1, data, logger)
|
|
|
|
|
+
|
|
|
|
|
+ mock_ws.assert_not_awaited()
|
|
|
|
|
+ mock_notify.assert_not_awaited()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_spoolman_partial_coverage_flags_only_uncovered_tray():
|
|
|
|
|
+ """A Spoolman assignment for A1 only, with a print using A1 + A2, flags
|
|
|
|
|
+ A2 alone."""
|
|
|
|
|
+ logger = logging.getLogger(__name__)
|
|
|
|
|
+ data = {"ams_mapping": [0, 1], "raw_data": {}}
|
|
|
|
|
+
|
|
|
|
|
+ session = _FakeSession(
|
|
|
|
|
+ "Printer A",
|
|
|
|
|
+ legacy=[],
|
|
|
|
|
+ spoolman=[SimpleNamespace(ams_id=0, tray_id=0)], # A1 only
|
|
|
|
|
+ )
|
|
|
|
|
+ p_session, p_status, p_ws, p_notify = _patches(session)
|
|
|
|
|
+ with p_session, p_status, p_ws as mock_ws, p_notify as mock_notify:
|
|
|
|
|
+ await notify_missing_spool_assignments_on_print_start(1, data, logger)
|
|
|
|
|
+
|
|
|
|
|
+ mock_ws.assert_awaited_once()
|
|
|
|
|
+ assert mock_ws.await_args.kwargs["missing_slots"] == [{"slot": "A2", "profile": "Unknown", "color": "Unknown"}]
|
|
|
|
|
+ mock_notify.assert_awaited_once()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_mixed_mode_union_covers_all_used_trays():
|
|
|
|
|
+ """A1 bound in the legacy table, A2 bound in spoolman_slot_assignments —
|
|
|
|
|
+ the union covers both used trays, so no notification fires."""
|
|
|
|
|
+ logger = logging.getLogger(__name__)
|
|
|
|
|
+ data = {"ams_mapping": [0, 1], "raw_data": {}}
|
|
|
|
|
+
|
|
|
|
|
+ session = _FakeSession(
|
|
|
|
|
+ "Printer A",
|
|
|
|
|
+ legacy=[SimpleNamespace(ams_id=0, tray_id=0)], # A1
|
|
|
|
|
+ spoolman=[SimpleNamespace(ams_id=0, tray_id=1)], # A2
|
|
|
|
|
+ )
|
|
|
|
|
+ p_session, p_status, p_ws, p_notify = _patches(session)
|
|
|
|
|
+ with p_session, p_status, p_ws as mock_ws, p_notify as mock_notify:
|
|
|
|
|
+ await notify_missing_spool_assignments_on_print_start(1, data, logger)
|
|
|
|
|
+
|
|
|
|
|
+ mock_ws.assert_not_awaited()
|
|
|
|
|
+ mock_notify.assert_not_awaited()
|