mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
When the user clicked Print Anyway on a filament-deficit warning, the
acknowledgement was one-shot. The route cleared manual_start and
filament_short, then the next scheduler tick re-ran
compute_deficit_for_queue_item against identical spool state, found
the same deficit, and re-set both flags. The item bounced between
"user said anyway" and "scheduler re-blocked" — every Play click
returned 409, every confirm got rolled back on the next tick.
Add a persistent acknowledgement flag on the queue item:
- New column `skip_filament_check` on print_queue. SQLite + Postgres
migration branched on is_sqlite() so Postgres doesn't reject
DEFAULT 0 on BOOLEAN.
- PrintQueueItemCreate + PrintQueueItemResponse schemas + the
TypeScript types carry the field.
- POST /print-queue/{id}/start with skip_filament_check=true now
ALSO sets item.skip_filament_check = True (not just clearing
manual_start / filament_short).
- PrintScheduler._block_on_filament_deficit short-circuits to
False — no compute, no flag-setting, no notification — when
item.skip_filament_check is True. We trust the operator's
decision and stop fighting them.
- PrintModal at queue-creation time threads
skip_filament_check=true into the create payload when the user
clicks Print Anyway on the frontend deficit warning, so a print
that was warned-then-acknowledged at add-to-queue time goes in
pre-acknowledged — scheduler never blocks it on first tick.
Flag is not auto-cleared on spool swap by design: if remaining is
now sufficient, the check returns no deficit anyway, so the flag
is moot. Auto-clearing would add lifecycle complexity without
changing behaviour.
AMS Backup awareness (the other half of the discussion) intentionally
NOT included — verified the H2D's bit-26 of print.cfg toggles with
the printer-side AMS Backup setting, but the X1C's cfg has a
different shape entirely and verifying every model family isn't
realistic. Silently under-warning would be worse than always
per-slot. The check stays single-slot for now.
158 lines
5.3 KiB
Python
158 lines
5.3 KiB
Python
"""Scheduler pre-dispatch filament-deficit guard tests (#1496).
|
|
|
|
``PrintScheduler._block_on_filament_deficit`` is the gate that keeps an
|
|
auto_dispatch=True VP intake (or any other scheduler-driven dispatch) from
|
|
sending a print onto a spool that can't satisfy it. On a deficit it
|
|
promotes the item to manual_start; when a previously-flagged item's spool
|
|
is now adequate it clears the flag so the next tick dispatches.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from backend.app.models.print_queue import PrintQueueItem
|
|
from backend.app.services.filament_deficit import FilamentDeficit
|
|
from backend.app.services.print_scheduler import PrintScheduler
|
|
|
|
|
|
@pytest.fixture
|
|
def scheduler():
|
|
"""A fresh scheduler instance — internal state is not exercised."""
|
|
return PrintScheduler()
|
|
|
|
|
|
@pytest.fixture
|
|
def queue_item(db_session, printer_factory):
|
|
"""Helper to drop a queue item the helper can mutate."""
|
|
|
|
async def _make(**overrides):
|
|
printer = await printer_factory()
|
|
defaults = {
|
|
"printer_id": printer.id,
|
|
"status": "pending",
|
|
"manual_start": False,
|
|
"filament_short": False,
|
|
}
|
|
defaults.update(overrides)
|
|
item = PrintQueueItem(**defaults)
|
|
db_session.add(item)
|
|
await db_session.commit()
|
|
await db_session.refresh(item)
|
|
return item
|
|
|
|
return _make
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_blocks_on_deficit_promotes_to_manual_start(scheduler, db_session, queue_item):
|
|
item = await queue_item()
|
|
with patch(
|
|
"backend.app.services.print_scheduler.compute_deficit_for_queue_item",
|
|
AsyncMock(
|
|
return_value=[
|
|
FilamentDeficit(
|
|
slot_id=1,
|
|
ams_id=0,
|
|
tray_id=0,
|
|
filament_type="PLA",
|
|
required_grams=270.0,
|
|
remaining_grams=200.0,
|
|
),
|
|
]
|
|
),
|
|
):
|
|
blocked = await scheduler._block_on_filament_deficit(db_session, item)
|
|
|
|
assert blocked is True
|
|
await db_session.refresh(item)
|
|
assert item.manual_start is True
|
|
assert item.filament_short is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clears_stale_flag_when_deficit_resolves(scheduler, db_session, queue_item):
|
|
"""Previously-flagged item whose spool was swapped is unblocked."""
|
|
item = await queue_item(filament_short=True, manual_start=False)
|
|
with patch(
|
|
"backend.app.services.print_scheduler.compute_deficit_for_queue_item",
|
|
AsyncMock(return_value=[]),
|
|
):
|
|
blocked = await scheduler._block_on_filament_deficit(db_session, item)
|
|
|
|
assert blocked is False
|
|
await db_session.refresh(item)
|
|
assert item.filament_short is False
|
|
assert item.manual_start is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_deficit_no_op(scheduler, db_session, queue_item):
|
|
"""Happy path — no deficit, no flag changes, dispatch proceeds."""
|
|
item = await queue_item()
|
|
with patch(
|
|
"backend.app.services.print_scheduler.compute_deficit_for_queue_item",
|
|
AsyncMock(return_value=[]),
|
|
):
|
|
blocked = await scheduler._block_on_filament_deficit(db_session, item)
|
|
|
|
assert blocked is False
|
|
await db_session.refresh(item)
|
|
assert item.filament_short is False
|
|
assert item.manual_start is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_helper_exception_does_not_wedge_dispatch(scheduler, db_session, queue_item):
|
|
"""A flaky deficit check (e.g. Spoolman timeout) must not block dispatch."""
|
|
item = await queue_item()
|
|
with patch(
|
|
"backend.app.services.print_scheduler.compute_deficit_for_queue_item",
|
|
AsyncMock(side_effect=RuntimeError("network down")),
|
|
):
|
|
blocked = await scheduler._block_on_filament_deficit(db_session, item)
|
|
|
|
assert blocked is False
|
|
await db_session.refresh(item)
|
|
assert item.filament_short is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_filament_check_short_circuits_without_compute(scheduler, db_session, queue_item):
|
|
"""User clicked Print Anyway (skip_filament_check=True): no compute, no flag (#1698-followup).
|
|
|
|
Pre-fix the scheduler re-ran the deficit check on every tick, re-set
|
|
manual_start/filament_short to True, and the item bounced between
|
|
"user said anyway" (route clears flags) and "scheduler re-blocked"
|
|
forever. With the persistent acknowledgement flag the scheduler bails
|
|
early without even touching the deficit helper.
|
|
"""
|
|
item = await queue_item(skip_filament_check=True)
|
|
compute_mock = AsyncMock(
|
|
return_value=[
|
|
FilamentDeficit(
|
|
slot_id=1,
|
|
ams_id=0,
|
|
tray_id=0,
|
|
filament_type="PLA",
|
|
required_grams=270.0,
|
|
remaining_grams=200.0,
|
|
),
|
|
]
|
|
)
|
|
with patch(
|
|
"backend.app.services.print_scheduler.compute_deficit_for_queue_item",
|
|
compute_mock,
|
|
):
|
|
blocked = await scheduler._block_on_filament_deficit(db_session, item)
|
|
|
|
assert blocked is False
|
|
compute_mock.assert_not_awaited()
|
|
await db_session.refresh(item)
|
|
# Flags must not get re-set by the scheduler now that the user has
|
|
# acknowledged the deficit.
|
|
assert item.filament_short is False
|
|
assert item.manual_start is False
|