@@ -496,6 +496,77 @@ def test_restart_action_service_exception_shows_warning(self, widget, monkeypatc
496496
497497 mock_messagebox .warning .assert_called_once ()
498498
499+ # --- restarting several selected rows ---
500+
501+ FILE_ROW_ID = "11111111-1111-1111-1111-111111111111"
502+ URL_ROW_ID = "22222222-2222-2222-2222-222222222222"
503+
504+ def _select_both_rows (self , widget , db , file_row_status , url_row_status ):
505+ """Give both rows a UUID id and a status, then select them"""
506+ query = QSqlQuery (db )
507+ query .exec (
508+ f"UPDATE transcription SET id = '{ self .FILE_ROW_ID } ', status = '{ file_row_status } ' WHERE id = '1'"
509+ )
510+ query .exec (
511+ f"UPDATE transcription SET id = '{ self .URL_ROW_ID } ', status = '{ url_row_status } ' WHERE id = '2'"
512+ )
513+ widget .refresh_all ()
514+ widget .selectAll ()
515+ assert len (widget .selectionModel ().selectedRows ()) == 2
516+
517+ def test_restart_action_restarts_every_selected_row (self , widget , db , monkeypatch ):
518+ self ._select_both_rows (widget , db , "failed" , "canceled" )
519+ mock_service = Mock ()
520+ widget .transcription_service = mock_service
521+ mock_restart = Mock ()
522+ monkeypatch .setattr (widget , "_restart_transcription_task" , mock_restart )
523+
524+ widget .on_restart_transcription_action ()
525+
526+ reset_ids = [c .args [0 ] for c in mock_service .reset_transcription_for_restart .call_args_list ]
527+ assert sorted (reset_ids ) == [UUID (self .FILE_ROW_ID ), UUID (self .URL_ROW_ID )]
528+ restarted_ids = [c .args [0 ].id for c in mock_restart .call_args_list ]
529+ assert sorted (restarted_ids ) == [self .FILE_ROW_ID , self .URL_ROW_ID ]
530+
531+ def test_restart_action_skips_selected_rows_that_cannot_restart (self , widget , db , monkeypatch ):
532+ self ._select_both_rows (widget , db , "failed" , "completed" )
533+ mock_service = Mock ()
534+ widget .transcription_service = mock_service
535+ mock_restart = Mock ()
536+ monkeypatch .setattr (widget , "_restart_transcription_task" , mock_restart )
537+ mock_messagebox = Mock ()
538+ monkeypatch .setattr ("PyQt6.QtWidgets.QMessageBox" , mock_messagebox )
539+
540+ widget .on_restart_transcription_action ()
541+
542+ mock_service .reset_transcription_for_restart .assert_called_once_with (UUID (self .FILE_ROW_ID ))
543+ assert [c .args [0 ].id for c in mock_restart .call_args_list ] == [self .FILE_ROW_ID ]
544+ mock_messagebox .information .assert_not_called ()
545+
546+ def test_restart_action_shows_message_once_if_no_selected_row_can_restart (self , widget , db , monkeypatch ):
547+ self ._select_both_rows (widget , db , "completed" , "in_progress" )
548+ mock_service = Mock ()
549+ widget .transcription_service = mock_service
550+ mock_messagebox = Mock ()
551+ monkeypatch .setattr ("PyQt6.QtWidgets.QMessageBox" , mock_messagebox )
552+
553+ widget .on_restart_transcription_action ()
554+
555+ mock_service .reset_transcription_for_restart .assert_not_called ()
556+ mock_messagebox .information .assert_called_once ()
557+
558+ def test_context_menu_offers_restart_if_any_selected_row_can_restart (self , widget , db , monkeypatch ):
559+ self ._select_both_rows (widget , db , "failed" , "completed" )
560+ first_selected_row = widget .selectionModel ().selectedRows ()[0 ]
561+ assert widget .transcription (first_selected_row ).status == "completed"
562+ mock_menu = Mock (spec = QMenu )
563+ monkeypatch .setattr ("buzz.widgets.transcription_tasks_table_widget.QMenu" , Mock (return_value = mock_menu ))
564+
565+ widget .contextMenuEvent (Mock ())
566+
567+ labels = [c .args [0 ] for c in mock_menu .addAction .call_args_list ]
568+ assert "Restart Transcription" in labels
569+
499570 # --- _restart_transcription_task ---
500571
501572 def _make_transcription (self , ** kwargs ):
@@ -729,6 +800,62 @@ def capture_file_transcription_options(**kwargs):
729800 # Only the valid format should be present, invalid one skipped silently
730801 assert len (captured ["output_formats" ]) == 1
731802
803+ @pytest .mark .parametrize ("stored, expected" , [(1 , True ), (0 , False ), (None , False )])
804+ def test_restart_task_keeps_word_level_timings_and_extract_speech (self , widget , monkeypatch , stored , expected ):
805+ # SQLite returns the BOOLEAN columns as 1 or 0
806+ transcription = self ._make_transcription (word_level_timings = stored , extract_speech = stored )
807+
808+ mock_model = Mock ()
809+ mock_model .get_local_model_path .return_value = "/fake/path"
810+ monkeypatch .setattr ("buzz.model_loader.TranscriptionModel" , Mock (return_value = mock_model ))
811+ mock_task_cls = Mock (return_value = Mock ())
812+ monkeypatch .setattr ("buzz.transcriber.transcriber.FileTranscriptionTask" , mock_task_cls )
813+ mock_main_window = Mock ()
814+ mock_main_window .parent .return_value = None
815+ monkeypatch .setattr (widget , "parent" , Mock (return_value = mock_main_window ))
816+
817+ widget ._restart_transcription_task (transcription )
818+
819+ options = mock_task_cls .call_args .kwargs ["transcription_options" ]
820+ assert options .word_level_timings is expected
821+ assert options .extract_speech is expected
822+
823+ def test_restart_keeps_word_level_timings_and_extract_speech_from_database (self , widget , db , monkeypatch ):
824+ from buzz .db .dao .transcription_dao import TranscriptionDAO
825+ from buzz .model_loader import TranscriptionModel
826+ from buzz .transcriber .transcriber import (
827+ FileTranscriptionOptions ,
828+ FileTranscriptionTask ,
829+ TranscriptionOptions ,
830+ )
831+
832+ task = FileTranscriptionTask (
833+ transcription_options = TranscriptionOptions (word_level_timings = True , extract_speech = True ),
834+ file_transcription_options = FileTranscriptionOptions (),
835+ model_path = "" ,
836+ file_path = "/a/b/word-level.mp3" ,
837+ )
838+ dao = TranscriptionDAO (db )
839+ dao .create_transcription (task )
840+ dao .update_transcription_as_failed (task .uid , "Error" )
841+ widget .refresh_all ()
842+ row = next (
843+ row for row in range (widget .model ().rowCount ())
844+ if widget .model ().record (row ).value ("id" ) == str (task .uid )
845+ )
846+ widget .selectRow (row )
847+
848+ widget .transcription_service = Mock ()
849+ monkeypatch .setattr (TranscriptionModel , "get_local_model_path" , lambda self : "/fake/model" )
850+ mock_worker = Mock ()
851+ monkeypatch .setattr (widget , "parent" , Mock (return_value = Mock (transcriber_worker = mock_worker )))
852+
853+ widget .on_restart_transcription_action ()
854+
855+ options = mock_worker .add_task .call_args .args [0 ].transcription_options
856+ assert options .word_level_timings is True
857+ assert options .extract_speech is True
858+
732859 def test_column_resize_event (self , widget ):
733860 """Test column resize event handling"""
734861 # Mock the save_column_widths method
0 commit comments