From d818703dade3df66f071d9297a2d020387142518 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:56:29 +0000 Subject: [PATCH 1/6] feat: improve feedback for long waits in CI/non-interactive mode - Update `countdown_timer` in `main.py` to log periodic updates every 10s in non-color mode. - Update `tests/test_ux.py` to verify the new behavior. - Fix test pollution in `tests/test_push_rules_perf.py` by using dynamic imports. - Create `.Jules/palette.md` to document the learning. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- .Jules/palette.md | 3 +++ main.py | 13 ++++++++++ tests/test_push_rules_perf.py | 15 +++++++++--- tests/test_ux.py | 46 ++++++++++++++++++++++++++++++----- 4 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 .Jules/palette.md 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 e1e3ced..239e2e6 100644 --- a/main.py +++ b/main.py @@ -288,6 +288,19 @@ 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.""" 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"{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 63a89c8..86cd275 100644 --- a/tests/test_push_rules_perf.py +++ b/tests/test_push_rules_perf.py @@ -7,10 +7,17 @@ # Add root to path to import main sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -import main +import importlib class TestPushRulesPerf(unittest.TestCase): def setUp(self): + # Dynamically import main to handle reloads by other tests + if 'main' in sys.modules: + self.main = sys.modules['main'] + else: + import main + self.main = main + self.client = MagicMock() self.profile_id = "test_profile" self.folder_name = "test_folder" @@ -50,7 +57,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, @@ -84,7 +91,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, @@ -108,7 +115,7 @@ def test_push_rules_skips_validation_for_existing(self, mock_is_valid): # h1 is already known, h2 is new existing_rules = {"h1"} - 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..71c4bae 100644 --- a/tests/test_ux.py +++ b/tests/test_ux.py @@ -30,17 +30,51 @@ 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) + + main.countdown_timer(10, "Test") - # Should not write to stderr - mock_stderr.write.assert_not_called() + # 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) + + +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...") From 251c359579d751b7b90c3be82fd5859bbda5e430 Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Sun, 15 Feb 2026 20:13:31 -0600 Subject: [PATCH 2/6] Update main.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 239e2e6..ced8a10 100644 --- a/main.py +++ b/main.py @@ -286,7 +286,9 @@ 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. From 13730621d7c28f4d9e0e1b32671b6a422d2319b0 Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Sun, 15 Feb 2026 20:13:40 -0600 Subject: [PATCH 3/6] Update tests/test_ux.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_ux.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_ux.py b/tests/test_ux.py index 71c4bae..ad593ee 100644 --- a/tests/test_ux.py +++ b/tests/test_ux.py @@ -48,6 +48,9 @@ def test_countdown_timer_no_colors_short(monkeypatch): mock_log.info.assert_not_called() # Should call sleep exactly once with full seconds 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): From 304d5829d42d5dbe496ed6beabf4b607a38a41f6 Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Sun, 15 Feb 2026 20:14:03 -0600 Subject: [PATCH 4/6] Update tests/test_push_rules_perf.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_push_rules_perf.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_push_rules_perf.py b/tests/test_push_rules_perf.py index 86cd275..9d05fd7 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 importlib - class TestPushRulesPerf(unittest.TestCase): def setUp(self): # Dynamically import main to handle reloads by other tests From 41c43e149449cdd0b313f25dda6d4875ecc21d4c Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Sun, 15 Feb 2026 20:14:15 -0600 Subject: [PATCH 5/6] Update main.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index ced8a10..7580492 100644 --- a/main.py +++ b/main.py @@ -297,7 +297,7 @@ def countdown_timer(seconds: int, message: str = "Waiting") -> None: 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"{message}: {remaining}s remaining...") + log.info(f"{sanitize_for_log(message)}: {remaining}s remaining...") sleep_time = min(step, remaining) time.sleep(sleep_time) From 78f07d37dab8876acb9182b294bed30e67584460 Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Sun, 15 Feb 2026 20:14:24 -0600 Subject: [PATCH 6/6] Update tests/test_push_rules_perf.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/test_push_rules_perf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_push_rules_perf.py b/tests/test_push_rules_perf.py index 9d05fd7..4b79ebf 100644 --- a/tests/test_push_rules_perf.py +++ b/tests/test_push_rules_perf.py @@ -11,7 +11,7 @@ class TestPushRulesPerf(unittest.TestCase): def setUp(self): # Dynamically import main to handle reloads by other tests if 'main' in sys.modules: - self.main = sys.modules['main'] + self.main = importlib.reload(sys.modules['main']) else: import main self.main = main