| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- """Tests for `SliceRequest` validator — covers both the legacy bare-int
- shape and the new source-aware shape, plus the backwards-compat
- normalisation that lets the route handler ignore the difference.
- """
- import pytest
- from pydantic import ValidationError
- from backend.app.schemas.slicer import PresetRef, SliceBundleSpec, SliceRequest
- class TestLegacyBareIntegerShape:
- """Existing clients (and stale browser tabs after upgrade) keep
- sending bare integer ids. They must continue working unchanged."""
- def test_bare_int_ids_normalise_to_local_preset_ref(self):
- req = SliceRequest(printer_preset_id=1, process_preset_id=2, filament_preset_id=3)
- assert req.printer_preset == PresetRef(source="local", id="1")
- assert req.process_preset == PresetRef(source="local", id="2")
- assert req.filament_preset == PresetRef(source="local", id="3")
- def test_legacy_ids_unchanged_in_payload(self):
- """The legacy fields stay populated — no behaviour change for
- clients that read them back from the model."""
- req = SliceRequest(printer_preset_id=10, process_preset_id=20, filament_preset_id=30)
- assert req.printer_preset_id == 10
- assert req.process_preset_id == 20
- assert req.filament_preset_id == 30
- class TestNewSourceAwareShape:
- """The new modal sends source-aware refs explicitly."""
- def test_cloud_refs_pass_through(self):
- req = SliceRequest(
- printer_preset=PresetRef(source="cloud", id="PFUprinter"),
- process_preset=PresetRef(source="cloud", id="PFUprocess"),
- filament_preset=PresetRef(source="cloud", id="PFUfilament"),
- )
- assert req.printer_preset.source == "cloud"
- assert req.printer_preset.id == "PFUprinter"
- def test_mixed_sources_per_slot(self):
- """A user may pick cloud for printer, local for process, standard
- for filament — the modal is per-slot."""
- req = SliceRequest(
- printer_preset=PresetRef(source="cloud", id="PFU123"),
- process_preset=PresetRef(source="local", id="42"),
- filament_preset=PresetRef(source="standard", id="Bambu PLA Basic"),
- )
- assert req.printer_preset.source == "cloud"
- assert req.process_preset.source == "local"
- assert req.filament_preset.source == "standard"
- class TestValidationErrors:
- def test_missing_printer_slot_raises(self):
- with pytest.raises(ValidationError) as exc:
- SliceRequest(process_preset_id=2, filament_preset_id=3)
- assert "printer" in str(exc.value)
- def test_invalid_source_rejected(self):
- with pytest.raises(ValidationError):
- SliceRequest(
- printer_preset={"source": "made_up", "id": "x"},
- process_preset_id=2,
- filament_preset_id=3,
- )
- class TestPriorityWhenBothSet:
- """If a client sends BOTH the legacy id AND the new ref for the same
- slot (unlikely in practice, but ambiguous), the new ref wins. Tests
- pin the resolution order so a future schema change can't silently
- flip it."""
- def test_explicit_ref_wins_over_legacy_id(self):
- req = SliceRequest(
- printer_preset_id=999, # would resolve to local:999
- printer_preset=PresetRef(source="cloud", id="PFU"),
- process_preset_id=2,
- filament_preset_id=3,
- )
- # Validator only fills the ref when it's None — the explicit cloud
- # ref stays untouched.
- assert req.printer_preset == PresetRef(source="cloud", id="PFU")
- class TestFilamentPresetsList:
- """Multi-color: the new array shape carries one filament profile per
- plate slot in plate order. Backwards-compat: legacy clients still
- submit a singular `filament_preset` and the validator promotes it into
- a one-element list so the route handler only deals with one shape."""
- def test_explicit_list_passes_through(self):
- refs = [
- PresetRef(source="cloud", id="A"),
- PresetRef(source="local", id="2"),
- PresetRef(source="standard", id="Bambu PLA Basic"),
- ]
- req = SliceRequest(
- printer_preset_id=1,
- process_preset_id=2,
- filament_preset_id=99, # explicit legacy id — should be ignored
- filament_presets=refs,
- )
- assert req.filament_presets == refs
- # Precedence pin: when caller sends both shapes, the array wins and
- # the singular gets backfilled from the array's first entry — NOT
- # from the legacy id 99. Documents the migration ordering for a
- # future change that might quietly mix them.
- assert req.filament_preset == refs[0]
- def test_empty_list_is_backfilled_from_singular(self):
- req = SliceRequest(printer_preset_id=1, process_preset_id=2, filament_preset_id=3)
- # Legacy single-color path: validator promotes the singular into a
- # one-element list so route handlers can iterate uniformly.
- assert req.filament_presets == [PresetRef(source="local", id="3")]
- def test_explicit_empty_list_with_singular_set_uses_singular(self):
- # User of the new schema can leave `filament_presets` as the empty
- # default and rely on the legacy `filament_preset_id` — same path
- # as `test_empty_list_is_backfilled_from_singular`.
- req = SliceRequest(
- printer_preset_id=1,
- process_preset_id=2,
- filament_preset=PresetRef(source="cloud", id="PFU"),
- filament_presets=[],
- )
- assert req.filament_presets == [PresetRef(source="cloud", id="PFU")]
- def test_list_preserves_order(self):
- refs = [
- PresetRef(source="cloud", id="slot1"),
- PresetRef(source="cloud", id="slot2"),
- PresetRef(source="cloud", id="slot3"),
- ]
- req = SliceRequest(
- printer_preset_id=1,
- process_preset_id=2,
- filament_preset_id=3,
- filament_presets=refs,
- )
- assert [r.id for r in req.filament_presets] == ["slot1", "slot2", "slot3"]
- class TestBundleDispatchShape:
- """When SliceRequest.bundle is set, the dispatcher picks the JSON
- triplet from a sidecar-side bundle by name and PresetRef resolution
- is skipped entirely. Validator must accept "bundle alone" without
- flagging missing presets."""
- def test_bundle_alone_validates(self):
- req = SliceRequest(
- bundle=SliceBundleSpec(
- bundle_id="abc123def456abcd",
- printer_name="# Bambu Lab H2D 0.4 nozzle",
- process_name="# 0.20mm Standard @BBL H2D",
- filament_names=["# Bambu PLA Basic @BBL H2D"],
- ),
- )
- # PresetRef fields are absent; that's fine in bundle mode.
- assert req.bundle is not None
- assert req.printer_preset is None
- assert req.process_preset is None
- assert req.filament_presets == []
- def test_bundle_with_filament_list_preserves_order(self):
- req = SliceRequest(
- bundle=SliceBundleSpec(
- bundle_id="abc",
- printer_name="P",
- process_name="Q",
- filament_names=["red", "blue", "green"],
- ),
- )
- assert req.bundle.filament_names == ["red", "blue", "green"]
- def test_bundle_rejects_empty_filament_list(self):
- with pytest.raises(ValidationError):
- SliceBundleSpec(
- bundle_id="abc",
- printer_name="P",
- process_name="Q",
- filament_names=[],
- )
- def test_bundle_rejects_empty_id(self):
- with pytest.raises(ValidationError):
- SliceBundleSpec(
- bundle_id="",
- printer_name="P",
- process_name="Q",
- filament_names=["F"],
- )
- def test_no_bundle_no_presets_still_rejected(self):
- # Dropping the bundle escape-hatch must not bypass the existing
- # presets-required check.
- with pytest.raises(ValidationError):
- SliceRequest()
- def test_bundle_with_presets_keeps_both_fields(self):
- # Sending both is allowed (validator accepts the bundle and skips
- # preset normalisation) — the dispatch picks bundle on the route
- # side. Confirms the validator doesn't reject overlapping intent
- # so a future client that wants to record the legacy presets
- # alongside doesn't fail validation.
- req = SliceRequest(
- printer_preset=PresetRef(source="standard", id="X1C"),
- process_preset=PresetRef(source="standard", id="0.20"),
- filament_presets=[PresetRef(source="standard", id="PLA")],
- bundle=SliceBundleSpec(
- bundle_id="abc",
- printer_name="P",
- process_name="Q",
- filament_names=["F"],
- ),
- )
- assert req.bundle is not None
- # Presets stay populated; dispatch ignores them when bundle is set.
- assert req.printer_preset is not None
|