mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
Reporter sliced in OrcaSlicer with timelapse on, sent the job to a VP queue, started from the queue, and got no timelapse video. Their dispatch chain itself was correct (queue item -> scheduler -> MQTT command honors `timelapse`); the gap was at queue-add time. The VP's `_add_to_print_queue` reads `default_timelapse` (and the four other print-option settings) from the workflow settings card. That was introduced in #1235 to stop column-level defaults from winning. But it also discarded the slicer's actual choice carried on the MQTT `project_file` command, which all the slicers (Studio / Handy / Orca) ship as `timelapse: true|1`. Result: a user with the new-install value `default_timelapse=false` had to either flip the global setting or edit every queue item by hand, even though their slicer's "Print options" UI clearly said "record timelapse". Investigation went wider than #1403 because Martin's hypothesis was "the print options modal isn't respected either." Cross-checking 86 captured P1S `project_file` commands across the support packages shows 46 from the queue scheduler and 33 from background_dispatch emitting `"timelapse": true` correctly to real printers - the modal + re-print path is intact end-to-end. The slicer-side gap was the only real bug. Two unrelated dead-code issues turned up in the same dig and are folded in below. Fix (VP queue inheritance) - `on_print_command` in the VP manager now stashes the slicer's project_file dict keyed by filename, then signals an asyncio.Event. - `_add_to_print_queue` checks the dict first; if empty, creates the event and waits up to 2 s for it before reading the settings fallback. Each option flows through per-field - slicer value wins if present, else the existing settings default (so users who explicitly set `default_timelapse=true` in their VP workflow card still get that on slicers that don't send a print command). - MQTT field naming preserved exactly: `bed_leveling` (single L) on the wire stays mapped to `bed_levelling` (double L) on the Bambuddy column. Integer 0/1 from H-family slicers and bool true/false from P1/X1 slicers both coerce via `bool()`. - Capture is gated on `mode == "print_queue"` so immediate / review / proxy modes keep their pre-fix no-op `on_print_command` and don't accumulate stashed entries over the VP's uptime. - Wait is also skipped when there's no MQTT server attached (`self._mqtt is None`), so unit tests that invoke `_add_to_print_queue` directly don't pay the 2 s tax. - Capture is consumed on use so the dict stays bounded. - `printer_manager.get_status(...).get(...)` against a `PrinterState` dataclass that has no `.get()` method. - Every print option discarded (timelapse, bed_levelling, AMS mapping). The route 500'd before ever reaching the printer. Rewritten to mirror `POST /print-queue/{item_id}/start`: clear `manual_start=False` on the next pending queue item and let the scheduler dispatch with the queue's stored options intact. Response shape preserved. Side-bug b: vibration_cali default drift in background_dispatch - `ReprintRequest.vibration_cali` and `FilePrintRequest.vibration_cali` both default to `True` (matches Bambu Studio behavior for X1/P1). - Both `_process_job` call sites read `job.options.get("vibration_cali", False)`. Cosmetic today because the frontend always sends the field, but a latent landmine for any future caller that bypasses the schema. Both sites flipped to `True`.
131 lines
4.4 KiB
Python
131 lines
4.4 KiB
Python
"""Regression tests for the webhook `/printer/{id}/start` route.
|
|
|
|
The previous implementation called `printer_manager.start_print()` directly
|
|
with `queue_item.archive_id` (an int) as the filename arg and no print
|
|
options, and used `await` on a non-async function. That route 500'd on
|
|
every invocation. The fix mirrors `POST /print-queue/{item_id}/start`:
|
|
clear the next pending item's `manual_start` so the scheduler picks it up
|
|
with the queue's stored options (timelapse, bed_levelling, etc.) intact.
|
|
"""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
@pytest.fixture
|
|
async def api_key_data(async_client: AsyncClient, db_session):
|
|
"""Create an API key with control_printer permission."""
|
|
from backend.app.core.auth import generate_api_key
|
|
from backend.app.models.api_key import APIKey
|
|
|
|
full_key, key_hash, key_prefix = generate_api_key()
|
|
api_key = APIKey(
|
|
name="webhook-test-key",
|
|
key_hash=key_hash,
|
|
key_prefix=key_prefix,
|
|
can_queue=True,
|
|
can_control_printer=True,
|
|
can_read_status=True,
|
|
enabled=True,
|
|
)
|
|
db_session.add(api_key)
|
|
await db_session.commit()
|
|
return full_key
|
|
|
|
|
|
@pytest.fixture
|
|
async def printer_with_queue(db_session):
|
|
"""Create a printer and a pending queue item with manual_start=True."""
|
|
from backend.app.models.print_queue import PrintQueueItem
|
|
from backend.app.models.printer import Printer
|
|
|
|
printer = Printer(
|
|
name="WebhookTest",
|
|
ip_address="192.168.1.42",
|
|
access_code="12345678",
|
|
serial_number="00M00A000000000",
|
|
model="P1S",
|
|
)
|
|
db_session.add(printer)
|
|
await db_session.commit()
|
|
|
|
item = PrintQueueItem(
|
|
printer_id=printer.id,
|
|
position=1,
|
|
status="pending",
|
|
manual_start=True,
|
|
timelapse=True,
|
|
bed_levelling=True,
|
|
flow_cali=False,
|
|
vibration_cali=True,
|
|
layer_inspect=False,
|
|
use_ams=True,
|
|
)
|
|
db_session.add(item)
|
|
await db_session.commit()
|
|
return printer, item
|
|
|
|
|
|
class TestWebhookStartPrint:
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.integration
|
|
async def test_clears_manual_start_on_next_pending_item(
|
|
self, async_client: AsyncClient, db_session, api_key_data, printer_with_queue
|
|
):
|
|
"""The webhook flips manual_start to False so the scheduler picks it up.
|
|
|
|
Pre-fix the route called `printer_manager.start_print()` directly
|
|
with no options and `archive_id` (int) as the filename — 500'd on
|
|
every invocation. Now it mirrors the regular `/print-queue/{id}/start`
|
|
affordance: scheduler dispatch handles FTP upload and all print
|
|
options via the queue's stored fields.
|
|
"""
|
|
printer, item = printer_with_queue
|
|
|
|
resp = await async_client.post(
|
|
f"/api/v1/webhook/printer/{printer.id}/start",
|
|
headers={"X-API-Key": api_key_data},
|
|
)
|
|
|
|
assert resp.status_code == 200, resp.text
|
|
assert resp.json()["queue_item_id"] == item.id
|
|
|
|
await db_session.refresh(item)
|
|
assert item.manual_start is False, "manual_start must be cleared so scheduler dispatches"
|
|
# Stored options must be untouched so the scheduler picks the user's choice.
|
|
assert item.timelapse is True
|
|
assert item.bed_levelling is True
|
|
assert item.vibration_cali is True
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.integration
|
|
async def test_returns_404_when_no_pending_items(self, async_client: AsyncClient, db_session, api_key_data):
|
|
from backend.app.models.printer import Printer
|
|
|
|
printer = Printer(
|
|
name="EmptyQueue",
|
|
ip_address="192.168.1.43",
|
|
access_code="12345678",
|
|
serial_number="00M00A000000001",
|
|
model="P1S",
|
|
)
|
|
db_session.add(printer)
|
|
await db_session.commit()
|
|
|
|
resp = await async_client.post(
|
|
f"/api/v1/webhook/printer/{printer.id}/start",
|
|
headers={"X-API-Key": api_key_data},
|
|
)
|
|
|
|
assert resp.status_code == 404
|
|
assert "No pending prints" in resp.json()["detail"]
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.integration
|
|
async def test_returns_404_when_printer_does_not_exist(self, async_client: AsyncClient, api_key_data):
|
|
resp = await async_client.post(
|
|
"/api/v1/webhook/printer/99999/start",
|
|
headers={"X-API-Key": api_key_data},
|
|
)
|
|
assert resp.status_code == 404
|