mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user