mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 20:56:18 +02:00
The dialog weighed each plate against the spool in the slot it mapped to and knew nothing about AMS Filament Backup, so a two-plate job needing 1441 g of ABS was refused against a 1000 g spool while the identical full spool in the next slot went uncounted. The dispatcher has pooled matching spools since #1762 and would have run the print -- "Print anyway" was always the right answer to this warning. The rule for which spools back each other up now lives in one place: build_slot_materials() in filament_deficit, which the dispatcher's pool and the new slot_materials half of GET /printers/{id}/inventory-remain both draw on. The dialog groups on the keys it is handed rather than resolving spools a second time, which is what let the two answers drift apart, and which also gives the check to Spoolman users -- it read the internal inventory only, so in Spoolman mode it approved everything. Where a pool really is short the warning quotes the pooled totals, since the per-slot figure reads as a contradiction next to a full peer spool.
150 lines
5.7 KiB
Python
150 lines
5.7 KiB
Python
"""Tests for GET /printers/{id}/inventory-remain (#1766).
|
|
|
|
The endpoint exposes the same `_build_inventory_remain_overrides` map the
|
|
dispatcher uses so PrintModal's client-side "Prefer Lowest Remaining Filament"
|
|
sort agrees with what gets dispatched — closes the gap where Spoolman-mode
|
|
users couldn't see inventory grams from the frontend.
|
|
|
|
It also carries `slot_materials`: every inventory binding with the backend's
|
|
material identity and extruder side, which is what lets the modal's pre-flight
|
|
filament check pool spools under AMS Filament Backup the way the dispatcher
|
|
does instead of resolving spools itself.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from backend.app.api.routes.printers import get_inventory_remain
|
|
from backend.app.services.filament_deficit import SlotMaterial
|
|
|
|
|
|
@pytest.fixture
|
|
def db():
|
|
return MagicMock()
|
|
|
|
|
|
async def _call_endpoint(db, printer_id=1):
|
|
return await get_inventory_remain(printer_id=printer_id, _=None, db=db)
|
|
|
|
|
|
class TestGetInventoryRemain:
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_when_printer_has_no_status(self, db):
|
|
# Printer disconnected / unknown — endpoint must not error, return {}.
|
|
with patch(
|
|
"backend.app.services.printer_manager.printer_manager.get_status",
|
|
return_value=None,
|
|
):
|
|
result = await _call_endpoint(db)
|
|
assert result == {"inventory_remain_g": {}, "slot_materials": []}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_serialises_globaltrayid_keys_as_strings(self, db):
|
|
# JSON requires string keys; client converts back to Number on receive.
|
|
# Asserts the key-shape contract the frontend depends on.
|
|
state = SimpleNamespace(raw_data={})
|
|
with (
|
|
patch(
|
|
"backend.app.services.printer_manager.printer_manager.get_status",
|
|
return_value=state,
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_loaded_filaments",
|
|
return_value=[
|
|
{"ams_id": 0, "tray_id": 0, "global_tray_id": 0, "is_external": False},
|
|
{"ams_id": 0, "tray_id": 3, "global_tray_id": 3, "is_external": False},
|
|
],
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_inventory_remain_overrides",
|
|
new=AsyncMock(return_value={0: 950.0, 3: 50.0}),
|
|
),
|
|
patch(
|
|
"backend.app.services.filament_deficit.build_slot_materials",
|
|
new=AsyncMock(return_value=[]),
|
|
),
|
|
):
|
|
result = await _call_endpoint(db)
|
|
|
|
assert result["inventory_remain_g"] == {"0": 950.0, "3": 50.0}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_dict_when_no_bound_slots(self, db):
|
|
# Loaded filaments exist but none are bound to an inventory spool.
|
|
# Backend returns {}; route serialises it unchanged.
|
|
state = SimpleNamespace(raw_data={})
|
|
with (
|
|
patch(
|
|
"backend.app.services.printer_manager.printer_manager.get_status",
|
|
return_value=state,
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_loaded_filaments",
|
|
return_value=[
|
|
{"ams_id": 0, "tray_id": 0, "global_tray_id": 0, "is_external": False},
|
|
],
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_inventory_remain_overrides",
|
|
new=AsyncMock(return_value={}),
|
|
),
|
|
patch(
|
|
"backend.app.services.filament_deficit.build_slot_materials",
|
|
new=AsyncMock(return_value=[]),
|
|
),
|
|
):
|
|
result = await _call_endpoint(db)
|
|
|
|
assert result["inventory_remain_g"] == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_slot_materials_carry_identity_and_extruder(self, db):
|
|
# The modal groups on (material_key, extruder) to decide what AMS
|
|
# Filament Backup can pool, so both fields have to survive the wire.
|
|
# Unlike inventory_remain_g this covers every binding, not just the
|
|
# slots currently loaded — the dispatcher pools all of them.
|
|
state = SimpleNamespace(raw_data={})
|
|
with (
|
|
patch(
|
|
"backend.app.services.printer_manager.printer_manager.get_status",
|
|
return_value=state,
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_loaded_filaments",
|
|
return_value=[],
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_inventory_remain_overrides",
|
|
new=AsyncMock(return_value={}),
|
|
),
|
|
patch(
|
|
"backend.app.services.filament_deficit.build_slot_materials",
|
|
new=AsyncMock(
|
|
return_value=[
|
|
SlotMaterial(
|
|
ams_id=0,
|
|
tray_id=2,
|
|
global_tray_id=2,
|
|
material_key="preset:PFUS6488|color:616777",
|
|
remaining_grams=1000.0,
|
|
extruder=0,
|
|
),
|
|
]
|
|
),
|
|
),
|
|
):
|
|
result = await _call_endpoint(db)
|
|
|
|
assert result["slot_materials"] == [
|
|
{
|
|
"ams_id": 0,
|
|
"tray_id": 2,
|
|
"global_tray_id": 2,
|
|
"material_key": "preset:PFUS6488|color:616777",
|
|
"remaining_g": 1000.0,
|
|
"extruder": 0,
|
|
}
|
|
]
|