@@ -95,15 +95,18 @@ def backend():
9595_PRINTER_ID = UUID ("bbbbbbbb-0000-0000-0000-000000000001" )
9696
9797
98- def _service (loader , renderer , queue , lookup_service , backend ):
99- return PrintService (
98+ def _service (loader , renderer , queue , lookup_service , backend , store = None ):
99+ kwargs = dict (
100100 template_loader = loader ,
101101 renderer = renderer ,
102102 print_queue = queue ,
103103 lookup_service = lookup_service ,
104104 printer_id = _PRINTER_ID ,
105105 backend = backend ,
106106 )
107+ if store is not None :
108+ kwargs ["store" ] = store
109+ return PrintService (** kwargs )
107110
108111
109112# ---------------------------------------------------------------------------
@@ -470,3 +473,87 @@ async def print_image(self, image, *, tape_mm, **kw):
470473 assert job .state == JobState .PAUSED , f"Expected PAUSED, got { job .state } "
471474 assert job .error_code == "tape_mismatch"
472475 assert job .error_detail == {"expected_mm" : 24 , "loaded_mm" : 12 }
476+
477+
478+ # ---------------------------------------------------------------------------
479+ # Fix C-1: PAUSED-Pfad ruft save_queued NICHT auf
480+ # ---------------------------------------------------------------------------
481+
482+
483+ async def test_tape_mismatch_queue_path_does_not_persist_db_job (
484+ loader , renderer , queue , lookup_service , backend
485+ ) -> None :
486+ """C-1-Fix: on_tape_mismatch=queue → save_queued wird NICHT aufgerufen.
487+
488+ Vorher wurde der Job als QUEUED persistiert, obwohl er in PAUSED-State
489+ versetzt wurde. Nach Hub-Restart würde list_pending() ihn als QUEUED
490+ finden und sofort drucken — Doppel-Druck-Risiko.
491+ Fix: PAUSED-Jobs bleiben in-memory-only, kein DB-Persist.
492+ """
493+ backend .preflight_check .return_value = PreflightStatus (
494+ hr_printer_status = "idle" ,
495+ loaded_tape_mm = 12 , # mismatch: template wants 24mm
496+ error_flags = [],
497+ )
498+ job = Job (id = "job-1" , printer_id = _PRINTER_ID , image_payload = b"" , tape_mm = 24 , options = {})
499+ from app .services .job_lifecycle import JobStateMachine
500+
501+ JobStateMachine .transition (job , JobState .PAUSED )
502+ queue .submit_paused_with_id .return_value = _FAKE_JOB_UUID
503+ queue .get .return_value = job
504+
505+ mock_store = AsyncMock ()
506+
507+ svc = _service (loader , renderer , queue , lookup_service , backend , store = mock_store )
508+ req = PrintRequest (
509+ template_id = "qr-only-24mm" ,
510+ data = RawLabelData (title = "T" , primary_id = "P" , qr_payload = "Q" ),
511+ on_tape_mismatch = "queue" ,
512+ )
513+
514+ job_id = await svc .submit_print_job (req )
515+
516+ assert isinstance (job_id , UUID )
517+ # save_queued darf NICHT aufgerufen worden sein — kein DB-Persist für PAUSED
518+ mock_store .save_queued .assert_not_awaited ()
519+ # submit_paused_with_id muss aber aufgerufen worden sein
520+ queue .submit_paused_with_id .assert_awaited_once ()
521+
522+
523+ # ---------------------------------------------------------------------------
524+ # Fix I-1: Rollback bei queue.submit_with_id Fehler
525+ # ---------------------------------------------------------------------------
526+
527+
528+ async def test_submit_with_failing_queue_marks_db_job_failed (
529+ loader , renderer , queue , lookup_service , backend
530+ ) -> None :
531+ """I-1-Fix: Wenn queue.submit_with_id wirft, muss der DB-Job auf FAILED gesetzt werden.
532+
533+ Ohne Rollback bliebe eine stale QUEUED-Row in der DB ohne Worker-Gegenstück.
534+ Nach Hub-Restart würde list_pending() sie finden und re-enqueuen — aber der
535+ Job hat keinen gültigen Zustand mehr.
536+ Fix: try/except um submit_with_id, bei Exception → mark_failed + re-raise.
537+ """
538+ submit_error = RuntimeError ("asyncio.Queue voll oder andere Fehlerursache" )
539+ queue .submit_with_id .side_effect = submit_error
540+
541+ mock_store = AsyncMock ()
542+
543+ svc = _service (loader , renderer , queue , lookup_service , backend , store = mock_store )
544+ req = PrintRequest (
545+ template_id = "qr-only-24mm" ,
546+ data = RawLabelData (title = "T" , primary_id = "P" , qr_payload = "Q" ),
547+ )
548+
549+ with pytest .raises (RuntimeError ):
550+ await svc .submit_print_job (req )
551+
552+ # save_queued muss aufgerufen worden sein (DB-Persist vor submit)
553+ mock_store .save_queued .assert_awaited_once ()
554+ # mark_failed muss aufgerufen worden sein mit passendem error-String
555+ mock_store .mark_failed .assert_awaited_once ()
556+ call_args = mock_store .mark_failed .call_args
557+ error_msg : str = call_args .args [1 ] if call_args .args else call_args .kwargs ["error" ]
558+ assert "submit_failed" in error_msg
559+ assert "RuntimeError" in error_msg
0 commit comments