fix(vp): show target printer's serial on proxy-mode VP card

The runtime services (SSDP, MQTT bind identity, cert subject) already
  advertise the target printer's serial via target_printer_serial or
  self.serial in proxy mode, but the API response that drives the VP
  settings card always returned the self-generated suffix-based serial.
  The card therefore displayed a serial that didn't match what slicers
  see, breaking the "one identity per VP" mental model.

  _vp_to_dict now resolves vp.target_printer_id -> Printer.serial_number
  when mode == VP_MODE_PROXY and substitutes the result into the response
  serial field. Archive / queue / review keep the self-generated serial
  (those modes never speak the target's identity). Orphaned target falls
  back to self-generated so the card still renders.
This commit is contained in:
maziggy
2026-06-08 09:54:56 +02:00
parent e22c7f5d64
commit 7b4c5b3c1f
3 changed files with 102 additions and 6 deletions
@@ -599,3 +599,83 @@ class TestVirtualPrinterAccessCodeInheritance:
vp = (await db_session.execute(select(VirtualPrinter).where(VirtualPrinter.id == vp_id))).scalar_one()
assert vp.access_code == "REALCODE"
class TestVirtualPrinterSerialSurface:
"""Proxy-mode VPs must surface the target printer's serial in API responses.
The bridge advertises the target's serial over SSDP and forwards the
target's identity to the slicer; the VP-settings card should show the
same serial so the user sees one consistent identity per VP. Archive /
queue / review VPs keep the self-generated suffix-based serial since
those modes never speak the target's identity.
"""
@pytest.mark.asyncio
@pytest.mark.integration
async def test_proxy_vp_response_uses_target_printer_serial(self, async_client: AsyncClient, printer_factory):
target = await printer_factory(name="Real X1C", access_code="REALCODE", serial_number="00M09A123456789")
create_resp = await async_client.post(
"/api/v1/virtual-printers",
json={
"name": "ProxyVP",
"mode": "proxy",
"target_printer_id": target.id,
},
)
assert create_resp.status_code == 200
vp_id = create_resp.json()["id"]
assert create_resp.json()["serial"] == "00M09A123456789"
get_resp = await async_client.get(f"/api/v1/virtual-printers/{vp_id}")
assert get_resp.status_code == 200
assert get_resp.json()["serial"] == "00M09A123456789"
list_resp = await async_client.get("/api/v1/virtual-printers")
assert list_resp.status_code == 200
listed = next(p for p in list_resp.json()["printers"] if p["id"] == vp_id)
assert listed["serial"] == "00M09A123456789"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_non_proxy_vp_keeps_self_generated_serial(self, async_client: AsyncClient, printer_factory):
# Even with a target printer set (#1429 access-code inheritance flow),
# archive / queue / review VPs are NOT bridging the target's identity
# to the slicer — they synthesise their own. Self-generated serial.
target = await printer_factory(name="Real X1C", access_code="REALCODE", serial_number="00M09A123456789")
create_resp = await async_client.post(
"/api/v1/virtual-printers",
json={
"name": "QueueVP",
"mode": "queue",
"target_printer_id": target.id,
},
)
assert create_resp.status_code == 200
assert create_resp.json()["serial"] != "00M09A123456789"
# The synthesised serial follows _get_serial_for_model's `<prefix><suffix>`
# shape — model-specific prefix + 8-char hex suffix from `vp.serial_suffix`.
assert len(create_resp.json()["serial"]) >= 8
@pytest.mark.asyncio
@pytest.mark.integration
async def test_proxy_vp_falls_back_to_self_generated_when_target_missing(
self, async_client: AsyncClient, db_session
):
# Defensive fallback: a proxy VP whose target_printer_id points at a
# row that no longer exists (printer deleted mid-config, manual SQL
# tweak, race) must not 500 — it returns the self-generated serial
# so the card still renders and the user can fix the target.
from backend.app.models.virtual_printer import VirtualPrinter
vp = VirtualPrinter(name="OrphanProxy", mode="proxy", target_printer_id=99999, enabled=False)
db_session.add(vp)
await db_session.commit()
get_resp = await async_client.get(f"/api/v1/virtual-printers/{vp.id}")
assert get_resp.status_code == 200
body = get_resp.json()
assert body["serial"] # non-empty
assert len(body["serial"]) >= 8