fix(archives): cross-model re-slice no longer carries source printer_id

When the user re-sliced an H2D archive for X1C, the new archive card and
  reprint modal both showed the source printer's name (e.g. "Workshop H2C")
  even though sliced_for_model on the same row correctly read "X1C".

  slice_and_persist_as_archive copied source_archive.printer_id verbatim
  onto the new PrintArchive row. Both the archive card
  (ArchivesPage.tsx:3571) and the reprint modal read printer_id first and
  only fall back to sliced_for_model when it's None. With printer_id set
  to the source's H2D printer, neither could see that the new file is for
  a different model.

  Same shape as the sliced_for_model fix already in the same function — a
  cross-model re-slice means the source's physical printer isn't where
  this output prints anymore. When the slicer-baked target model differs
  from source_archive.sliced_for_model, drop printer_id so both surfaces
  fall back to the sliced_for_model badge ("X1C"). Same-model re-slices
  keep printer_id so the reprint modal still pre-selects the source
  printer.

  Edge case: when source_archive.sliced_for_model is None (older archives
  that predate that column being populated), we can't tell whether this
  is a cross-model re-slice. Fail open and preserve printer_id rather
  than spuriously nulling it.

  slice_and_persist (the library-file path) doesn't have this bug —
  LibraryFile has no printer_id column.

  Tests in TestSliceArchiveResliceModel cover all three branches: cross-
  model nulls printer_id; same-model keeps it; unknown source model
  preserves it.

  Surfaced via the bambuddy demo doing H2D -> X1C re-slices after the
  .bbscfg System-tier slicing fix landed.
This commit is contained in:
maziggy
2026-05-23 14:25:12 +02:00
parent 5f473801f8
commit c0c07bc509
2 changed files with 194 additions and 1 deletions
+16 -1
View File
@@ -3624,8 +3624,23 @@ async def slice_and_persist_as_archive(
new_filament_type = parsed_metadata.get("filament_type") or source_archive.filament_type
new_filament_color = parsed_metadata.get("filament_color") or source_archive.filament_color
# When the user re-slices for a different printer model than the source,
# the source's printer_id (e.g. an H2D's "Workshop H2C") no longer
# represents where the new archive can be reprinted. The archive card
# and reprint modal both read printer_id first and only fall back to
# sliced_for_model when it's None, so leaving the inherited id makes
# the X1C-sliced card display the source H2D's printer name.
# Same pitfall as the sliced_for_model copy a few lines below.
new_target_model = parsed_metadata.get("sliced_for_model") or source_archive.sliced_for_model
is_cross_model_reslice = (
new_target_model is not None
and source_archive.sliced_for_model is not None
and new_target_model != source_archive.sliced_for_model
)
new_printer_id = None if is_cross_model_reslice else source_archive.printer_id
new_archive = PrintArchive(
printer_id=source_archive.printer_id,
printer_id=new_printer_id,
project_id=source_archive.project_id,
filename=out_filename,
file_path=str(out_path.relative_to(app_settings.base_dir)),
@@ -1039,6 +1039,184 @@ class TestSliceArchiveResliceModel:
source_reloaded = await db_session.get(PrintArchive, source_id)
assert source_reloaded.sliced_for_model == "X1C"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_cross_model_reslice_drops_source_printer_id(
self, async_client: AsyncClient, db_session, slice_test_setup, printer_factory, archive_factory, monkeypatch
):
"""A cross-model re-slice (source's X1C → target's H2D) must not carry
over ``source.printer_id``. The archive card and reprint modal both
read ``printer_id`` first and only fall back to ``sliced_for_model``
when it's None, so leaving the inherited id makes the H2D-sliced card
display the source's X1C printer name (the "Workshop H2C" bug)."""
from backend.app.models.archive import PrintArchive
tmp_path = slice_test_setup["tmp_path"]
monkeypatch.setattr(app_settings, "archive_dir", tmp_path / "archive")
src_dir = tmp_path / "archives" / "src"
src_dir.mkdir(parents=True, exist_ok=True)
src_3mf = src_dir / "cube.3mf"
src_3mf.write_bytes(_make_3mf_with_settings())
source_printer = await printer_factory()
source = await archive_factory(
source_printer.id,
filename="cube.3mf",
file_path=str(src_3mf.relative_to(tmp_path)),
sliced_for_model="X1C",
with_run=False,
)
source_id = source.id
source_printer_id = source_printer.id
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=200,
content=_make_sliced_3mf("O1D"), # H2D
headers={
"x-print-time-seconds": "600",
"x-filament-used-g": "5.0",
"x-filament-used-mm": "1600.0",
},
)
_install_mock_sidecar(handler)
resp = await async_client.post(
f"/api/v1/archives/{source_id}/slice",
json={
"printer_preset_id": slice_test_setup["printer_id"],
"process_preset_id": slice_test_setup["process_id"],
"filament_preset_id": slice_test_setup["filament_id"],
},
)
assert resp.status_code == 202, resp.text
final = await _wait_for_job(async_client, resp.json()["job_id"])
assert final["status"] == "completed", final
new_archive = await db_session.get(PrintArchive, final["result"]["archive_id"])
assert new_archive.sliced_for_model == "H2D"
# Card / reprint modal will now fall back to the sliced_for_model
# badge instead of showing the source printer's name.
assert new_archive.printer_id is None
# Source untouched: still bound to its original printer.
source_reloaded = await db_session.get(PrintArchive, source_id)
assert source_reloaded.printer_id == source_printer_id
@pytest.mark.asyncio
@pytest.mark.integration
async def test_same_model_reslice_preserves_source_printer_id(
self, async_client: AsyncClient, db_session, slice_test_setup, printer_factory, archive_factory, monkeypatch
):
"""Same-model re-slice (X1C → X1C, e.g. just swapped a process preset)
keeps ``printer_id`` so the reprint modal pre-selects the original
printer. Only cross-model re-slices null it out."""
from backend.app.models.archive import PrintArchive
tmp_path = slice_test_setup["tmp_path"]
monkeypatch.setattr(app_settings, "archive_dir", tmp_path / "archive")
src_dir = tmp_path / "archives" / "src"
src_dir.mkdir(parents=True, exist_ok=True)
src_3mf = src_dir / "cube.3mf"
src_3mf.write_bytes(_make_3mf_with_settings())
source_printer = await printer_factory()
source = await archive_factory(
source_printer.id,
filename="cube.3mf",
file_path=str(src_3mf.relative_to(tmp_path)),
sliced_for_model="X1C",
with_run=False,
)
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=200,
content=_make_sliced_3mf("C11"), # X1C — same model as source
headers={
"x-print-time-seconds": "600",
"x-filament-used-g": "5.0",
"x-filament-used-mm": "1600.0",
},
)
_install_mock_sidecar(handler)
resp = await async_client.post(
f"/api/v1/archives/{source.id}/slice",
json={
"printer_preset_id": slice_test_setup["printer_id"],
"process_preset_id": slice_test_setup["process_id"],
"filament_preset_id": slice_test_setup["filament_id"],
},
)
assert resp.status_code == 202, resp.text
final = await _wait_for_job(async_client, resp.json()["job_id"])
assert final["status"] == "completed", final
new_archive = await db_session.get(PrintArchive, final["result"]["archive_id"])
assert new_archive.sliced_for_model == "X1C"
# Same-model: keep the source's printer assignment so reprint pre-selects it.
assert new_archive.printer_id == source_printer.id
@pytest.mark.asyncio
@pytest.mark.integration
async def test_reslice_with_unknown_source_model_preserves_printer_id(
self, async_client: AsyncClient, db_session, slice_test_setup, printer_factory, archive_factory, monkeypatch
):
"""When ``source.sliced_for_model`` is None (older archive that
predates that column being populated), the backend can't tell whether
this is a cross-model re-slice. Fail open and preserve ``printer_id``
rather than spuriously nulling it — current pre-fix behaviour, kept
as a deliberate edge case."""
from backend.app.models.archive import PrintArchive
tmp_path = slice_test_setup["tmp_path"]
monkeypatch.setattr(app_settings, "archive_dir", tmp_path / "archive")
src_dir = tmp_path / "archives" / "src"
src_dir.mkdir(parents=True, exist_ok=True)
src_3mf = src_dir / "cube.3mf"
src_3mf.write_bytes(_make_3mf_with_settings())
source_printer = await printer_factory()
source = await archive_factory(
source_printer.id,
filename="cube.3mf",
file_path=str(src_3mf.relative_to(tmp_path)),
sliced_for_model=None,
with_run=False,
)
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=200,
content=_make_sliced_3mf("O1D"),
headers={
"x-print-time-seconds": "600",
"x-filament-used-g": "5.0",
"x-filament-used-mm": "1600.0",
},
)
_install_mock_sidecar(handler)
resp = await async_client.post(
f"/api/v1/archives/{source.id}/slice",
json={
"printer_preset_id": slice_test_setup["printer_id"],
"process_preset_id": slice_test_setup["process_id"],
"filament_preset_id": slice_test_setup["filament_id"],
},
)
assert resp.status_code == 202, resp.text
final = await _wait_for_job(async_client, resp.json()["job_id"])
assert final["status"] == "completed", final
new_archive = await db_session.get(PrintArchive, final["result"]["archive_id"])
# Insufficient info to decide cross-model → preserve printer_id.
assert new_archive.printer_id == source_printer.id
class TestSliceArchiveReslicedThumbnail:
"""#1493 follow-up: the re-sliced archive's cover image preference order is