test_queue_nozzle_rack_choice_api_1784.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. """The rack-position pick has to survive the round trip (#1784).
  2. It did not, first time out: the field was declared on ``PrintQueueItemUpdate``
  3. and the response model but not on ``PrintQueueItemCreate``, so Pydantic dropped
  4. it from every POST without complaint. The queued item then carried no pick, the
  5. dispatcher assigned positions itself, and the print ran from hotends the
  6. operator had not chosen -- with nothing in the logs but ``chosen auto``.
  7. A silently-dropped field is invisible at every layer above it, so it is pinned
  8. here at the layer it crosses: HTTP in, database out.
  9. """
  10. import pytest
  11. from httpx import AsyncClient
  12. pytestmark = pytest.mark.integration
  13. @pytest.fixture
  14. async def printer(db_session):
  15. from backend.app.models.printer import Printer
  16. printer = Printer(
  17. name="H2C-1",
  18. ip_address="192.168.1.210",
  19. serial_number="RACKCHOICE0001",
  20. access_code="12345678",
  21. model="H2C",
  22. )
  23. db_session.add(printer)
  24. await db_session.commit()
  25. await db_session.refresh(printer)
  26. return printer
  27. @pytest.fixture
  28. async def archive(db_session, printer):
  29. from backend.app.models.archive import PrintArchive
  30. archive = PrintArchive(
  31. printer_id=printer.id,
  32. filename="benchy.gcode.3mf",
  33. file_path="archives/benchy.gcode.3mf",
  34. file_size=1024,
  35. status="completed",
  36. )
  37. db_session.add(archive)
  38. await db_session.commit()
  39. await db_session.refresh(archive)
  40. return archive
  41. async def _stored_choice(db_session, item_id):
  42. """What actually landed in the column, not what the response echoed."""
  43. from backend.app.models.print_queue import PrintQueueItem
  44. db_session.expire_all()
  45. item = await db_session.get(PrintQueueItem, item_id)
  46. return item.nozzle_rack_choice
  47. @pytest.mark.asyncio
  48. class TestCreate:
  49. async def test_a_pick_posted_on_create_reaches_the_column(
  50. self, async_client: AsyncClient, printer, archive, db_session
  51. ):
  52. response = await async_client.post(
  53. "/api/v1/queue/",
  54. json={
  55. "printer_id": printer.id,
  56. "archive_id": archive.id,
  57. # Group 2 to rack position 1, group 1 to position 3 -- the pick
  58. # BambuStudio dispatched as [16, 1, 18] on 2026-08-13.
  59. "nozzle_rack_choice": {"2": 1, "1": 3},
  60. },
  61. )
  62. assert response.status_code == 200
  63. result = response.json()
  64. assert result["nozzle_rack_choice"] == {"2": 1, "1": 3}
  65. assert await _stored_choice(db_session, result["id"]) is not None
  66. async def test_creating_without_one_leaves_the_column_null(
  67. self, async_client: AsyncClient, printer, archive, db_session
  68. ):
  69. """Null is the signal to assign positions at dispatch."""
  70. response = await async_client.post("/api/v1/queue/", json={"printer_id": printer.id, "archive_id": archive.id})
  71. assert response.status_code == 200
  72. assert response.json()["nozzle_rack_choice"] is None
  73. assert await _stored_choice(db_session, response.json()["id"]) is None
  74. @pytest.mark.asyncio
  75. class TestUpdate:
  76. async def test_editing_an_item_replaces_its_pick(self, async_client: AsyncClient, printer, archive, db_session):
  77. created = await async_client.post(
  78. "/api/v1/queue/",
  79. json={
  80. "printer_id": printer.id,
  81. "archive_id": archive.id,
  82. "nozzle_rack_choice": {"2": 1, "1": 3},
  83. },
  84. )
  85. item_id = created.json()["id"]
  86. response = await async_client.patch(f"/api/v1/queue/{item_id}", json={"nozzle_rack_choice": {"2": 1, "1": 2}})
  87. assert response.status_code == 200
  88. assert response.json()["nozzle_rack_choice"] == {"2": 1, "1": 2}
  89. async def test_clearing_the_pick_hands_the_choice_back_to_the_dispatcher(
  90. self, async_client: AsyncClient, printer, archive, db_session
  91. ):
  92. created = await async_client.post(
  93. "/api/v1/queue/",
  94. json={
  95. "printer_id": printer.id,
  96. "archive_id": archive.id,
  97. "nozzle_rack_choice": {"2": 1, "1": 3},
  98. },
  99. )
  100. item_id = created.json()["id"]
  101. response = await async_client.patch(f"/api/v1/queue/{item_id}", json={"nozzle_rack_choice": None})
  102. assert response.status_code == 200
  103. assert response.json()["nozzle_rack_choice"] is None
  104. assert await _stored_choice(db_session, item_id) is None
  105. async def test_an_unrelated_edit_does_not_disturb_the_pick(
  106. self, async_client: AsyncClient, printer, archive, db_session
  107. ):
  108. created = await async_client.post(
  109. "/api/v1/queue/",
  110. json={
  111. "printer_id": printer.id,
  112. "archive_id": archive.id,
  113. "nozzle_rack_choice": {"2": 1, "1": 3},
  114. },
  115. )
  116. item_id = created.json()["id"]
  117. response = await async_client.patch(f"/api/v1/queue/{item_id}", json={"manual_start": True})
  118. assert response.status_code == 200
  119. assert response.json()["nozzle_rack_choice"] == {"2": 1, "1": 3}
  120. class TestSchemaCoverage:
  121. def test_create_update_and_response_all_declare_the_field(self):
  122. """The original bug in one assertion: it was on two of the three.
  123. A field missing from a request schema is dropped in silence, so there is
  124. no error anywhere to catch it -- only a print that runs from the wrong
  125. hotend.
  126. """
  127. from backend.app.schemas.print_queue import (
  128. PrintQueueItemCreate,
  129. PrintQueueItemResponse,
  130. PrintQueueItemUpdate,
  131. QueueVariantCreate,
  132. )
  133. for schema in (PrintQueueItemCreate, PrintQueueItemUpdate, PrintQueueItemResponse, QueueVariantCreate):
  134. assert "nozzle_rack_choice" in schema.model_fields, schema.__name__
  135. def test_the_create_schema_actually_keeps_a_posted_pick(self):
  136. from backend.app.schemas.print_queue import PrintQueueItemCreate
  137. parsed = PrintQueueItemCreate(printer_id=1, archive_id=1, nozzle_rack_choice={"2": 1, "1": 3})
  138. assert parsed.nozzle_rack_choice == {2: 1, 1: 3}