"""Optional auto-delete of a printed file from the printer's own storage after a successful print (Settings -> Print -> "Delete file from printer after successful print"). Only applies to files that are also backed by the bridge's own GCode store (otherwise the file would be gone with no copy left anywhere) and only on a clean "finished" state - not on stoped/canceled prints, and never when the setting is off (the default). """ import argparse import tempfile from unittest.mock import MagicMock from kobrax_moonraker_bridge import GCodeStore, KobraXBridge def _bridge(delete_after_print=1): c = MagicMock() c.callbacks = {} c.connected = False args = argparse.Namespace( printer_ip="", mqtt_port=9883, username="", password="", mode_id="20030", device_id="", host="127.0.0.1", port=7125, data_dir=tempfile.mkdtemp(prefix="kxdelafterprint-"), delete_printer_file_after_print=delete_after_print, ) store = GCodeStore(args.data_dir) b = KobraXBridge(c, args=args, store=store) return b, c def _seed_file(bridge, filename="test.gcode"): file_id = "abc123" bridge._store.save_file(file_id, filename, b"; gcode content") return file_id def _print_report(state, filename=None): payload = {"state": state, "data": {}} if filename is not None: payload["data"]["filename"] = filename return payload def test_finished_print_deletes_printer_file_when_enabled_and_in_store(): b, c = _bridge(delete_after_print=1) _seed_file(b, "test.gcode") b._on_print(_print_report("printing", "test.gcode")) assert b._current_job_id assert b._current_job_filename == "test.gcode" b._on_print(_print_report("finished")) delete_calls = [ call for call in c.publish.call_args_list if call.args[:2] == ("file", "deleteBatch") ] assert len(delete_calls) == 1 payload = delete_calls[0].args[2] assert payload == {"root": "local", "files": [{"path": "/", "filename": "test.gcode"}]} def test_finished_print_no_delete_when_setting_disabled(): b, c = _bridge(delete_after_print=0) _seed_file(b, "test.gcode") b._on_print(_print_report("printing", "test.gcode")) b._on_print(_print_report("finished")) delete_calls = [ call for call in c.publish.call_args_list if call.args[:2] == ("file", "deleteBatch") ] assert delete_calls == [] def test_finished_print_no_delete_when_file_not_in_bridge_store(): """Files started directly from the printer/Anycubic Slicer aren't in the bridge's own GCode store - must never be deleted, since that would leave no copy anywhere.""" b, c = _bridge(delete_after_print=1) # No _seed_file() call - the file is not in the store. b._on_print(_print_report("printing", "not_in_store.gcode")) assert not b._current_job_id # no store match -> no job tracked either b._on_print(_print_report("finished")) delete_calls = [ call for call in c.publish.call_args_list if call.args[:2] == ("file", "deleteBatch") ] assert delete_calls == [] def test_canceled_print_does_not_delete_file(): """Only a clean "finished" triggers the delete - a stopped/canceled print keeps its file, since the user may want to retry it.""" b, c = _bridge(delete_after_print=1) _seed_file(b, "test.gcode") b._on_print(_print_report("printing", "test.gcode")) b._on_print(_print_report("canceled")) delete_calls = [ call for call in c.publish.call_args_list if call.args[:2] == ("file", "deleteBatch") ] assert delete_calls == [] assert b._current_job_filename == "" def test_stoped_print_does_not_delete_file(): b, c = _bridge(delete_after_print=1) _seed_file(b, "test.gcode") b._on_print(_print_report("printing", "test.gcode")) b._on_print(_print_report("stoped")) delete_calls = [ call for call in c.publish.call_args_list if call.args[:2] == ("file", "deleteBatch") ] assert delete_calls == [] def test_current_job_filename_reset_after_finished(): """Regression guard: _current_job_filename must not leak into the next print's finished-handling if that next print isn't itself tracked.""" b, c = _bridge(delete_after_print=1) _seed_file(b, "test.gcode") b._on_print(_print_report("printing", "test.gcode")) b._on_print(_print_report("finished")) assert b._current_job_filename == "" # A second "finished" with no new job in between must not re-trigger a delete. c.publish.reset_mock() b._on_print(_print_report("finished")) delete_calls = [ call for call in c.publish.call_args_list if call.args[:2] == ("file", "deleteBatch") ] assert delete_calls == [] def test_delete_publish_failure_does_not_raise(): """A broken MQTT send during the delete request must not propagate out of _on_print() - it runs on the MQTT reader thread, and an unhandled exception there would break processing of subsequent messages.""" b, c = _bridge(delete_after_print=1) _seed_file(b, "test.gcode") c.publish.side_effect = RuntimeError("send failed") b._on_print(_print_report("printing", "test.gcode")) b._on_print(_print_report("finished")) # must not raise