mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
#1429 (reported by @TrickShotMLG02, confirmed by @Mape6 on a flat single-LAN that rules out subnet / mDNS-reflector theories): with the physical printer off the slicer's "Send" landed in Bambuddy's archive; once the printer powered on every subsequent "Send" went straight to the printer's SD card and bypassed Bambuddy. Bundle analysis: mape6-before showed clean FTP receive + archive lines, mape6-after had zero FTP attempts to Bambuddy once the printer was online. Cause: mqtt_bridge.py::_resolve_client encoded _target_ip_uint32_le / _vp_ip_uint32_le ONLY on client-identity change and early-returned on every refresh tick when the same client object was still bound. If target_client.ip_address was empty at first bind (DB row stale, or client constructed before SSDP refresh filled it in), the encoding stayed None, the net.info[*].ip rewrite block was skipped, the cache filled with the real printer IP, sticky-key preservation kept the poisoned net value alive across every subsequent incremental push, and the slicer followed the leaked IP. Only Bambuddy-restart-with-printer-off cleared it — the workaround both reporters independently arrived at. Same shape on multi-NIC printers (X1C, H2D Pro): the rewrite only matched entries whose ip equalled _target_ip_uint32_le, so a secondary interface IP Bambuddy never saw would leak through unchanged. Bridge fix: - _resolve_client calls a new _refresh_ip_encoding() on every refresh tick, even when client identity is unchanged; self-heals once ip_address becomes valid. - _refresh_ip_encoding() sweeps the existing _latest_print_state when encoding becomes valid for the first time. Without the sweep, sticky-key preservation keeps the pre-arm poisoned cache alive forever — incremental pushes that don't include net carry the bad value forward. - _rewrite_net_info_ips() rewrites EVERY non-zero net.info[].ip entry that doesn't already equal the VP IP, not just entries matching _target_ip_uint32_le. Multi-NIC printers stop leaking secondary interfaces. Zero-IP placeholders are left alone so "active interface" detection still works. - INFO logging on encoding arm/update and on cache sweep so future bundles directly answer "did the rewrite fire?". Mode wire-value rename (#1429 follow-up, separate confusion source): - Both reporters' support bundles showed mode: immediate while the UI said "Archive"; @TrickShotMLG02 quoted: "I have no idea why it says immediate in the support-info.json file. In the webui the printer is set to archive". UI button "Archive" had always saved immediate, and "Queue" had always saved print_queue. Canonical wire values are now archive / review / queue / proxy, matching the button labels 1:1. - New normalize_vp_mode() + VP_MODE_* constants in models/virtual_printer.py; manager.py normalises on construction so a legacy row read pre-migration still dispatches correctly. - core/database.py::run_migrations rewrites existing virtual_printers and settings rows; idempotent (re-runs are no-ops); identical SQL under SQLite and Postgres. - API routes accept both legacy and canonical on input, normalise before storage. GET /settings/virtual-printer normalises on read so the frontend's mode-button highlight works for stale legacy values. - Three frontend VP components (VirtualPrinterSettings, VirtualPrinterCard, VirtualPrinterAddDialog) switched click handlers and type aliases to canonical; each got its own normalizeMode() helper so a stale-cached settings payload still highlights the right button. Two pre-existing `printer.mode === 'queue' ? 'review'` legacy mappings in VirtualPrinterCard were the source of a test failure caught mid-implementation where the new canonical 'queue' was being mis-aliased back to 'review' and hiding the auto-dispatch + force-color-match toggles. mode handler is NOT the dispatch bug: manager.py::_archive_file (the handler for archive mode) doesn't dispatch to the physical printer. The "files end up on the printer's SD card" symptom was the IP-leak from the bridge cache. The mode rename is purely clarity / support- bundle accuracy.
152 lines
5.0 KiB
Python
152 lines
5.0 KiB
Python
"""Regression test for the VP mode wire-value rename migration (#1429 follow-up).
|
|
|
|
The UI buttons "Archive" and "Queue" had always saved the wire values
|
|
`immediate` and `print_queue` — confusing in every support bundle. The
|
|
rename migration in ``run_migrations`` rewrites existing rows to the
|
|
canonical names. This test verifies it on both fresh and legacy schemas
|
|
and confirms it's idempotent so reruns are safe (boot-on-boot).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
|
|
from backend.app.core.database import run_migrations
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def force_sqlite_dialect(monkeypatch):
|
|
"""Force the SQLite branch regardless of test env settings."""
|
|
from backend.app.core import db_dialect
|
|
|
|
monkeypatch.setattr(db_dialect, "is_sqlite", lambda: True)
|
|
monkeypatch.setattr(db_dialect, "is_postgres", lambda: False)
|
|
from backend.app.core import database as database_module
|
|
|
|
monkeypatch.setattr(database_module, "is_sqlite", lambda: True)
|
|
|
|
|
|
def _register_all_models():
|
|
"""run_migrations touches multiple tables; the full schema must exist."""
|
|
from backend.app.models import ( # noqa: F401
|
|
ams_history,
|
|
ams_label,
|
|
api_key,
|
|
archive,
|
|
color_catalog,
|
|
external_link,
|
|
filament,
|
|
group,
|
|
kprofile_note,
|
|
maintenance,
|
|
notification,
|
|
notification_template,
|
|
print_log,
|
|
print_queue,
|
|
printer,
|
|
project,
|
|
project_bom,
|
|
settings,
|
|
slot_preset,
|
|
smart_plug,
|
|
smart_plug_energy_snapshot,
|
|
spool,
|
|
spool_assignment,
|
|
spool_catalog,
|
|
spool_k_profile,
|
|
spool_usage_history,
|
|
spoolbuddy_device,
|
|
user,
|
|
user_email_pref,
|
|
virtual_printer,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def engine():
|
|
from backend.app.core.database import Base
|
|
|
|
_register_all_models()
|
|
|
|
eng = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
|
|
async with eng.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
yield eng
|
|
await eng.dispose()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_legacy_mode_rows_get_canonical_names(engine):
|
|
"""Existing rows with `immediate` / `print_queue` get rewritten to
|
|
`archive` / `queue` while canonical values and unrelated modes pass
|
|
through untouched."""
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO virtual_printers (id, name, enabled, mode, serial_suffix, position) VALUES "
|
|
"(1, 'A', 0, 'immediate', '391800001', 1),"
|
|
"(2, 'B', 0, 'print_queue', '391800002', 2),"
|
|
"(3, 'C', 0, 'review', '391800003', 3),"
|
|
"(4, 'D', 0, 'proxy', '391800004', 4),"
|
|
"(5, 'E', 0, 'archive', '391800005', 5),"
|
|
"(6, 'F', 0, 'queue', '391800006', 6)"
|
|
)
|
|
)
|
|
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
|
|
async with engine.connect() as conn:
|
|
result = await conn.execute(text("SELECT id, mode FROM virtual_printers ORDER BY id"))
|
|
rows = dict(result.fetchall())
|
|
|
|
assert rows[1] == "archive" # immediate → archive
|
|
assert rows[2] == "queue" # print_queue → queue
|
|
assert rows[3] == "review" # untouched
|
|
assert rows[4] == "proxy" # untouched
|
|
assert rows[5] == "archive" # already canonical
|
|
assert rows[6] == "queue" # already canonical
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_legacy_settings_row_gets_canonical_name(engine):
|
|
"""The legacy single-VP `virtual_printer_mode` setting also gets renamed
|
|
so the GET response (which feeds the support bundle and the settings
|
|
page) reads the canonical name."""
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text("INSERT INTO settings (key, value) VALUES ('virtual_printer_mode', 'immediate')"))
|
|
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
|
|
async with engine.connect() as conn:
|
|
result = await conn.execute(text("SELECT value FROM settings WHERE key = 'virtual_printer_mode'"))
|
|
value = result.scalar()
|
|
|
|
assert value == "archive"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_migration_is_idempotent(engine):
|
|
"""Running the migration twice must be a no-op on canonical values —
|
|
every boot re-runs the migration set."""
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO virtual_printers (id, name, enabled, mode, serial_suffix, position) "
|
|
"VALUES (1, 'A', 0, 'immediate', '391800001', 1)"
|
|
)
|
|
)
|
|
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
# Second run on already-canonical values.
|
|
async with engine.begin() as conn:
|
|
await run_migrations(conn)
|
|
|
|
async with engine.connect() as conn:
|
|
result = await conn.execute(text("SELECT mode FROM virtual_printers WHERE id = 1"))
|
|
assert result.scalar() == "archive"
|