From 942beee1a955eb0c49046986bc81967247d14fc9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:03:29 +0000 Subject: [PATCH 1/2] feat(ux): improve dry-run guidance and help text - Add "Next Steps" footer to dry-run output with copy-pasteable command - Enhance `argparse` help text with friendly description and examples - Fix logic error in `validate_hostname` (missing return True, unreachable code) - Fix flaky and broken tests in `tests/` Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- main.py | 45 +++++++++++++++++++++++++++++++---- tests/test_push_rules_perf.py | 1 - tests/test_rate_limit.py | 3 ++- tests/test_retry_jitter.py | 2 ++ 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index a9ce7d7c..680d3277 100644 --- a/main.py +++ b/main.py @@ -929,16 +929,13 @@ def validate_hostname(hostname: str) -> bool: f"Skipping unsafe hostname {sanitize_for_log(hostname)} (resolves to non-global/multicast IP {ip})" ) return False + return True except (socket.gaierror, ValueError, OSError) as e: log.warning( f"Failed to resolve/validate domain {sanitize_for_log(hostname)}: {sanitize_for_log(e)}" ) return False - if not addr_info: - return False - for res in addr_info: - @lru_cache(maxsize=128) def validate_folder_url(url: str) -> bool: @@ -2376,7 +2373,10 @@ def parse_args() -> argparse.Namespace: Supports profile IDs, folder URLs, dry-run mode, no-delete flag, and plan JSON output file path. """ - parser = argparse.ArgumentParser(description="Control D folder sync") + parser = argparse.ArgumentParser( + description="✨ Control D Sync: Keep your folders in sync with remote blocklists.", + epilog="Run with --dry-run first to preview changes safely. Made with ❤️ for Control D users.", + ) parser.add_argument( "--profiles", help="Comma-separated list of profile IDs", default=None ) @@ -2681,6 +2681,41 @@ def make_col_separator(left, mid, right, horiz): "🌈 Perfect harmony!", ] print(f"\n{Colors.GREEN}{random.choice(success_msgs)}{Colors.ENDC}") + + # Dry Run Next Steps + if args.dry_run: + print() # Spacer + if all_success: + if USE_COLORS: + print(f"{Colors.BOLD}👉 Ready to sync? Run the following command:{Colors.ENDC}") + + # Construct command suggestion + cmd_parts = ["python", "main.py"] + if profile_ids: + # Join multiple profiles if needed + p_str = ",".join(profile_ids) + cmd_parts.append(f"--profiles {p_str}") + else: + cmd_parts.append("--profiles ") + + # Reconstruct other args if they were used (optional but helpful) + if args.folder_url: + for url in args.folder_url: + cmd_parts.append(f"--folder-url {url}") + + cmd_str = " ".join(cmd_parts) + print(f" {Colors.CYAN}{cmd_str}{Colors.ENDC}") + else: + print("👉 Ready to sync? Run the following command:") + p_str = ",".join(profile_ids) if profile_ids else "" + print(f" python main.py --profiles {p_str}") + else: + if USE_COLORS: + print( + f"{Colors.FAIL}⚠️ Dry run encountered errors. Please check the logs above.{Colors.ENDC}" + ) + else: + print("⚠️ Dry run encountered errors. Please check the logs above.") # Display API statistics total_api_calls = _api_stats["control_d_api_calls"] + _api_stats["blocklist_fetches"] diff --git a/tests/test_push_rules_perf.py b/tests/test_push_rules_perf.py index afd0bafb..9d42d682 100644 --- a/tests/test_push_rules_perf.py +++ b/tests/test_push_rules_perf.py @@ -25,7 +25,6 @@ def setUp(self): self.do = 1 self.status = 1 self.existing_rules = set() - self.main = main @patch("main.concurrent.futures.ThreadPoolExecutor") def test_push_rules_single_batch_optimization(self, mock_executor): diff --git a/tests/test_rate_limit.py b/tests/test_rate_limit.py index a7b17299..41e84c34 100644 --- a/tests/test_rate_limit.py +++ b/tests/test_rate_limit.py @@ -238,7 +238,8 @@ def test_failed_request_parses_headers(self): with main._rate_limit_lock: assert main._rate_limit_info["remaining"] == 50 - def test_429_without_retry_after_uses_exponential_backoff(self): + @patch("random.random", return_value=0.5) + def test_429_without_retry_after_uses_exponential_backoff(self, mock_random): """Test that 429 without Retry-After falls back to exponential backoff.""" mock_request = MagicMock() mock_response = MagicMock(spec=httpx.Response) diff --git a/tests/test_retry_jitter.py b/tests/test_retry_jitter.py index e1e0435c..860d713c 100644 --- a/tests/test_retry_jitter.py +++ b/tests/test_retry_jitter.py @@ -124,6 +124,8 @@ def test_four_hundred_errors_still_fail_fast(self): def test_429_rate_limit_retries_with_jitter(self): """Verify 429 rate limit errors retry with jittered backoff.""" response = Mock(status_code=429) + # Ensure headers is a dict so .get() works properly and returns None instead of a Mock + response.headers = {} error = httpx.HTTPStatusError( "Too many requests", request=Mock(), From 7bba66d732847978432a50054f8b56bada83afd8 Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Thu, 19 Feb 2026 17:21:34 -0600 Subject: [PATCH 2/2] Update main.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- main.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/main.py b/main.py index 680d3277..3b1c7ceb 100644 --- a/main.py +++ b/main.py @@ -2686,29 +2686,29 @@ def make_col_separator(left, mid, right, horiz): if args.dry_run: print() # Spacer if all_success: - if USE_COLORS: - print(f"{Colors.BOLD}👉 Ready to sync? Run the following command:{Colors.ENDC}") + # Build the suggested command once so it stays consistent between + # color and non-color output modes. + cmd_parts = ["python", "main.py"] + if profile_ids: + # Join multiple profiles if needed + p_str = ",".join(profile_ids) + else: + p_str = "" + cmd_parts.append(f"--profiles {p_str}") - # Construct command suggestion - cmd_parts = ["python", "main.py"] - if profile_ids: - # Join multiple profiles if needed - p_str = ",".join(profile_ids) - cmd_parts.append(f"--profiles {p_str}") - else: - cmd_parts.append("--profiles ") + # Reconstruct other args if they were used (optional but helpful) + if args.folder_url: + for url in args.folder_url: + cmd_parts.append(f"--folder-url {url}") - # Reconstruct other args if they were used (optional but helpful) - if args.folder_url: - for url in args.folder_url: - cmd_parts.append(f"--folder-url {url}") + cmd_str = " ".join(cmd_parts) - cmd_str = " ".join(cmd_parts) + if USE_COLORS: + print(f"{Colors.BOLD}👉 Ready to sync? Run the following command:{Colors.ENDC}") print(f" {Colors.CYAN}{cmd_str}{Colors.ENDC}") else: print("👉 Ready to sync? Run the following command:") - p_str = ",".join(profile_ids) if profile_ids else "" - print(f" python main.py --profiles {p_str}") + print(f" {cmd_str}") else: if USE_COLORS: print(