mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-02 12:15:36 +02:00
Two tightly-coupled deliverables in one drop -- a new AMS Filament Backup status/control surface, and the #1766 fix that depends on it. Added -- AMS Filament Backup status + control - Parse bit 18 of top-level print.cfg into PrinterState.ams_filament_backup on every push_status. Verified against OrcaSlicer source (DeviceManager.cpp:4961) and a live H2D ON/OFF capture. Tri-state (None = A1 family / pre-cfg push) preserves today's behaviour. - Hold-timer guard (3 s) prevents stale frames from flickering the badge back to the printer's old cfg after a user-initiated toggle. - POST /printers/{id}/ams-backup toggle, set_ams_filament_backup() client method calling _set_print_option("auto_switch_filament", enabled). - GET /printers/{id}/inventory-remain endpoint exposes the same map the dispatcher uses (internal and Spoolman modes both work uniformly). - Small icon badge in the printer card's "Filaments" section header (placement reads as printer-wide because the cfg bit is printer-wide, not per-AMS). Click to toggle, success toast. - 5 i18n keys x 11 locales for the badge UI. Fixed -- #1766: prefer_lowest didn't pick lowest, ignored backup state - Backend gate in _compute_ams_mapping_for_printer: coerce prefer_lowest to False when status.ams_filament_backup is False; log the skip. - New effectivePreferLowest(setting, backup) helper applied at every frontend sort entry point: single-printer PrintModal, multi-printer hook per-printer, PrinterSelector InlineMappingEditor, FilamentMapping standalone editor (the last had NO preferLowest awareness at all before this change). - New preferLowestSortKey(f, inventoryByTrayId) mirrors backend's two-tier key exactly, including the banding tie-break (regular AMS < AMS-HT < external) so the client-side pre-compute matches the dispatch-time pick. An earlier draft used a flat `amsId * 4 + trayId` priority which gave external slots (ams_id = -1) a NEGATIVE priority -- caught in code review before commit. - Settings -> Filament -> "Prefer lowest remaining filament" gets an explanatory note about the printer-side AMS Backup dependency, with i18n key in all 11 locales.
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):
|
|
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
|