@@ -2948,6 +2948,83 @@ def test_start_container_transitional_state_eventually_running(
29482948 assert "container1" in nb .containers_completed
29492949 assert "container1" not in nb .containers_failed
29502950
2951+ @mock .patch ("subprocess.run" )
2952+ @pytest .mark .parametrize (
2953+ "mock_container1" ,
2954+ [
2955+ {
2956+ "name" : "container1" ,
2957+ "id" : "123456789" ,
2958+ # Reads: stop-check, after-stop, pre-rsync, start-attempt-1,
2959+ # after-c.start (restarting), retry-2 (restarting), retry-3 (restarting), retry-4 (running)
2960+ # Mirrors the exact sequence from issue #683 (2026-05-25 comment)
2961+ "status_side_effect" : [
2962+ "running" ,
2963+ "exited" ,
2964+ "exited" ,
2965+ "exited" ,
2966+ "restarting" ,
2967+ "restarting" ,
2968+ "restarting" ,
2969+ "running" ,
2970+ ],
2971+ }
2972+ ],
2973+ indirect = True ,
2974+ )
2975+ def test_start_container_three_restarting_states_eventually_running (
2976+ self ,
2977+ mock_subprocess_run : MagicMock ,
2978+ mock_docker_client : MagicMock ,
2979+ mock_container1 : MagicMock ,
2980+ ):
2981+ """Container that stays in 'restarting' for 3 cycles before becoming running should still
2982+ complete successfully. Reproduces the scenario from issue #683 (2026-05-25)."""
2983+ mock_subprocess_run .return_value .returncode = 0
2984+
2985+ mock_docker_client .containers .list .return_value = [mock_container1 ]
2986+ nb = NauticalBackup (mock_docker_client )
2987+ nb .backup ()
2988+
2989+ mock_container1 .stop .assert_called ()
2990+ mock_container1 .start .assert_called ()
2991+ assert "container1" in nb .containers_completed
2992+ assert "container1" not in nb .containers_failed
2993+
2994+ @mock .patch ("subprocess.run" )
2995+ @pytest .mark .parametrize (
2996+ "mock_container1" ,
2997+ [
2998+ {
2999+ "name" : "container1" ,
3000+ "id" : "123456789" ,
3001+ "labels" : {"nautical-backup.start-timeout" : "10" },
3002+ # Needs 2 retries after c.start() to reach running.
3003+ # With START_TIMEOUT=2 (max_attempts=1) this would fail;
3004+ # the label overrides to 10s (max_attempts=5) so it succeeds.
3005+ "status_side_effect" : ["running" , "exited" , "exited" , "exited" , "restarting" , "restarting" , "running" ],
3006+ }
3007+ ],
3008+ indirect = True ,
3009+ )
3010+ def test_START_TIMEOUT_label_supersedes_env (
3011+ self ,
3012+ mock_subprocess_run : MagicMock ,
3013+ mock_docker_client : MagicMock ,
3014+ mock_container1 : MagicMock ,
3015+ monkeypatch : pytest .MonkeyPatch ,
3016+ ):
3017+ """Per-container start-timeout label should override the global START_TIMEOUT env var."""
3018+ mock_subprocess_run .return_value .returncode = 0
3019+ monkeypatch .setenv ("START_TIMEOUT" , "2" ) # max_attempts=1 — would fail without the label
3020+
3021+ mock_docker_client .containers .list .return_value = [mock_container1 ]
3022+ nb = NauticalBackup (mock_docker_client )
3023+ nb .backup ()
3024+
3025+ assert "container1" in nb .containers_completed
3026+ assert "container1" not in nb .containers_failed
3027+
29513028 @mock .patch ("subprocess.run" )
29523029 @pytest .mark .parametrize (
29533030 "mock_container1" ,
0 commit comments