"""Regression test: Spoolman fallback split silently deducted filament from spools not used in the current print. _spoolman_unreported() falls back to an equal split across every mapped AMS slot when per-slot attribution data (_spoolman_slot_usage) is still empty - previously true right after print start, because _spoolman_sync_midprint() ran (in the poll loop) before the first _spoolman_attribute_tick() had a chance to populate any per-slot data, since _spoolman_last_sync was reset to 0.0 (== "due immediately") instead of the current time. With 4 spools mapped, that meant the full (possibly already nonzero) supplies_usage got reported equally to all 4 spools regardless of which one was actually printing with. The fix: only fall back to a full-credit report when there is exactly one mapped slot (single-extruder, no ambiguity possible) - otherwise report nothing until real attribution data exists. Additionally, _on_print() now resets _spoolman_last_sync to the current time (not 0.0) at print start, so the first mid-print sync check is due only after a real sync_rate interval has passed. """ import time def _configure(bridge, slot_spools): bridge._spoolman_slot_spools = dict(slot_spools) bridge._spoolman_slot_usage = {} bridge._spoolman_slot_reported = {} async def test_unreported_reports_nothing_for_multiple_slots_without_attribution(client): _, bridge = client _configure(bridge, {0: 11, 1: 15, 2: 17, 3: 23}) bridge._state["supplies_usage"] = 4369 result = bridge._spoolman_unreported() assert result == {} async def test_unreported_credits_the_single_slot_without_attribution(client): _, bridge = client _configure(bridge, {0: 42}) bridge._state["supplies_usage"] = 500 result = bridge._spoolman_unreported() assert result == {0: 500} async def test_unreported_single_slot_subtracts_already_reported(client): _, bridge = client _configure(bridge, {0: 42}) bridge._spoolman_slot_reported = {0: 200.0} bridge._state["supplies_usage"] = 500 result = bridge._spoolman_unreported() assert result == {0: 300.0} async def test_unreported_uses_real_attribution_once_available(client): _, bridge = client _configure(bridge, {0: 11, 1: 15}) bridge._spoolman_slot_usage = {0: 120.0} bridge._state["supplies_usage"] = 120 result = bridge._spoolman_unreported() # Only the slot that was actually attributed usage gets a nonzero report; # the untouched slot must not receive any share of it. assert result[0] == 120.0 assert result[1] == 0.0 async def test_print_start_resets_last_sync_to_now_not_epoch(client): """_spoolman_last_sync=0.0 at print start meant the poll loop's mid-print sync check (time.time() - last_sync >= sync_rate) was true on the very first tick, before _spoolman_attribute_tick() ever ran once - triggering the fallback-split bug above immediately after print start, before any real per-slot usage existed yet.""" _, bridge = client before = time.time() bridge._on_print({"state": "printing", "data": {"filename": "test.gcode"}}) assert bridge._spoolman_last_sync >= before assert bridge._spoolman_last_sync <= time.time()