"""Unit-Tests für Spulenwahl und Steuer-Parameter-Aliase.""" import unittest from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock, patch from grotti import jobs from grotti import moonraker class ChooseSpoolTests(unittest.TestCase): def test_rejects_spool_created_after_print_end(self): end = datetime(2026, 10, 1, 12, 0, tzinfo=timezone.utc) old = { "id": 1, "material_type": "PLA", "created_at": end - timedelta(days=2), } new = { "id": 2, "material_type": "PLA", "created_at": end + timedelta(hours=1), } conn = MagicMock() with patch("grotti.jobs.store.spools_for_printer", return_value=[new]): with patch("grotti.jobs.store.get_spool_row", return_value=new): self.assertIsNone(jobs.choose_spool(conn, "h", "PLA", 2, ended_at=end)) with patch("grotti.jobs.store.spools_for_printer", return_value=[old]): self.assertEqual(jobs.choose_spool(conn, "h", "PLA", None, ended_at=end)["id"], 1) def test_prefers_host_spool_over_stale_hint(self): """Rollenwechsel: Hint im Regal, neue Rolle am Drucker → Abzug auf die neue.""" end = datetime(2026, 10, 18, 14, 58, tzinfo=timezone.utc) hinted = { "id": 4, "material_type": "PLA", "printer_host": "", "created_at": end - timedelta(days=5), } current = { "id": 14, "material_type": "PLA", "printer_host": "h", "created_at": end - timedelta(minutes=10), } conn = MagicMock() with patch("grotti.jobs.store.spools_for_printer", return_value=[current]): with patch("grotti.jobs.store.get_spool_row", return_value=hinted): self.assertEqual(jobs.choose_spool(conn, "h", "PLA", 4, ended_at=end)["id"], 14) def test_keeps_hint_when_no_host_spool(self): end = datetime(2026, 10, 18, 14, 58, tzinfo=timezone.utc) hinted = { "id": 4, "material_type": "PLA", "printer_host": "", "created_at": end - timedelta(days=5), } conn = MagicMock() with patch("grotti.jobs.store.spools_for_printer", return_value=[]): with patch("grotti.jobs.store.get_spool_row", return_value=hinted): self.assertEqual(jobs.choose_spool(conn, "h", "PLA", 4, ended_at=end)["id"], 4) def test_resolve_tracking_spool_prefers_host(self): conn = MagicMock() with patch( "grotti.jobs.resolve_spools_at_host", return_value=[{"id": 14, "printer_host": "h"}], ): self.assertEqual(jobs.resolve_tracking_spool(conn, "h", 4), 14) def test_ace_target_skips_spool_created_after_end(self): end = datetime(2026, 10, 15, 22, 0, tzinfo=timezone.utc) future = {"id": 18, "created_at": end + timedelta(days=6)} conn = MagicMock() with patch("grotti.jobs.store.spool_at_slot", return_value=future): self.assertEqual( jobs._targets_from_ace(conn, "h", [{"ams_index": 3}], ended_at=end), [], ) def test_spool_valid_for_end_naive(self): end = datetime(2026, 10, 1, 12, 0, tzinfo=timezone.utc) spool = {"created_at": datetime(2026, 9, 1, 12, 0)} self.assertTrue(jobs._spool_valid_for_end(spool, end)) class UndoDeductionTests(unittest.TestCase): def test_reopens_last_segment(self): conn = MagicMock() conn.execute.return_value.fetchall.return_value = [ {"spool_id": 7, "weight_g": 10}, ] run = {"id": 42, "deducted": True} self.assertTrue(jobs.undo_deduction(conn, run)) sqls = [str(call.args[0]) for call in conn.execute.call_args_list] self.assertTrue(any("UPDATE run_segments" in sql and "mm_end = NULL" in sql for sql in sqls)) class SteuernAliasTests(unittest.TestCase): @patch("grotti.moonraker.gcode_script", return_value={"ok": True}) def test_value_alias_for_temp(self, gcode): moonraker.steuern("10.0.0.1", "nozzle", {"value": 210}) script = gcode.call_args[0][1] self.assertIn("TARGET=210.0", script) @patch("grotti.moonraker.gcode_script", return_value={"ok": True}) def test_percent_alias_for_speed(self, gcode): moonraker.steuern("10.0.0.1", "speed", {"value": 120}) script = gcode.call_args[0][1] self.assertIn("M220 S120", script) class HostValidationTests(unittest.TestCase): def test_validate_printer_host(self): from web.app import validate_printer_host from grotti.errors import GrottiError self.assertEqual(validate_printer_host("10.10.1.60"), "10.10.1.60") self.assertEqual(validate_printer_host("kobra.local:80"), "kobra.local:80") self.assertEqual(validate_printer_host("COM5@250000"), "COM5@250000") self.assertEqual(validate_printer_host("/dev/ttyUSB0"), "/dev/ttyUSB0") with self.assertRaises(GrottiError): validate_printer_host("http://evil") with self.assertRaises(GrottiError): validate_printer_host("../etc/passwd") if __name__ == "__main__": unittest.main()