@@ -93,3 +93,57 @@ async def test_printqueue_calls_mark_failed_on_printer_error() -> None:
9393 actual_uuid = store .mark_failed .call_args .args [0 ]
9494 assert actual_uuid == expected_uuid
9595 store .mark_done .assert_not_awaited ()
96+
97+
98+ @pytest .mark .asyncio
99+ async def test_stop_marks_inflight_jobs_as_failed_in_db () -> None :
100+ """stop() muss PRINTING-Jobs per store.mark_failed(id, 'shutdown') in DB persistieren.
101+
102+ Spec-Errata C2: Der In-Memory-Zustand wurde bereits vor diesem Fix korrekt
103+ auf FAILED gesetzt — aber der DB-Store-Aufruf fehlte (C-1 Fix).
104+ Dieser Test verifiziert dass stop() await self._store.mark_failed(UUID(job.id), 'shutdown')
105+ aufruft wenn ein Job beim Shutdown noch in PRINTING war.
106+ """
107+ import asyncio
108+
109+ store = AsyncMock (spec = MemoryJobStore )
110+
111+ # Printer der nie fertig wird — blockiert den Worker im print_image-Aufruf
112+ # bis der Test stop() aufruft und der Task gecancelled wird.
113+ printer = _make_printer ()
114+
115+ async def _blocking_print (* _args : object , ** _kwargs : object ) -> None :
116+ await asyncio .sleep (60 ) # blockiert bis CancelledError via stop()
117+
118+ printer .print_image = _blocking_print # type: ignore[assignment]
119+
120+ queue = PrintQueue (printers = [printer ], store = store )
121+ await queue .start ()
122+
123+ job_id = await queue .submit (_PRINTER_ID , _sample_image (), tape_mm = 12 )
124+
125+ # Kurz warten bis der Worker den Job in PRINTING übernommen hat.
126+ from app .services .job_lifecycle import JobState as InMemState
127+ for _ in range (50 ):
128+ job = await queue .get (job_id )
129+ if job .state == InMemState .PRINTING :
130+ break
131+ await asyncio .sleep (0.05 )
132+ else :
133+ pytest .fail ("Job erreichte PRINTING-State nicht innerhalb der Wartezeit" )
134+
135+ # stop() soll den Worker cancellen und dann PRINTING→FAILED + mark_failed aufrufen.
136+ await queue .stop (timeout_s = 0.1 )
137+
138+ # Verifizierung: mark_failed muss mit der richtigen UUID und error='shutdown' aufgerufen worden sein.
139+ expected_uuid = UUID (job_id )
140+ # mark_failed kann mehrfach aufgerufen werden (einmal durch Worker-CancelledError-Pfad
141+ # und einmal durch stop()-Cleanup) — mindestens ein Call muss (uuid, 'shutdown') sein.
142+ shutdown_calls = [
143+ call for call in store .mark_failed .call_args_list
144+ if call .args == (expected_uuid , "shutdown" )
145+ ]
146+ assert shutdown_calls , (
147+ f"store.mark_failed(UUID(job_id), 'shutdown') wurde nicht aufgerufen. "
148+ f"Tatsächliche Calls: { store .mark_failed .call_args_list } "
149+ )
0 commit comments