mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
OrcaSlicer's Sync filaments finds a slot's preset by the slot's filament ID alone. The Configure dialog looked up an Orca profile's ID in the browser and quietly sent the generic for the material when that came back empty, so Orca custom filaments reached the slicer as Generic and the log showed nothing. configure now resolves the ID on the server from orca_profile_id, follows inherits to the parent profile or the Bambu filament it was copied from, logs the outcome and reports a fallback, which the dialog shows as a warning. Slot presets also store the filament ID they were written with, so the slot card and the Configure dialog stop showing a preset once the slot is changed from OrcaSlicer's Device tab or the printer. Re-configuring a slot for another filament no longer carries over the old filament's active K-profile, which switched the slot back to the old filament.
579 lines
23 KiB
Python
579 lines
23 KiB
Python
"""Tests for ``resolve_slicer_filament`` (#1815).
|
|
|
|
The defensive filter at the end of the resolver clears ``tray_info_idx``
|
|
when its value isn't slicer-acceptable (literal material names + PFUS /
|
|
PFCN cloud-preset prefixes that the printer's calibration table can't
|
|
key on). Pre-#1815 it cleared ``setting_id`` alongside, which dropped
|
|
the slicer's only handle on the user's actual custom preset and forced
|
|
the caller into the generic-material fallback — Bambu Studio then
|
|
displayed "Generic <Material>" for spools whose Bambu Cloud detail
|
|
lookup didn't resolve a ``filament_id`` (cloud unauth on the on_ams_change
|
|
replay path, transient cloud failure, or custom presets whose detail
|
|
JSON omits ``filament_id``).
|
|
|
|
Post-#1815 the filter preserves a setting_id that's still a valid
|
|
slicer reference (PFUS / PFCN cloud user/shared preset, or GFS Bambu
|
|
official preset) even when ``tray_info_idx`` is cleared.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from backend.app.services.slicer_filament_resolver import resolve_slicer_filament
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pfus_cloud_unavailable_preserves_setting_id():
|
|
"""Reporter scenario: PFUS cloud user preset, cloud lookup fails to
|
|
return a filament_id. setting_id must survive so the slicer can
|
|
still find the user's actual custom preset."""
|
|
db = MagicMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=None),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="PFUS990b6e19965353",
|
|
slicer_filament_name="Jayo PETG HF",
|
|
material="PETG",
|
|
)
|
|
assert tray_info_idx == ""
|
|
assert setting_id == "PFUS990b6e19965353"
|
|
assert sub_brand is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pfcn_cloud_unavailable_preserves_setting_id():
|
|
"""PFCN partner/shared cloud preset (e.g. Polymaker H2D variants,
|
|
#1648) shares the same shape problem as PFUS."""
|
|
db = MagicMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=None),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="PFCN1234567890",
|
|
slicer_filament_name="Polymaker PolyTerra PLA",
|
|
material="PLA",
|
|
)
|
|
assert tray_info_idx == ""
|
|
assert setting_id == "PFCN1234567890"
|
|
assert sub_brand is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pfus_cloud_resolves_filament_id_regression_guard():
|
|
"""When cloud auth works and returns a filament_id, the resolver
|
|
keeps its existing behaviour: tray_info_idx = real filament_id,
|
|
setting_id = original PFUS reference."""
|
|
db = MagicMock()
|
|
cloud_mock = MagicMock()
|
|
cloud_mock.is_authenticated = True
|
|
cloud_mock.get_setting_detail = AsyncMock(return_value={"filament_id": "P285e239", "name": "Jayo PETG HF @P1S"})
|
|
cloud_mock.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud_mock),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=MagicMock(),
|
|
slicer_filament="PFUS990b6e19965353",
|
|
slicer_filament_name="Jayo PETG HF",
|
|
material="PETG",
|
|
)
|
|
assert tray_info_idx == "P285e239"
|
|
assert setting_id == "PFUS990b6e19965353"
|
|
assert sub_brand == "Jayo PETG HF"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gfs_cloud_unavailable_resolves_via_normalize():
|
|
"""GFS Bambu official preset + cloud unavailable: normalize strips
|
|
the 'S' to give a real filament_id ('GFG02'), so tray_info_idx is
|
|
valid and the defensive filter doesn't trigger. setting_id stays as
|
|
the original GFS reference. Regression guard for the cloud-down
|
|
Bambu-official path."""
|
|
db = MagicMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=None),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="GFSG02",
|
|
slicer_filament_name=None,
|
|
material="PETG",
|
|
)
|
|
assert tray_info_idx == "GFG02"
|
|
assert setting_id == "GFSG02"
|
|
assert sub_brand is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_literal_material_name_clears_both():
|
|
"""slicer_filament='PETG' (free-text material leak from legacy
|
|
spools): both tray_info_idx and setting_id must be cleared so the
|
|
caller's generic-material fallback rescues the slot. Regression
|
|
guard that the PFUS preservation doesn't accidentally preserve
|
|
literal material names."""
|
|
db = MagicMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=None),
|
|
):
|
|
tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="PETG",
|
|
slicer_filament_name=None,
|
|
material="PETG",
|
|
)
|
|
assert tray_info_idx == ""
|
|
assert setting_id == ""
|
|
assert sub_brand is None
|
|
|
|
|
|
class TestThePresetsOwnType:
|
|
"""#2902: a preset is chosen from a list the slicer defines, so its
|
|
``filament_type`` is the slicer's own answer to what the material is --
|
|
no reading of a product name required. Raised by @doncaruana on the issue
|
|
after the first fix reduced "PLA Aero" to "PLA".
|
|
|
|
The resolver hands that answer back as the fourth element; the two assign
|
|
routes write it into ``tray_type`` in preference to reducing the spool's
|
|
material column. ``None`` means no preset said, and the reduction stands.
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_local_presets_type_is_returned(self):
|
|
db = MagicMock()
|
|
lp = MagicMock()
|
|
lp.filament_type = "PLA-AERO"
|
|
lp.setting = None
|
|
lp.name = "Bambu PLA Aero @BBL X1C"
|
|
result = MagicMock()
|
|
result.scalar_one_or_none = MagicMock(return_value=lp)
|
|
db.execute = AsyncMock(return_value=result)
|
|
|
|
_idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="38",
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert type_override == "PLA-AERO"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_cloud_presets_type_is_read_out_of_its_profile(self):
|
|
"""Both slicers store it as a one-element array, and the preset JSON
|
|
sits under ``setting`` in the cloud envelope."""
|
|
db = MagicMock()
|
|
cloud = MagicMock()
|
|
cloud.is_authenticated = True
|
|
cloud.get_setting_detail = AsyncMock(
|
|
return_value={
|
|
"filament_id": "GFA11",
|
|
"name": "Bambu PLA Aero @BBL X1C",
|
|
"setting": {"filament_type": ["PLA-AERO"]},
|
|
}
|
|
)
|
|
cloud.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud),
|
|
):
|
|
idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="GFSA11",
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert idx == "GFA11"
|
|
assert type_override == "PLA-AERO"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_bare_string_filament_type_is_accepted_too(self):
|
|
"""Hand-written and older profiles store it unwrapped. ``orca_profiles``
|
|
accepts both forms, so this has to as well."""
|
|
db = MagicMock()
|
|
cloud = MagicMock()
|
|
cloud.is_authenticated = True
|
|
cloud.get_setting_detail = AsyncMock(
|
|
return_value={"filament_id": "GFG02", "setting": {"filament_type": "PETG"}}
|
|
)
|
|
cloud.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud),
|
|
):
|
|
_idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="GFSG02",
|
|
slicer_filament_name=None,
|
|
material="PETG",
|
|
)
|
|
assert type_override == "PETG"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_preset_means_no_answer(self):
|
|
"""A spool with no slicer_filament -- the case this issue was reported
|
|
for. ``material`` is required on a spool and ``slicer_filament`` is
|
|
not, so the reduction has to stay as the fallback."""
|
|
db = MagicMock()
|
|
_idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament=None,
|
|
slicer_filament_name=None,
|
|
material="PLA+",
|
|
)
|
|
assert type_override is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_preset_that_does_not_say_gets_no_opinion(self):
|
|
db = MagicMock()
|
|
cloud = MagicMock()
|
|
cloud.is_authenticated = True
|
|
cloud.get_setting_detail = AsyncMock(return_value={"filament_id": "GFG02", "setting": {}})
|
|
cloud.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud),
|
|
):
|
|
_idx, _sid, _brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="GFSG02",
|
|
slicer_filament_name=None,
|
|
material="PETG",
|
|
)
|
|
assert type_override is None
|
|
|
|
|
|
class TestACustomPresetsOwnFilamentId:
|
|
"""Where the id that carries a custom preset into an AMS slot comes from.
|
|
|
|
The slot holds one filament reference and the printer truncates it to 8
|
|
characters, so a custom preset reaches the slicer as itself only when its
|
|
own filament_id ("P" + 7 hex) goes into ``tray_info_idx``. 92 trays across
|
|
eight models in the support archive do exactly that, so the mechanism
|
|
works -- what #3003 found is that we only ever read one of the two places
|
|
Bambu Cloud returns that id from.
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filament_id_is_read_from_inside_the_preset_json(self):
|
|
"""The envelope has none, the preset JSON does -- and it wins over base_id.
|
|
|
|
Before #3003 this fell through to the base_id branch and the slot came
|
|
out as the Bambu profile the custom preset inherits from.
|
|
"""
|
|
db = MagicMock()
|
|
cloud = MagicMock()
|
|
cloud.is_authenticated = True
|
|
cloud.get_setting_detail = AsyncMock(
|
|
return_value={
|
|
"name": "SUNLU PLA Transparent @BBL A1",
|
|
"base_id": "GFSNLS03",
|
|
"setting": {"filament_id": "P4d64437", "filament_type": ["PLA"]},
|
|
}
|
|
)
|
|
cloud.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud),
|
|
):
|
|
idx, sid, brand, _type = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="PFUSfb87cd50b76616",
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert idx == "P4d64437"
|
|
assert sid == "PFUSfb87cd50b76616"
|
|
assert brand == "SUNLU PLA Transparent"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_envelope_still_wins_when_it_has_one(self):
|
|
"""Unchanged behaviour for the presets that already resolved."""
|
|
db = MagicMock()
|
|
cloud = MagicMock()
|
|
cloud.is_authenticated = True
|
|
cloud.get_setting_detail = AsyncMock(
|
|
return_value={
|
|
"filament_id": "P285e239",
|
|
"name": "Jayo PETG HF @P1S",
|
|
"base_id": "GFSG02",
|
|
"setting": {"filament_id": "P999aaaa"},
|
|
}
|
|
)
|
|
cloud.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud),
|
|
):
|
|
idx, _sid, _brand, _type = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="PFUS992454068158eb",
|
|
slicer_filament_name=None,
|
|
material="PETG",
|
|
)
|
|
assert idx == "P285e239"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_base_id_is_still_the_fallback_when_neither_place_has_one(self):
|
|
"""A preset with no filament_id of its own genuinely is its base, and
|
|
the base id is storable, so it is the right answer -- just not one to
|
|
reach for while the preset's own id is sitting under ``setting``."""
|
|
db = MagicMock()
|
|
cloud = MagicMock()
|
|
cloud.is_authenticated = True
|
|
cloud.get_setting_detail = AsyncMock(
|
|
return_value={"name": "My PLA @BBL A1", "base_id": "GFSNLS03", "setting": {}}
|
|
)
|
|
cloud.close = AsyncMock()
|
|
with patch(
|
|
"backend.app.api.routes.cloud.build_authenticated_cloud",
|
|
AsyncMock(return_value=cloud),
|
|
):
|
|
idx, sid, _brand, _type = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament="PFUSfb87cd50b76616",
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert idx == "GFNLS03"
|
|
assert sid == "PFUSfb87cd50b76616"
|
|
|
|
|
|
class TestOrcaCloudIsTheFirstSource:
|
|
"""Source order is Orca Cloud, Bambu Cloud, local import, generic.
|
|
|
|
Orca was absent from the resolver entirely: a spool referencing an Orca
|
|
profile stores the bare UUID, which matched no branch and fell through
|
|
``normalize_slicer_filament`` -- a function that passes anything it does
|
|
not recognise straight through. The UUID reached tray_info_idx, a field
|
|
the printer truncates to 8 characters (#3003).
|
|
"""
|
|
|
|
ORCA_ID = "3f2a9c1e-4b7d-4a02-9f61-8c5e2d1a7b30"
|
|
|
|
@staticmethod
|
|
def _svc(profile):
|
|
svc = MagicMock()
|
|
svc.get_profile = AsyncMock(return_value=profile)
|
|
svc.close = AsyncMock()
|
|
return svc
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_profiles_own_filament_id_is_used(self):
|
|
db = MagicMock()
|
|
svc = self._svc(
|
|
{
|
|
"id": self.ORCA_ID,
|
|
"name": "Overture Matte PLA @Orca",
|
|
"content": {"filament_id": "P56e1be0", "filament_type": ["PLA"]},
|
|
}
|
|
)
|
|
with patch(
|
|
"backend.app.api.routes.orca_cloud._build_authenticated_service",
|
|
AsyncMock(return_value=svc),
|
|
):
|
|
idx, sid, brand, type_override = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament=self.ORCA_ID,
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert idx == "P56e1be0"
|
|
# The UUID is foreign to the slicer in either field, so nothing carries it.
|
|
assert sid == ""
|
|
assert brand == "Overture Matte PLA"
|
|
assert type_override == "PLA"
|
|
svc.close.assert_awaited()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_profile_with_no_filament_id_leaves_the_caller_its_fallback(self):
|
|
db = MagicMock()
|
|
svc = self._svc({"id": self.ORCA_ID, "name": "My PLA", "content": {"filament_type": ["PLA"]}})
|
|
with patch(
|
|
"backend.app.api.routes.orca_cloud._build_authenticated_service",
|
|
AsyncMock(return_value=svc),
|
|
):
|
|
idx, sid, _brand, _type = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament=self.ORCA_ID,
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert idx == ""
|
|
assert sid == ""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_unreachable_orca_never_leaks_the_uuid(self):
|
|
"""No pairing, dead token, Orca down -- all the same answer. The UUID
|
|
must not reach tray_info_idx, which is what happened before the branch
|
|
existed at all."""
|
|
db = MagicMock()
|
|
with patch(
|
|
"backend.app.api.routes.orca_cloud._build_authenticated_service",
|
|
AsyncMock(side_effect=RuntimeError("Orca Cloud is not connected")),
|
|
):
|
|
idx, sid, _brand, _type = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament=self.ORCA_ID,
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert idx == ""
|
|
assert sid == ""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_caller_without_the_permission_skips_the_lookup(self):
|
|
db = MagicMock()
|
|
user = MagicMock()
|
|
user.has_permission = MagicMock(return_value=False)
|
|
with patch(
|
|
"backend.app.api.routes.orca_cloud._build_authenticated_service",
|
|
AsyncMock(side_effect=AssertionError("must not be called")),
|
|
):
|
|
idx, _sid, _brand, _type = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=user,
|
|
slicer_filament=self.ORCA_ID,
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert idx == ""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_uuid_is_refused_as_tray_info_idx_by_the_closing_guard(self):
|
|
"""Belt and braces: a profile whose content names itself by UUID still
|
|
does not put one in the field."""
|
|
db = MagicMock()
|
|
svc = self._svc({"id": self.ORCA_ID, "name": "Odd", "content": {"filament_id": self.ORCA_ID}})
|
|
with patch(
|
|
"backend.app.api.routes.orca_cloud._build_authenticated_service",
|
|
AsyncMock(return_value=svc),
|
|
):
|
|
idx, sid, _brand, _type = await resolve_slicer_filament(
|
|
db=db,
|
|
current_user=None,
|
|
slicer_filament=self.ORCA_ID,
|
|
slicer_filament_name=None,
|
|
material="PLA",
|
|
)
|
|
assert idx == ""
|
|
assert sid == ""
|
|
|
|
|
|
class TestOrcaLookupFollowsParents:
|
|
"""``lookup_orca_filament_id`` (#3216).
|
|
|
|
A profile that inherits carries only its overrides, so its filament id lives
|
|
on a parent: another of the user's Orca profiles, or a system preset. The
|
|
parent's id is what OrcaSlicer resolves the slot to, which beats a generic.
|
|
"""
|
|
|
|
ORCA_ID = "34d8f588-860b-5be1-bcbe-c0d46d96324b"
|
|
|
|
@staticmethod
|
|
def _svc(profile, others=()):
|
|
svc = MagicMock()
|
|
svc.get_profile = AsyncMock(return_value=profile)
|
|
svc.list_profiles = AsyncMock(return_value=[profile, *others])
|
|
svc.close = AsyncMock()
|
|
return svc
|
|
|
|
async def _lookup(self, svc):
|
|
from backend.app.services.slicer_filament_resolver import lookup_orca_filament_id
|
|
|
|
with patch(
|
|
"backend.app.api.routes.orca_cloud._build_authenticated_service",
|
|
AsyncMock(return_value=svc),
|
|
):
|
|
return await lookup_orca_filament_id(MagicMock(), None, self.ORCA_ID)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_own_id_needs_no_second_pull(self):
|
|
svc = self._svc({"id": self.ORCA_ID, "name": "Mine", "content": {"filament_id": "Pfc74047", "inherits": ""}})
|
|
found = await self._lookup(svc)
|
|
assert (found.filament_id, found.source, found.reason) == ("Pfc74047", "own", "")
|
|
svc.list_profiles.assert_not_awaited()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inherited_from_a_user_profile(self):
|
|
child = {"id": self.ORCA_ID, "name": "Mine v2", "content": {"inherits": "Mine v1", "filament_type": ["PLA"]}}
|
|
parent = {"id": "p", "name": "Mine v1", "content": {"name": "Mine v1", "filament_id": "Pad3f856"}}
|
|
found = await self._lookup(self._svc(child, [parent]))
|
|
assert (found.filament_id, found.source) == ("Pad3f856", "inherited")
|
|
assert found.filament_type == "PLA"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_through_two_user_profiles_to_a_bambu_system_preset(self):
|
|
child = {"id": self.ORCA_ID, "name": "C", "content": {"inherits": "B"}}
|
|
middle = {"id": "b", "name": "B", "content": {"inherits": "Bambu PLA Basic @BBL H2D"}}
|
|
found = await self._lookup(self._svc(child, [middle]))
|
|
# The catalog id OrcaSlicer translates at the printer boundary.
|
|
assert (found.filament_id, found.source) == ("GFA00", "inherited")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_unknown_system_parent_leaves_the_fallback(self):
|
|
child = {"id": self.ORCA_ID, "name": "C", "content": {"inherits": "Someone PLA @Somewhere"}}
|
|
found = await self._lookup(self._svc(child))
|
|
assert (found.filament_id, found.reason) == ("", "no_filament_id")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_inherits_cycle_terminates(self):
|
|
a = {"id": self.ORCA_ID, "name": "A", "content": {"inherits": "B"}}
|
|
b = {"id": "b", "name": "B", "content": {"inherits": "A"}}
|
|
found = await self._lookup(self._svc(a, [b]))
|
|
assert (found.filament_id, found.reason) == ("", "no_filament_id")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_failed_lookup_says_so(self):
|
|
svc = MagicMock()
|
|
svc.get_profile = AsyncMock(side_effect=RuntimeError("network down"))
|
|
svc.close = AsyncMock()
|
|
found = await self._lookup(svc)
|
|
assert (found.filament_id, found.reason) == ("", "lookup_failed")
|
|
svc.close.assert_awaited()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_caller_without_orca_access_is_told_so(self):
|
|
from backend.app.services.slicer_filament_resolver import lookup_orca_filament_id
|
|
|
|
user = MagicMock()
|
|
user.has_permission.return_value = False
|
|
found = await lookup_orca_filament_id(MagicMock(), user, self.ORCA_ID)
|
|
assert found.reason == "no_permission"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_failed_parent_pull_keeps_name_and_type(self):
|
|
"""The spool resolver uses them for the slot's brand and type, as it did
|
|
before the lookup followed parents."""
|
|
child = {"id": self.ORCA_ID, "name": "Mine v2", "content": {"inherits": "Mine v1", "filament_type": ["PETG"]}}
|
|
svc = self._svc(child)
|
|
svc.list_profiles = AsyncMock(side_effect=RuntimeError("timeout"))
|
|
found = await self._lookup(svc)
|
|
assert (found.filament_id, found.reason) == ("", "lookup_failed")
|
|
assert (found.name, found.filament_type) == ("Mine v2", "PETG")
|