mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
On a dual-nozzle H2C with a Filament Track Switch, a queued print targeting one nozzle fed a same-type wrong-colour spool: the backend mapping (_match_filaments_to_slots) hard-filtered candidate trays to the requested extruder, excluding the correct spool loaded in the other nozzle's AMS — which the FTS can route across. Confirmed from the reporter's captures: same model mapped to AMS-A slot 2 (black) on the left nozzle but AMS-B slot 3 (red) on the right. The #1162 FTS-skip existed only in the frontend mapping, never the queue-dispatch path. Read fila_switch.installed in _compute_ams_mapping_for_printer and skip the per-nozzle filter when an FTS is present. Single-nozzle printers are unaffected (no nozzle_id in the 3MF, no FTS). Regression tests in TestFtsNozzleBypass.
97 lines
3.9 KiB
Python
97 lines
3.9 KiB
Python
"""Tests for the AMS Filament Backup gate on prefer_lowest sort (#1766).
|
|
|
|
The reporter set ``prefer_lowest_filament=True`` but the printer kept picking
|
|
the first matching spool. Root cause: without the printer's AMS Filament
|
|
Backup enabled, switching to the second spool is impossible — so sorting
|
|
toward the lowest leaves the print at risk. The gate coerces prefer_lowest
|
|
to False whenever the printer reports backup OFF, with None (unknown / A1
|
|
family) preserving today's behaviour.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from backend.app.services.print_scheduler import PrintScheduler
|
|
|
|
|
|
@pytest.fixture
|
|
def scheduler():
|
|
return PrintScheduler()
|
|
|
|
|
|
def _patch_status(backup):
|
|
"""Patch ``printer_manager.get_status`` to return a stub PrinterState whose
|
|
``ams_filament_backup`` is the requested tri-state value."""
|
|
return patch(
|
|
"backend.app.services.print_scheduler.printer_manager.get_status",
|
|
return_value=SimpleNamespace(ams_filament_backup=backup, raw_data={}),
|
|
)
|
|
|
|
|
|
async def _run_with_backup(scheduler, backup_state, prefer_lowest_setting):
|
|
"""Drive ``_compute_ams_mapping_for_printer`` past the gate point and
|
|
return whatever ``prefer_lowest`` value gets handed to the matcher."""
|
|
db = MagicMock()
|
|
item = SimpleNamespace(filament_overrides=None)
|
|
filament_reqs = [{"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": ""}]
|
|
loaded = [
|
|
{
|
|
"ams_id": 0,
|
|
"tray_id": 0,
|
|
"global_tray_id": 0,
|
|
"is_external": False,
|
|
"type": "PLA",
|
|
"color": "#000000",
|
|
"tray_info_idx": "",
|
|
},
|
|
]
|
|
|
|
captured: dict = {}
|
|
|
|
def _capture_match(reqs, loaded_, prefer, overrides, fts_installed=False):
|
|
captured["prefer_lowest"] = prefer
|
|
return [0]
|
|
|
|
with (
|
|
_patch_status(backup_state),
|
|
patch.object(scheduler, "_get_filament_requirements", new=AsyncMock(return_value=filament_reqs)),
|
|
patch.object(scheduler, "_build_loaded_filaments", return_value=loaded),
|
|
patch.object(scheduler, "_get_bool_setting", new=AsyncMock(return_value=prefer_lowest_setting)),
|
|
patch.object(scheduler, "_build_inventory_remain_overrides", new=AsyncMock(return_value={})),
|
|
patch.object(scheduler, "_match_filaments_to_slots", side_effect=_capture_match),
|
|
):
|
|
await scheduler._compute_ams_mapping_for_printer(db, printer_id=1, item=item)
|
|
|
|
return captured.get("prefer_lowest")
|
|
|
|
|
|
class TestPreferLowestBackupGate:
|
|
@pytest.mark.asyncio
|
|
async def test_backup_off_disables_prefer_lowest(self, scheduler):
|
|
# User setting ON but printer reports backup OFF — gate must coerce.
|
|
# This is the #1766 fix: previously the sort applied and picked the
|
|
# near-empty spool, leaving the print to fail mid-job.
|
|
out = await _run_with_backup(scheduler, backup_state=False, prefer_lowest_setting=True)
|
|
assert out is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_backup_on_preserves_prefer_lowest(self, scheduler):
|
|
# Backup ON — sort applies as the user intended.
|
|
out = await _run_with_backup(scheduler, backup_state=True, prefer_lowest_setting=True)
|
|
assert out is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_backup_unknown_preserves_prefer_lowest(self, scheduler):
|
|
# None = unknown / unsupported (A1 family). Must NOT be treated as OFF
|
|
# — that would regress A1 users who currently get the sort applied.
|
|
out = await _run_with_backup(scheduler, backup_state=None, prefer_lowest_setting=True)
|
|
assert out is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_setting_off_short_circuits(self, scheduler):
|
|
# User setting OFF — backup state is irrelevant; sort never applies.
|
|
out = await _run_with_backup(scheduler, backup_state=True, prefer_lowest_setting=False)
|
|
assert out is False
|