|
@@ -1226,6 +1226,221 @@ class TestAssignSpoolEmptyDetection:
|
|
|
assert body["configured"] is True
|
|
assert body["configured"] is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class TestAssignSpoolPresenceBit:
|
|
|
|
|
+ """#3084: the slot the firmware says is full, and the cache says is empty.
|
|
|
|
|
+
|
|
|
|
|
+ ``apply_tray_exist_bits`` stamps ``state = 9`` on every slot whose
|
|
|
|
|
+ ``tray_exist_bits`` bit is 0 and annotates ``exists`` on every slot it
|
|
|
|
|
+ looks at — but when the bit comes back it only refreshes ``exists`` and
|
|
|
|
|
+ leaves the 9 where it was. Swap a Bambu spool for a non-RFID one and the
|
|
|
|
|
+ slot sits at ``exists=True, state=9`` until something configures it.
|
|
|
|
|
+
|
|
|
|
|
+ Reported on an H2D/H2C AMS-HT: remove the Bambu spool (bits ``f``), insert
|
|
|
|
|
+ a third-party one 9 seconds later (bits ``1000f``), then Assign Spool 28
|
|
|
|
|
+ seconds after that — and no ``ams_filament_setting`` was published at all.
|
|
|
|
|
+ Configure worked, because it publishes unconditionally.
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_the_bit_overrules_a_stale_empty_state(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory, spool_factory
|
|
|
|
|
+ ):
|
|
|
|
|
+ """exists=True with a leftover state=9 — MQTT must fire."""
|
|
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
|
|
+ spool = await spool_factory(slicer_filament="GFL05", material="PLA")
|
|
|
|
|
+
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.ams_set_filament_setting.return_value = True
|
|
|
|
|
+ mock_client.extrusion_cali_sel.return_value = True
|
|
|
|
|
+
|
|
|
|
|
+ tray_data = {"id": 0, "state": 9, "exists": True, "tray_type": "", "tray_color": "", "tray_info_idx": ""}
|
|
|
|
|
+ status = _make_mock_status(ams_data=[{"id": 128, "tray": [tray_data]}])
|
|
|
|
|
+
|
|
|
|
|
+ with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
|
|
|
|
|
+ mock_pm.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm.get_status.return_value = status
|
|
|
|
|
+
|
|
|
|
|
+ response = await async_client.post(
|
|
|
|
|
+ "/api/v1/inventory/assignments",
|
|
|
|
|
+ json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 128, "tray_id": 0},
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert response.status_code == 200
|
|
|
|
|
+ mock_client.ams_set_filament_setting.assert_called_once()
|
|
|
|
|
+ body = response.json()
|
|
|
|
|
+ assert body["configured"] is True
|
|
|
|
|
+ assert body["pending_config"] is False
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_an_empty_bit_does_not_start_suppressing_pushes(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory, spool_factory
|
|
|
|
|
+ ):
|
|
|
|
|
+ """The bit overrules the 9 and nothing else.
|
|
|
|
|
+
|
|
|
|
|
+ Reading it the other way too would be tidier — skip the push firmware
|
|
|
|
|
+ is going to drop — but it also means a slot that silently stops
|
|
|
|
|
+ configuring on whichever AMS variant we compute the bit position
|
|
|
|
|
+ wrong for. The saving is one MQTT message; the failure is the bug
|
|
|
|
|
+ this commit is fixing, inverted. So a state that does not say "empty"
|
|
|
|
|
+ still publishes, exactly as it did before.
|
|
|
|
|
+ """
|
|
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
|
|
+ spool = await spool_factory(slicer_filament="GFL05", material="PLA")
|
|
|
|
|
+
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.ams_set_filament_setting.return_value = True
|
|
|
|
|
+ mock_client.extrusion_cali_sel.return_value = True
|
|
|
|
|
+
|
|
|
|
|
+ tray_data = {"id": 3, "state": 11, "exists": False, "tray_type": "", "tray_color": ""}
|
|
|
|
|
+ status = _make_mock_status(ams_data=[{"id": 2, "tray": [tray_data]}])
|
|
|
|
|
+
|
|
|
|
|
+ with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
|
|
|
|
|
+ mock_pm.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm.get_status.return_value = status
|
|
|
|
|
+
|
|
|
|
|
+ response = await async_client.post(
|
|
|
|
|
+ "/api/v1/inventory/assignments",
|
|
|
|
|
+ json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 2, "tray_id": 3},
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert response.status_code == 200
|
|
|
|
|
+ mock_client.ams_set_filament_setting.assert_called_once()
|
|
|
|
|
+ assert response.json()["pending_config"] is False
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_the_pre_assign_workflow_still_skips_a_genuinely_empty_slot(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory, spool_factory
|
|
|
|
|
+ ):
|
|
|
|
|
+ """SpoolBuddy weighs a spool and assigns it before it goes in. Bit
|
|
|
|
|
+ clear and state 9 agree that the slot is empty, so the push is still
|
|
|
|
|
+ deferred to the replay — unchanged."""
|
|
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
|
|
+ spool = await spool_factory(slicer_filament="GFL05", material="PLA")
|
|
|
|
|
+
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.ams_set_filament_setting.return_value = True
|
|
|
|
|
+
|
|
|
|
|
+ tray_data = {"id": 3, "state": 9, "exists": False, "tray_type": "", "tray_color": ""}
|
|
|
|
|
+ status = _make_mock_status(ams_data=[{"id": 2, "tray": [tray_data]}])
|
|
|
|
|
+
|
|
|
|
|
+ with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
|
|
|
|
|
+ mock_pm.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm.get_status.return_value = status
|
|
|
|
|
+
|
|
|
|
|
+ response = await async_client.post(
|
|
|
|
|
+ "/api/v1/inventory/assignments",
|
|
|
|
|
+ json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 2, "tray_id": 3},
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert response.status_code == 200
|
|
|
|
|
+ mock_client.ams_set_filament_setting.assert_not_called()
|
|
|
|
|
+ body = response.json()
|
|
|
|
|
+ assert body["configured"] is False
|
|
|
|
|
+ assert body["pending_config"] is True
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_an_unannotated_tray_still_reads_the_state(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory, spool_factory
|
|
|
|
|
+ ):
|
|
|
|
|
+ """No presence bit in the payload → the 9/10 heuristic still decides.
|
|
|
|
|
+
|
|
|
|
|
+ The external spool's ``vt_tray`` has no bit in the mask, and neither do
|
|
|
|
|
+ the hand-built payloads every other test in this file uses.
|
|
|
|
|
+ """
|
|
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
|
|
+ spool = await spool_factory(slicer_filament="GFL05", material="PLA")
|
|
|
|
|
+
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.ams_set_filament_setting.return_value = True
|
|
|
|
|
+
|
|
|
|
|
+ status = _make_mock_status(ams_data=[{"id": 2, "tray": [{"id": 3, "state": 9, "tray_type": ""}]}])
|
|
|
|
|
+
|
|
|
|
|
+ with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
|
|
|
|
|
+ mock_pm.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm.get_status.return_value = status
|
|
|
|
|
+
|
|
|
|
|
+ response = await async_client.post(
|
|
|
|
|
+ "/api/v1/inventory/assignments",
|
|
|
|
|
+ json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 2, "tray_id": 3},
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert response.status_code == 200
|
|
|
|
|
+ mock_client.ams_set_filament_setting.assert_not_called()
|
|
|
|
|
+ assert response.json()["pending_config"] is True
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_deferred_config_fires_for_a_spool_the_ams_cannot_name(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory, spool_factory, db_session: AsyncSession
|
|
|
|
|
+ ):
|
|
|
|
|
+ """The pre-assign workflow's half of the same bug.
|
|
|
|
|
+
|
|
|
|
|
+ A non-RFID spool inserted into a pre-assigned slot brings no
|
|
|
|
|
+ ``tray_type`` with it, and the stale 9 kept the replay's "loaded" test
|
|
|
|
|
+ false, so the deferred configuration never fired for it either. The
|
|
|
|
|
+ presence bit is the only thing in the payload that changed.
|
|
|
|
|
+ """
|
|
|
|
|
+ from unittest.mock import AsyncMock
|
|
|
|
|
+
|
|
|
|
|
+ from backend.app.main import on_ams_change
|
|
|
|
|
+ from backend.app.models.spool_assignment import SpoolAssignment
|
|
|
|
|
+
|
|
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
|
|
+ spool = await spool_factory(slicer_filament="GFL05", material="PLA")
|
|
|
|
|
+
|
|
|
|
|
+ pre_assignment = SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=2,
|
|
|
|
|
+ tray_id=3,
|
|
|
|
|
+ fingerprint_color=None,
|
|
|
|
|
+ fingerprint_type=None,
|
|
|
|
|
+ )
|
|
|
|
|
+ db_session.add(pre_assignment)
|
|
|
|
|
+ await db_session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ ams_data = [{"id": 2, "tray": [{"id": 3, "state": 9, "exists": True, "tray_type": "", "tray_color": ""}]}]
|
|
|
|
|
+
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.ams_set_filament_setting.return_value = True
|
|
|
|
|
+ mock_client.extrusion_cali_sel.return_value = True
|
|
|
|
|
+
|
|
|
|
|
+ status = _make_mock_status(ams_data=ams_data)
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.main.printer_manager") as mock_pm_main,
|
|
|
|
|
+ patch("backend.app.services.printer_manager.printer_manager") as mock_pm_inv,
|
|
|
|
|
+ patch("backend.app.main.mqtt_relay") as mock_relay,
|
|
|
|
|
+ patch("backend.app.main.ws_manager") as mock_ws,
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_pm_main.get_printer.return_value = MagicMock(name="H2D", serial_number="0948BB540200427")
|
|
|
|
|
+ mock_pm_main.get_status.return_value = status
|
|
|
|
|
+ mock_pm_main.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm_main.get_model.return_value = "H2D"
|
|
|
|
|
+ mock_pm_inv.get_client.return_value = mock_client
|
|
|
|
|
+ mock_pm_inv.get_status.return_value = status
|
|
|
|
|
+ mock_relay.on_ams_change = AsyncMock()
|
|
|
|
|
+ mock_ws.send_printer_status = AsyncMock()
|
|
|
|
|
+ mock_ws.broadcast = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ await on_ams_change(printer.id, ams_data)
|
|
|
|
|
+
|
|
|
|
|
+ mock_client.ams_set_filament_setting.assert_called_once()
|
|
|
|
|
+ call_kwargs = mock_client.ams_set_filament_setting.call_args.kwargs
|
|
|
|
|
+ assert call_kwargs["ams_id"] == 2
|
|
|
|
|
+ assert call_kwargs["tray_id"] == 3
|
|
|
|
|
+ assert call_kwargs["tray_info_idx"] == "GFL05"
|
|
|
|
|
+
|
|
|
|
|
+ # The assignment is still there — the pass that fires the config is the
|
|
|
|
|
+ # same pass that deletes stale ones (#3100).
|
|
|
|
|
+ db_session.expunge_all()
|
|
|
|
|
+ assert await db_session.get(SpoolAssignment, pre_assignment.id) is not None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class TestAssignSpoolPfcnCloudPreset:
|
|
class TestAssignSpoolPfcnCloudPreset:
|
|
|
"""Assign path for PFCN-prefix cloud presets (#1648).
|
|
"""Assign path for PFCN-prefix cloud presets (#1648).
|
|
|
|
|
|
|
@@ -1561,6 +1776,118 @@ class TestAutoUnlinkDuringRunout:
|
|
|
assert await db_session.get(SpoolAssignment, assignment_id) is not None
|
|
assert await db_session.get(SpoolAssignment, assignment_id) is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class TestAutoUnlinkOccupiedSlot:
|
|
|
|
|
+ """#3100: six saved assignments deleted across three X1 Carbons.
|
|
|
|
|
+
|
|
|
|
|
+ Each one had an explicit earlier assignment and then an ``Auto-unlink ...
|
|
|
|
|
+ fingerprint mismatch``, and the reporter recovered the mappings from logs
|
|
|
|
|
+ because they were gone from the inventory API, not merely hidden. Two
|
|
|
|
|
+ shapes, one cause: a slot the presence bit calls occupied while the tray
|
|
|
|
|
+ reports nothing about what is in it.
|
|
|
|
|
+
|
|
|
|
|
+ ``cur=/ fp=BCBCBCFF/PLA spool=8A8F92FF/PLA`` — an established assignment,
|
|
|
|
|
+ a blank idle report, and the row deleted. The spool never went anywhere;
|
|
|
|
|
+ the AMS simply had nothing to say about a filament it cannot read.
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ async def _run(printer_id, ams_data, status):
|
|
|
|
|
+ from unittest.mock import AsyncMock
|
|
|
|
|
+
|
|
|
|
|
+ from backend.app.main import on_ams_change
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("backend.app.main.printer_manager") as mock_pm_main,
|
|
|
|
|
+ patch("backend.app.main.mqtt_relay") as mock_relay,
|
|
|
|
|
+ patch("backend.app.main.ws_manager") as mock_ws,
|
|
|
|
|
+ ):
|
|
|
|
|
+ mock_pm_main.get_printer.return_value = MagicMock(name="X1C", serial_number="0948BB540200427")
|
|
|
|
|
+ mock_pm_main.get_status.return_value = status
|
|
|
|
|
+ mock_pm_main.get_model.return_value = "X1C"
|
|
|
|
|
+ mock_pm_main.get_client.return_value = None
|
|
|
|
|
+ mock_relay.on_ams_change = AsyncMock()
|
|
|
|
|
+ mock_ws.send_printer_status = AsyncMock()
|
|
|
|
|
+ mock_ws.broadcast = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ await on_ams_change(printer_id, ams_data)
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ async def _assignment(db_session, printer, spool):
|
|
|
|
|
+ from backend.app.models.spool_assignment import SpoolAssignment
|
|
|
|
|
+
|
|
|
|
|
+ assignment = SpoolAssignment(
|
|
|
|
|
+ spool_id=spool.id,
|
|
|
|
|
+ printer_id=printer.id,
|
|
|
|
|
+ ams_id=1,
|
|
|
|
|
+ tray_id=1,
|
|
|
|
|
+ fingerprint_color="BCBCBCFF",
|
|
|
|
|
+ fingerprint_type="PLA",
|
|
|
|
|
+ )
|
|
|
|
|
+ db_session.add(assignment)
|
|
|
|
|
+ await db_session.commit()
|
|
|
|
|
+ return assignment.id
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_a_blank_report_from_an_occupied_slot_keeps_the_assignment(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory, spool_factory, db_session: AsyncSession
|
|
|
|
|
+ ):
|
|
|
|
|
+ from backend.app.models.spool_assignment import SpoolAssignment
|
|
|
|
|
+
|
|
|
|
|
+ printer = await printer_factory(name="X1C")
|
|
|
|
|
+ spool = await spool_factory(material="PLA", rgba="8A8F92FF")
|
|
|
|
|
+ assignment_id = await self._assignment(db_session, printer, spool)
|
|
|
|
|
+
|
|
|
|
|
+ ams_data = [{"id": 1, "tray": [{"id": 1, "exists": True, "tray_type": "", "tray_color": "", "state": 9}]}]
|
|
|
|
|
+ await self._run(printer.id, ams_data, _make_printing_status(ams_data, state="IDLE"))
|
|
|
|
|
+
|
|
|
|
|
+ db_session.expunge_all()
|
|
|
|
|
+ assert await db_session.get(SpoolAssignment, assignment_id) is not None, (
|
|
|
|
|
+ "a spool the AMS cannot identify is not a spool that was removed"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_a_blank_report_from_an_empty_slot_still_unlinks(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory, spool_factory, db_session: AsyncSession
|
|
|
|
|
+ ):
|
|
|
|
|
+ """The guard reads the bit, not the blankness — take the spool out and
|
|
|
|
|
+ the assignment still goes, which is what makes the test above a
|
|
|
|
|
+ distinction rather than a blanket reprieve."""
|
|
|
|
|
+ from backend.app.models.spool_assignment import SpoolAssignment
|
|
|
|
|
+
|
|
|
|
|
+ printer = await printer_factory(name="X1C")
|
|
|
|
|
+ spool = await spool_factory(material="PLA", rgba="8A8F92FF")
|
|
|
|
|
+ assignment_id = await self._assignment(db_session, printer, spool)
|
|
|
|
|
+
|
|
|
|
|
+ ams_data = [{"id": 1, "tray": [{"id": 1, "exists": False, "tray_type": "", "tray_color": "", "state": 9}]}]
|
|
|
|
|
+ await self._run(printer.id, ams_data, _make_printing_status(ams_data, state="IDLE"))
|
|
|
|
|
+
|
|
|
|
|
+ db_session.expunge_all()
|
|
|
|
|
+ assert await db_session.get(SpoolAssignment, assignment_id) is None
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_a_genuinely_different_filament_in_an_occupied_slot_still_unlinks(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory, spool_factory, db_session: AsyncSession
|
|
|
|
|
+ ):
|
|
|
|
|
+ """The guard is for a blank report only. A slot that names a filament
|
|
|
|
|
+ which is not the assigned spool is a swap, bit set or not."""
|
|
|
|
|
+ from backend.app.models.spool_assignment import SpoolAssignment
|
|
|
|
|
+
|
|
|
|
|
+ printer = await printer_factory(name="X1C")
|
|
|
|
|
+ spool = await spool_factory(material="PLA", rgba="8A8F92FF")
|
|
|
|
|
+ assignment_id = await self._assignment(db_session, printer, spool)
|
|
|
|
|
+
|
|
|
|
|
+ ams_data = [
|
|
|
|
|
+ {"id": 1, "tray": [{"id": 1, "exists": True, "tray_type": "PETG", "tray_color": "00FF00FF", "state": 11}]}
|
|
|
|
|
+ ]
|
|
|
|
|
+ await self._run(printer.id, ams_data, _make_printing_status(ams_data, state="IDLE"))
|
|
|
|
|
+
|
|
|
|
|
+ db_session.expunge_all()
|
|
|
|
|
+ assert await db_session.get(SpoolAssignment, assignment_id) is None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class TestSpoolmanSlotAssignmentDuringRunout:
|
|
class TestSpoolmanSlotAssignmentDuringRunout:
|
|
|
"""`spoolman_slot_assignments` is how a tag-less spool assigned through the
|
|
"""`spoolman_slot_assignments` is how a tag-less spool assigned through the
|
|
|
Bambuddy UI is resolved at completion (#1459). Deleting the row when a slot
|
|
Bambuddy UI is resolved at completion (#1459). Deleting the row when a slot
|
|
@@ -1578,14 +1905,14 @@ class TestSpoolmanSlotAssignmentDuringRunout:
|
|
|
db_session.add(Settings(key=key, value=value))
|
|
db_session.add(Settings(key=key, value=value))
|
|
|
await db_session.commit()
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
- async def _run(self, printer_id, state):
|
|
|
|
|
|
|
+ async def _run(self, printer_id, state, tray=None):
|
|
|
from unittest.mock import AsyncMock
|
|
from unittest.mock import AsyncMock
|
|
|
|
|
|
|
|
from backend.app.main import on_ams_change
|
|
from backend.app.main import on_ams_change
|
|
|
|
|
|
|
|
# A tray the firmware has cleared: parse_ams_tray returns None, which
|
|
# A tray the firmware has cleared: parse_ams_tray returns None, which
|
|
|
# is what marks the slot empty for the cleanup pass.
|
|
# is what marks the slot empty for the cleanup pass.
|
|
|
- ams_data = [{"id": 0, "tray": [{"id": 2, "tray_type": "", "tray_color": "", "state": 26}]}]
|
|
|
|
|
|
|
+ ams_data = [{"id": 0, "tray": [tray or {"id": 2, "tray_type": "", "tray_color": "", "state": 26}]}]
|
|
|
|
|
|
|
|
spoolman_client = MagicMock()
|
|
spoolman_client = MagicMock()
|
|
|
spoolman_client.health_check = AsyncMock(return_value=True)
|
|
spoolman_client.health_check = AsyncMock(return_value=True)
|
|
@@ -1628,6 +1955,37 @@ class TestSpoolmanSlotAssignmentDuringRunout:
|
|
|
db_session.expunge_all()
|
|
db_session.expunge_all()
|
|
|
assert await db_session.get(SpoolmanSlotAssignment, row_id) is not None
|
|
assert await db_session.get(SpoolmanSlotAssignment, row_id) is not None
|
|
|
|
|
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @pytest.mark.integration
|
|
|
|
|
+ async def test_an_occupied_slot_the_ams_cannot_read_keeps_its_row(
|
|
|
|
|
+ self, async_client: AsyncClient, printer_factory, db_session: AsyncSession
|
|
|
|
|
+ ):
|
|
|
|
|
+ """Spoolman mode's half of #3100.
|
|
|
|
|
+
|
|
|
|
|
+ parse_ams_tray calls a tray with no type empty, and a tag-less spool
|
|
|
|
|
+ has none until something configures it — so the row assigned through
|
|
|
|
|
+ the UI was deleted by the first idle push after the spool went in.
|
|
|
|
|
+ Firmware's presence bit is the same answer the built-in inventory
|
|
|
|
|
+ uses, so the two modes stay in step.
|
|
|
|
|
+ """
|
|
|
|
|
+ from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
|
|
|
+
|
|
|
|
|
+ await self._enable_spoolman(db_session)
|
|
|
|
|
+ printer = await printer_factory(name="H2D")
|
|
|
|
|
+ row = SpoolmanSlotAssignment(printer_id=printer.id, ams_id=0, tray_id=2, spoolman_spool_id=41)
|
|
|
|
|
+ db_session.add(row)
|
|
|
|
|
+ await db_session.commit()
|
|
|
|
|
+ row_id = row.id
|
|
|
|
|
+
|
|
|
|
|
+ await self._run(
|
|
|
|
|
+ printer.id,
|
|
|
|
|
+ _make_printing_status(None, state="IDLE"),
|
|
|
|
|
+ tray={"id": 2, "exists": True, "tray_type": "", "tray_color": "", "state": 9},
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ db_session.expunge_all()
|
|
|
|
|
+ assert await db_session.get(SpoolmanSlotAssignment, row_id) is not None
|
|
|
|
|
+
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.integration
|
|
|
async def test_the_slot_row_is_still_cleaned_up_when_idle(
|
|
async def test_the_slot_row_is_still_cleaned_up_when_idle(
|