Files
bambuddy/backend/tests/integration/test_webhook_start_print.py
T
maziggy 173edd9b7c ● fix(vp-queue): inherit slicer print options instead of always using defaults (#1403)
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`.
2026-05-18 09:38:53 +02:00

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