diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..5d11198 --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2025-01-26 - [Silent Waits in CI] +**Learning:** Long silent waits in CLI tools (especially in CI/non-interactive mode) cause user anxiety about hung processes. +**Action:** Always provide periodic heartbeat logs (e.g. every 10s) for long operations in non-interactive environments. diff --git a/main.py b/main.py index 4b786ac..65080e7 100644 --- a/main.py +++ b/main.py @@ -307,8 +307,23 @@ def _get_progress_bar_width() -> int: def countdown_timer(seconds: int, message: str = "Waiting") -> None: - """Shows a countdown timer if strictly in a TTY, otherwise just sleeps.""" + """Show a countdown in interactive/color mode; in no-color/non-interactive + mode, sleep silently for short waits and log periodic heartbeat messages + for longer waits.""" if not USE_COLORS: + # UX Improvement: For long waits in non-interactive/no-color mode (e.g. CI), + # log periodic updates instead of sleeping silently. + if seconds > 10: + step = 10 + for remaining in range(seconds, 0, -step): + # Don't log the first one if we already logged "Waiting..." before calling this + if remaining < seconds: + log.info(f"{sanitize_for_log(message)}: {remaining}s remaining...") + + sleep_time = min(step, remaining) + time.sleep(sleep_time) + return + time.sleep(seconds) return diff --git a/tests/test_push_rules_perf.py b/tests/test_push_rules_perf.py index 3ae10fb..be8b909 100644 --- a/tests/test_push_rules_perf.py +++ b/tests/test_push_rules_perf.py @@ -7,8 +7,6 @@ # Add root to path to import main sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -import main - class TestPushRulesPerf(unittest.TestCase): def setUp(self): # Ensure we are using the current main module instance (in case of reloads) @@ -55,7 +53,7 @@ def test_push_rules_single_batch_optimization(self, mock_executor, mock_as_compl # For this test, we mock _api_post_form? # No, _api_post_form calls client.post. - main.push_rules( + self.main.push_rules( self.profile_id, self.folder_name, self.folder_id, @@ -89,7 +87,7 @@ def test_push_rules_multi_batch(self, mock_executor, mock_as_completed): mock_as_completed.return_value = [mock_future, mock_future] # 2 batches - main.push_rules( + self.main.push_rules( self.profile_id, self.folder_name, self.folder_id, diff --git a/tests/test_ux.py b/tests/test_ux.py index 311b7e9..ad593ee 100644 --- a/tests/test_ux.py +++ b/tests/test_ux.py @@ -30,17 +30,54 @@ def test_countdown_timer_visuals(monkeypatch): # Check for ANSI clear line code assert "\033[K" in combined_output -def test_countdown_timer_no_colors(monkeypatch): - """Verify that countdown_timer sleeps without writing to stderr if NO_COLOR.""" +def test_countdown_timer_no_colors_short(monkeypatch): + """Verify that short countdowns sleep silently without writing to stderr if NO_COLOR.""" monkeypatch.setattr(main, "USE_COLORS", False) mock_stderr = MagicMock() monkeypatch.setattr(sys, "stderr", mock_stderr) mock_sleep = MagicMock() monkeypatch.setattr(main.time, "sleep", mock_sleep) - main.countdown_timer(3, "Test") + # Mock log to ensure it's not called + mock_log = MagicMock() + monkeypatch.setattr(main, "log", mock_log) - # Should not write to stderr - mock_stderr.write.assert_not_called() + main.countdown_timer(10, "Test") + + # Should not log + mock_log.info.assert_not_called() # Should call sleep exactly once with full seconds - mock_sleep.assert_called_once_with(3) + mock_sleep.assert_called_once_with(10) + # Should not write anything to stderr for short, no-color countdowns + mock_stderr.write.assert_not_called() + mock_stderr.flush.assert_not_called() + + +def test_countdown_timer_no_colors_long(monkeypatch): + """Verify that long countdowns log periodic updates if NO_COLOR.""" + monkeypatch.setattr(main, "USE_COLORS", False) + mock_sleep = MagicMock() + monkeypatch.setattr(main.time, "sleep", mock_sleep) + + mock_log = MagicMock() + monkeypatch.setattr(main, "log", mock_log) + + # Test with 25 seconds + main.countdown_timer(25, "LongWait") + + # Expected sleep calls: + # 1. min(10, 25) -> 10 (remaining 25) + # 2. min(10, 15) -> 10 (remaining 15) + # 3. min(10, 5) -> 5 (remaining 5) + + # Expected log calls: + # 1. "LongWait: 15s remaining..." (after first sleep/loop iteration) + # 2. "LongWait: 5s remaining..." (after second sleep/loop iteration) + + assert mock_sleep.call_count == 3 + mock_sleep.assert_any_call(10) + mock_sleep.assert_any_call(5) + + assert mock_log.info.call_count == 2 + mock_log.info.assert_any_call("LongWait: 15s remaining...") + mock_log.info.assert_any_call("LongWait: 5s remaining...")