Merge remote-tracking branch 'upstream/dev' into feature/billing

This commit is contained in:
behrinml
2026-08-05 20:19:32 +02:00
215 changed files with 28788 additions and 2152 deletions
@@ -438,6 +438,211 @@ class TestPrintQueueAPI:
assert result["archive_id"] == archive.id
assert result["ams_mapping"] == [5, -1, 2, -1]
@pytest.mark.asyncio
@pytest.mark.integration
async def test_add_to_queue_falls_back_to_archive_slicer_ams_mapping_when_unset(
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
):
"""When the caller sends no explicit ams_mapping, but the archive
carries the slicer's own saved pick for this exact printer
(extra_data.slicer_ams_mapping, written by a VP with "Save AMS
mapping" on), the queue item should inherit it — the same
exact-physical-spool reuse the "Mapping" button gives you, but
automatic when nothing was hand-edited.
"""
printer = await printer_factory()
archive = await archive_factory(
extra_data={"slicer_ams_mapping": {"mapping": [5, -1, 2, -1], "printer_id": printer.id}}
)
data = {
"printer_id": printer.id,
"archive_id": archive.id,
}
response = await async_client.post("/api/v1/queue/", json=data)
assert response.status_code == 200
result = response.json()
assert result["ams_mapping"] == [5, -1, 2, -1]
@pytest.mark.asyncio
@pytest.mark.integration
async def test_add_to_queue_ignores_archive_slicer_ams_mapping_for_different_printer(
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
):
"""A saved mapping's tray IDs only mean something relative to the
printer they were resolved against. Reprinting the same archive on a
*different* printer must not inherit it — tray 5 on printer A can
hold a completely different spool than tray 5 on printer B.
"""
origin_printer = await printer_factory()
other_printer = await printer_factory()
archive = await archive_factory(
extra_data={"slicer_ams_mapping": {"mapping": [5, -1, 2, -1], "printer_id": origin_printer.id}}
)
data = {
"printer_id": other_printer.id,
"archive_id": archive.id,
}
response = await async_client.post("/api/v1/queue/", json=data)
assert response.status_code == 200
result = response.json()
assert result["ams_mapping"] is None
@pytest.mark.asyncio
@pytest.mark.integration
async def test_add_to_queue_ignores_archive_slicer_ams_mapping_for_model_based_dispatch(
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
):
"""A model-based item (no fixed printer_id) can't know in advance
which printer the scheduler will pick, so a saved mapping resolved
against one specific printer must never be inherited here either.
"""
origin_printer = await printer_factory()
archive = await archive_factory(
extra_data={"slicer_ams_mapping": {"mapping": [5, -1, 2, -1], "printer_id": origin_printer.id}}
)
data = {
"target_model": "X1C",
"archive_id": archive.id,
}
response = await async_client.post("/api/v1/queue/", json=data)
assert response.status_code == 200
result = response.json()
assert result["ams_mapping"] is None
@pytest.mark.asyncio
@pytest.mark.integration
async def test_add_to_queue_explicit_ams_mapping_wins_over_archive_fallback(
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
):
"""An explicit ams_mapping in the request (e.g. from the filament
mapping panel) must take priority over the archive's saved slicer
pick — the fallback only fires when the caller sent nothing at all.
"""
printer = await printer_factory()
archive = await archive_factory(
extra_data={"slicer_ams_mapping": {"mapping": [5, -1, 2, -1], "printer_id": printer.id}}
)
data = {
"printer_id": printer.id,
"archive_id": archive.id,
"ams_mapping": [9, -1, 1, -1],
}
response = await async_client.post("/api/v1/queue/", json=data)
assert response.status_code == 200
result = response.json()
assert result["ams_mapping"] == [9, -1, 1, -1]
@pytest.mark.asyncio
@pytest.mark.integration
async def test_add_to_queue_archive_extra_data_without_slicer_mapping_key_not_used(
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
):
"""extra_data present but without a slicer_ams_mapping key (the
common case — most archives have other metadata but no saved slicer
mapping) must not accidentally trip the fallback."""
printer = await printer_factory()
archive = await archive_factory(extra_data={"filament_slots": []})
data = {
"printer_id": printer.id,
"archive_id": archive.id,
}
response = await async_client.post("/api/v1/queue/", json=data)
assert response.status_code == 200
result = response.json()
assert result["ams_mapping"] is None
@pytest.mark.asyncio
@pytest.mark.integration
async def test_add_to_queue_force_color_match_overrides_beat_the_archive_fallback(
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
):
"""Force-color-match overrides are the caller asking the scheduler to
match strictly against the printer's live trays, and they are only ever
applied inside `_compute_ams_mapping_for_printer` — the function a
stored mapping makes the scheduler skip. Inheriting the saved mapping
here would silently retire the strictness that was just requested
(#2700 review).
"""
printer = await printer_factory()
archive = await archive_factory(
extra_data={"slicer_ams_mapping": {"mapping": [5, -1, 2, -1], "printer_id": printer.id}}
)
data = {
"printer_id": printer.id,
"archive_id": archive.id,
"filament_overrides": [
{"slot_id": 1, "type": "PLA", "color": "#FF0000", "force_color_match": True},
],
}
response = await async_client.post("/api/v1/queue/", json=data)
assert response.status_code == 200
result = response.json()
assert result["ams_mapping"] is None
@pytest.mark.asyncio
@pytest.mark.integration
async def test_add_to_queue_plain_overrides_still_allow_the_archive_fallback(
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
):
"""Only force_color_match stands the fallback down. A plain preference
override is a filament swap, not a request for live colour matching, so
the saved mapping is still the best starting point.
"""
printer = await printer_factory()
archive = await archive_factory(
extra_data={"slicer_ams_mapping": {"mapping": [5, -1, 2, -1], "printer_id": printer.id}}
)
data = {
"printer_id": printer.id,
"archive_id": archive.id,
"filament_overrides": [{"slot_id": 1, "type": "PLA", "color": "#FF0000"}],
}
response = await async_client.post("/api/v1/queue/", json=data)
assert response.status_code == 200
result = response.json()
assert result["ams_mapping"] == [5, -1, 2, -1]
@pytest.mark.asyncio
@pytest.mark.integration
async def test_queue_response_flags_saved_mapping_only_for_its_own_printer(
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
):
"""`archive_has_slicer_ams_mapping` drives a badge that claims the
print reuses the slicer's exact trays. Global tray IDs mean nothing on
another printer, so the flag must be false for a row targeting one —
otherwise the badge is there while nothing is reused (#2700 review).
"""
origin_printer = await printer_factory()
other_printer = await printer_factory()
archive = await archive_factory(
extra_data={"slicer_ams_mapping": {"mapping": [5, -1, 2, -1], "printer_id": origin_printer.id}}
)
own = await async_client.post(
"/api/v1/queue/", json={"printer_id": origin_printer.id, "archive_id": archive.id}
)
assert own.status_code == 200
assert own.json()["archive_has_slicer_ams_mapping"] is True
foreign = await async_client.post(
"/api/v1/queue/", json={"printer_id": other_printer.id, "archive_id": archive.id}
)
assert foreign.status_code == 200
assert foreign.json()["archive_has_slicer_ams_mapping"] is False
# Model-based: the scheduler hasn't picked a printer yet, so the
# mapping is not reused there either.
model_based = await async_client.post("/api/v1/queue/", json={"target_model": "X1C", "archive_id": archive.id})
assert model_based.status_code == 200
assert model_based.json()["archive_has_slicer_ams_mapping"] is False
@pytest.mark.asyncio
@pytest.mark.integration
async def test_add_to_queue_with_plate_id(