Fix cross-model queue items being misrepresented and editable into a broken state (#671)

The edit dialog offered a printer picker and target-model dropdown for
an item with alternatives. Saving left a row with variants AND a
printer_id, and the scheduler's fixed-printer branch wins that race, so
it dispatched a row whose library_file_id is still null and failed in
the upload. PATCH now refuses printer/model changes on such an item —
comparing against the current value, since the dialog re-sends
target_model unchanged — and the route eager-loads variants, without
which the guard could not see them and every PATCH response dropped the
alternatives from its payload.

Names come from a shared helper now. A cross-model item holds neither
archive_id nor library_file_id until dispatch, so five separate inlined
fallbacks all rendered "File #null"; they now read "x1c.gcode.3mf +1
more".

The queue also grouped these under "Any H2D" — the first candidate
mirrored onto the row — filing a job under a printer it might never run
on. It groups as "Any H2D / X1C", matching the row beneath it.
This commit is contained in:
maziggy
2026-08-03 12:33:19 +02:00
parent ef7c1b21f1
commit ea63355fde
25 changed files with 593 additions and 198 deletions
@@ -222,6 +222,58 @@ class TestQueueWithVariants:
assert r.status_code == 400
assert "No active printers" in r.json()["detail"]
@pytest.mark.asyncio
@pytest.mark.integration
async def test_assigning_a_printer_is_refused(self, async_client, db_session, sliced_file_factory, printer_factory):
"""The edit dialog offers a printer picker for every queue item. Taking it
would leave a row with variants AND a printer_id — and the fixed-printer
branch of the scheduler wins that race, dispatching a row whose
library_file_id is still null."""
printer = await printer_factory(model="H2S")
await printer_factory(model="H2C")
h2s = await sliced_file_factory("H2S")
h2c = await sliced_file_factory("H2C")
item_id = (await _queue_variants(async_client, h2s.id, h2c.id)).json()["id"]
r = await async_client.patch(f"/api/v1/queue/{item_id}", json={"printer_id": printer.id})
assert r.status_code == 400
assert "alternatives" in r.json()["detail"]
assert len(await _variants_of(db_session, item_id)) == 2, "the alternatives survive the refusal"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_narrowing_to_one_model_is_refused(self, async_client, sliced_file_factory, printer_factory):
"""Saving "Any H2C" over a two-candidate job would silently discard the
H2S alternative the user deliberately queued."""
await printer_factory(model="H2S")
await printer_factory(model="H2C")
h2s = await sliced_file_factory("H2S")
h2c = await sliced_file_factory("H2C")
item_id = (await _queue_variants(async_client, h2s.id, h2c.id)).json()["id"]
r = await async_client.patch(f"/api/v1/queue/{item_id}", json={"target_model": "H2C"})
assert r.status_code == 400
@pytest.mark.asyncio
@pytest.mark.integration
async def test_resending_the_unchanged_model_is_allowed(self, async_client, sliced_file_factory, printer_factory):
"""The edit dialog re-sends target_model on every save, so an unchanged
value must not block editing the schedule or print options."""
await printer_factory(model="H2S")
await printer_factory(model="H2C")
h2s = await sliced_file_factory("H2S")
h2c = await sliced_file_factory("H2C")
created = (await _queue_variants(async_client, h2s.id, h2c.id)).json()
r = await async_client.patch(
f"/api/v1/queue/{created['id']}",
json={"target_model": created["target_model"], "timelapse": True},
)
assert r.status_code == 200
assert r.json()["timelapse"] is True
assert len(r.json()["variants"]) == 2, "the response still carries the alternatives"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_quantity_gives_each_copy_its_own_candidates(