Files
bambuddy/backend/tests/unit/test_scheduler_backup_gate.py
T
maziggy 70312b0a11 fix(scheduler): skip per-nozzle filter under FTS so dispatch feeds the right spool (#2186)
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.
2026-07-07 12:17:46 +02:00

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