| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- """The printer kill switch acts on prints Bambuddy did not start.
- Two ways it did not:
- - The fast check counted every print in ``_active_prints`` as Bambuddy's, and
- ``on_print_start`` puts every print it archives there, those started from
- Bambu Studio, Handy or the printer's screen included. A print running when the
- switch was turned on, or one archived before the switch looked, was never
- stopped.
- - A LAN print reports ``subtask_id`` "0". The database check answered "not sure
- yet" for it and waited, every frame, until the print was archived and the
- first hole let it through.
- Bambuddy's own prints are now the ones it dispatched: an expected print, the
- print user it recorded, or an archive it adopted for a dispatch or that carries
- its dispatch marker. A "0" print is matched to Bambuddy's printing archive on
- that printer by file name; if there is none it waits only while a queue job of
- Bambuddy's is printing there.
- """
- from types import SimpleNamespace
- from unittest.mock import AsyncMock
- import pytest
- from backend.app import main as main_module
- from backend.app.main import register_expected_print
- from backend.app.models.archive import PrintArchive
- from backend.tests.integration.test_external_print_confirmation_1898 import ( # noqa: F401
- SUBTASK,
- _clear_print_state,
- _drive_print_start,
- )
- pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
- @pytest.fixture(autouse=True)
- def _clear_owned(monkeypatch):
- main_module._owned_print_archive_ids.clear()
- monkeypatch.setattr(main_module.printer_manager, "get_current_print_user", lambda printer_id: None)
- yield
- main_module._owned_print_archive_ids.clear()
- def _status(subtask_id="0"):
- return SimpleNamespace(
- current_print=None, subtask_name=SUBTASK, subtask_id=subtask_id, gcode_file=f"{SUBTASK}.gcode.3mf"
- )
- async def test_a_print_bambuddy_did_not_start_stays_unauthorized_once_archived(test_engine, printer_factory):
- """The AP-1 scenario: archived by on_print_start, the print used to pass the
- fast check from then on."""
- printer_id = (await printer_factory()).id
- await _drive_print_start(test_engine, SimpleNamespace(id=printer_id), download_ok=True)
- assert any(key[0] == printer_id for key in main_module._active_prints)
- assert main_module._is_bambuddy_authorized_print_in_memory(printer_id, _status()) is False
- async def test_a_print_bambuddy_dispatched_is_its_own_once_adopted(test_engine, db_session, printer_factory):
- printer_id = (await printer_factory()).id
- archive = PrintArchive(
- printer_id=printer_id,
- filename=f"{SUBTASK}.gcode.3mf",
- file_path=f"archives/test/{SUBTASK}.gcode.3mf",
- file_size=1,
- print_name=SUBTASK,
- status="pending",
- )
- db_session.add(archive)
- await db_session.commit()
- register_expected_print(printer_id, f"{SUBTASK}.gcode.3mf", archive.id)
- await _drive_print_start(test_engine, SimpleNamespace(id=printer_id), download_ok=False)
- # Without the expected entries (print start consumes the names the printer
- # reported; the rest are evicted later), the print is still Bambuddy's.
- main_module._expected_prints.clear()
- assert archive.id in main_module._owned_print_archive_ids
- assert main_module._is_bambuddy_authorized_print_in_memory(printer_id, _status()) is True
- def _db(archive, dispatched_queue_item_id=None):
- """Both lookups: by subtask id (one row) and, for "0", Bambuddy's printing rows."""
- rows = [archive] if archive is not None else []
- return SimpleNamespace(
- execute=AsyncMock(return_value=SimpleNamespace(scalar_one_or_none=lambda: archive, scalars=lambda: rows)),
- scalar=AsyncMock(return_value=dispatched_queue_item_id),
- )
- def _archive(archive_id, filename, *, marker="run-1", print_name=None):
- return SimpleNamespace(
- id=archive_id, filename=filename, print_name=print_name, billing_run_id=marker, created_by_id=None
- )
- async def test_a_lan_print_with_no_dispatch_behind_it_is_unauthorized():
- assert await main_module._is_bambuddy_authorized_print(9, _status("0"), _db(None)) is False
- async def test_a_lan_print_matching_bambuddys_dispatched_archive_is_its_own():
- """After a restart: no id to match, so the file name and the dispatch marker."""
- archive = _archive(31, f"{SUBTASK}.gcode.3mf")
- assert await main_module._is_bambuddy_authorized_print(9, _status("0"), _db(archive)) is True
- assert 31 in main_module._owned_print_archive_ids
- async def test_a_lan_print_waits_while_bambuddy_has_a_job_printing_there():
- """Nothing of Bambuddy's by that name, but a queue job is printing there."""
- assert await main_module._is_bambuddy_authorized_print(9, _status("0"), _db(None, 401)) is None
- async def test_a_name_the_printer_echoes_with_underscores_still_matches():
- """Bambuddy uploads "My Part" and the printer reports "My_Part"."""
- archive = _archive(33, "My Part.gcode.3mf")
- state = SimpleNamespace(current_print=None, subtask_name="My_Part", subtask_id="0", gcode_file="My_Part.gcode.3mf")
- assert await main_module._is_bambuddy_authorized_print(9, state, _db(archive)) is True
- async def test_a_bambuddy_archive_of_another_name_does_not_cover_a_lan_print():
- other = _archive(34, "Other_job.gcode.3mf")
- assert await main_module._is_bambuddy_authorized_print(9, _status("0"), _db(other)) is False
- async def test_only_bambuddys_printing_archives_on_that_printer_are_read():
- from sqlalchemy.dialects import sqlite
- db = _db(None)
- await main_module._is_bambuddy_authorized_print(9, _status("0"), db)
- query = db.execute.await_args.args[0]
- sql = str(query.compile(dialect=sqlite.dialect(), compile_kwargs={"literal_binds": True}))
- assert "print_archives.printer_id = 9" in sql
- assert "print_archives.status = 'printing'" in sql
- assert "print_archives.billing_run_id IS NOT NULL" in sql
- async def test_a_lan_print_that_names_nothing_yet_waits():
- state = SimpleNamespace(current_print=None, subtask_name="", subtask_id="0", gcode_file="")
- db = _db(None)
- assert await main_module._is_bambuddy_authorized_print(9, state, db) is None
- db.execute.assert_not_awaited()
- async def test_the_printers_built_in_calibration_is_left_alone():
- """Bambuddy's Calibrate and the printer's screen play gcode from the
- printer's system partition (measured: /usr/etc/print/<model>/...)."""
- state = SimpleNamespace(
- current_print=None,
- subtask_name="auto_cali_for_user_param.gcode",
- subtask_id="0",
- gcode_file="/usr/etc/print/O1C2/auto_cali_for_user_param.gcode",
- )
- db = _db(None)
- assert await main_module._is_bambuddy_authorized_print(9, state, db) is True
- db.execute.assert_not_awaited()
- async def test_a_print_named_like_a_calibration_is_still_a_print():
- """Bambu Studio's pressure-advance print reports /data/Metadata/plate_1.gcode
- and subtask auto_pa_line_calib_mode (measured on an H2C), exactly as a user
- print given that name would."""
- state = SimpleNamespace(
- current_print=None,
- subtask_name="pa_line_calib_mode",
- subtask_id="0",
- gcode_file="/data/Metadata/plate_1.gcode",
- )
- assert await main_module._is_bambuddy_authorized_print(9, state, _db(None)) is False
- async def test_a_lan_print_is_not_covered_by_an_archive_that_has_its_own_id():
- from sqlalchemy.dialects import sqlite
- db = _db(None)
- await main_module._is_bambuddy_authorized_print(9, _status("0"), db)
- sql = str(db.execute.await_args.args[0].compile(dialect=sqlite.dialect(), compile_kwargs={"literal_binds": True}))
- assert "print_archives.subtask_id IS NULL" in sql
- async def test_a_missing_subtask_id_still_waits():
- """The first frames after a restart can arrive before the field is filled."""
- db = _db(None)
- assert await main_module._is_bambuddy_authorized_print(9, _status(None), db) is None
- db.execute.assert_not_awaited()
- async def test_a_finished_print_is_no_longer_owned():
- main_module._owned_print_archive_ids.add(41)
- main_module._active_prints[(9, f"{SUBTASK}.gcode.3mf")] = 41
- main_module._active_prints.clear()
- main_module._evict_stale_expected_prints()
- assert 41 not in main_module._owned_print_archive_ids
- @pytest.mark.parametrize(("marker", "owned"), [("run-7", True), (None, False)])
- async def test_a_print_resumed_after_a_restart_is_owned_only_with_bambuddys_marker(
- test_engine, db_session, printer_factory, marker, owned
- ):
- """on_print_start resumes the printing archive it finds by name; that
- archive is Bambuddy's only if Bambuddy dispatched it."""
- printer_id = (await printer_factory()).id
- archive = PrintArchive(
- printer_id=printer_id,
- filename=f"{SUBTASK}.gcode.3mf",
- file_path=f"archives/test/{SUBTASK}.gcode.3mf",
- file_size=1,
- print_name=SUBTASK,
- status="printing",
- billing_run_id=marker,
- )
- db_session.add(archive)
- await db_session.commit()
- await _drive_print_start(
- test_engine, SimpleNamespace(id=printer_id), download_ok=False, state_fields={"progress": 40}
- )
- assert (archive.id in main_module._owned_print_archive_ids) is owned
- async def test_a_bare_calibration_name_is_not_exempt_from_the_kill_switch():
- """A1, A1 Mini, P1 and A2L report their built-in calibration as a bare
- auto_cali_for_user_param.gcode, which a user's own file can be called too;
- nothing on these printers tells the two apart (no project URL is ever
- recorded there), so the kill switch does not exempt it (maintainer
- decision 2026-10-18)."""
- state = SimpleNamespace(
- current_print=None,
- subtask_name="auto_cali_for_user_param.gcode",
- subtask_id="0",
- gcode_file="auto_cali_for_user_param.gcode",
- current_project_url=None,
- )
- assert await main_module._is_bambuddy_authorized_print(9, state, _db(None)) is False
|