test_use_ams_reconcile_2595.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. """use_ams must be reconciled against the resolved ams_mapping at dispatch (#2595).
  2. A print sent to a Virtual Printer is sliced against the VP, which advertises no
  3. AMS, so the slicer sends ``use_ams=false`` and that flag is stamped onto the
  4. queue item. But an "Any [model]" queue item is colour-matched to a real printer
  5. at dispatch, resolving a *real* AMS slot in ``ams_mapping``. The stale
  6. ``use_ams=False`` used to survive to the print command, so the printer ignored
  7. the mapped slot and aborted at layer 0 on the empty external spool ("not enough
  8. filament"). Diagnosed by @Sawtaytoes.
  9. The command builder now treats the mapping as authoritative for single-nozzle
  10. printers: a real tray forces ``use_ams=True``; an explicit external selection
  11. forces it False; an unresolved ``-1`` mapping (#2589) does neither. Dual-nozzle
  12. is untouched (``use_ams`` is nozzle routing there).
  13. """
  14. import json
  15. from unittest.mock import MagicMock
  16. import pytest
  17. from backend.app.services.bambu_mqtt import BambuMQTTClient
  18. class TestUseAmsReconcile:
  19. @pytest.fixture
  20. def mqtt_client(self):
  21. client = BambuMQTTClient(
  22. ip_address="192.168.1.100",
  23. serial_number="01P00A452600691",
  24. access_code="12345678",
  25. )
  26. # Single-nozzle X1C so the dual-nozzle bypass does not apply.
  27. client.model = "X1C"
  28. client._client = MagicMock()
  29. client.state.connected = True
  30. return client
  31. def _sent_command(self, mqtt_client) -> dict:
  32. assert mqtt_client._client.publish.called, "start_print did not publish"
  33. payload = mqtt_client._client.publish.call_args.args[1]
  34. return json.loads(payload)["print"]
  35. def test_vp_false_with_real_tray_forces_use_ams_true(self, mqtt_client):
  36. """The reported bug: use_ams=False (VP-stamped) + a real AMS slot -> True."""
  37. assert mqtt_client.start_print("shell.3mf", ams_mapping=[4], use_ams=False) is True
  38. cmd = self._sent_command(mqtt_client)
  39. assert cmd["use_ams"] is True
  40. def test_false_with_padded_real_tray_forces_true(self, mqtt_client):
  41. """A padded mapping ([-1, -1, tray]) still has a real slot -> True."""
  42. assert mqtt_client.start_print("shell.3mf", ams_mapping=[-1, -1, 5], use_ams=False) is True
  43. cmd = self._sent_command(mqtt_client)
  44. assert cmd["use_ams"] is True
  45. def test_the_schedulers_plate_answer_is_not_overridden(self, mqtt_client):
  46. """#3087 on a multi-plate file: this plate prints only from the spool
  47. holder; the real tray belongs to another plate."""
  48. assert mqtt_client.start_print("shell.3mf", ams_mapping=[5, 254], use_ams=False, plate_uses_only_external=True)
  49. assert self._sent_command(mqtt_client)["use_ams"] is False
  50. def test_false_all_external_stays_false(self, mqtt_client):
  51. """An explicit external selection must NOT be force-enabled."""
  52. assert mqtt_client.start_print("shell.3mf", ams_mapping=[254], use_ams=False) is True
  53. cmd = self._sent_command(mqtt_client)
  54. assert cmd["use_ams"] is False
  55. def test_false_mixed_external_stays_false(self, mqtt_client):
  56. assert mqtt_client.start_print("shell.3mf", ams_mapping=[255, 254], use_ams=False) is True
  57. cmd = self._sent_command(mqtt_client)
  58. assert cmd["use_ams"] is False
  59. def test_false_unresolved_stays_false(self, mqtt_client):
  60. """An unresolved [-1] is neither external nor a real tray — leave it alone
  61. (preserves the #2589 contract; it should have been recomputed upstream)."""
  62. assert mqtt_client.start_print("shell.3mf", ams_mapping=[-1], use_ams=False) is True
  63. cmd = self._sent_command(mqtt_client)
  64. assert cmd["use_ams"] is False
  65. def test_true_all_external_still_downgrades(self, mqtt_client):
  66. """The original #2589 all-external downgrade still fires."""
  67. assert mqtt_client.start_print("shell.3mf", ams_mapping=[254], use_ams=True) is True
  68. cmd = self._sent_command(mqtt_client)
  69. assert cmd["use_ams"] is False
  70. def test_true_real_tray_stays_true(self, mqtt_client):
  71. assert mqtt_client.start_print("shell.3mf", ams_mapping=[5], use_ams=True) is True
  72. cmd = self._sent_command(mqtt_client)
  73. assert cmd["use_ams"] is True
  74. def test_dual_nozzle_use_ams_untouched(self, mqtt_client):
  75. """Dual-nozzle: use_ams is nozzle routing, not an AMS flag — never coerced."""
  76. mqtt_client._is_dual_nozzle = True
  77. assert mqtt_client.start_print("shell.3mf", ams_mapping=[4], use_ams=False) is True
  78. cmd = self._sent_command(mqtt_client)
  79. assert cmd["use_ams"] is False