diff --git a/tests/tools/test_skills_hub_clawhub.py b/tests/tools/test_skills_hub_clawhub.py index 6ac434dd207b..8018a589cbb9 100644 --- a/tests/tools/test_skills_hub_clawhub.py +++ b/tests/tools/test_skills_hub_clawhub.py @@ -1,17 +1,22 @@ #!/usr/bin/env python3 +import io import unittest +import zipfile from unittest.mock import patch +import httpx + from tools.skills_hub import ClawHubSource, SkillMeta class _MockResponse: - def __init__(self, status_code=200, json_data=None, text="", headers=None): + def __init__(self, status_code=200, json_data=None, text="", headers=None, content=b""): self.status_code = status_code self._json_data = json_data self.text = text self.headers = headers or {} + self.content = content def json(self): return self._json_data @@ -264,6 +269,24 @@ def side_effect(url, *args, **kwargs): self.assertIsNotNone(bundle) self.assertEqual(bundle.files["SKILL.md"], "# Skill") + @patch("tools.skills_hub.time.sleep") + @patch("tools.skills_hub.httpx.get") + def test_download_zip_retries_http_error_before_success(self, mock_get, mock_sleep): + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w") as zf: + zf.writestr("SKILL.md", "# Skill") + + mock_get.side_effect = [ + httpx.ConnectTimeout("temporary timeout"), + _MockResponse(status_code=200, content=zip_buffer.getvalue()), + ] + + files = self.src._download_zip("caldav-calendar", "1.0.1") + + self.assertEqual(files, {"SKILL.md": "# Skill"}) + self.assertEqual(mock_get.call_count, 2) + mock_sleep.assert_called_once_with(1) + @patch("tools.skills_hub.check_website_access", return_value=None) @patch("tools.skills_hub.is_safe_url") @patch("tools.skills_hub.httpx.get") diff --git a/tools/skills_hub.py b/tools/skills_hub.py index d84081731834..3023d2fc2873 100644 --- a/tools/skills_hub.py +++ b/tools/skills_hub.py @@ -2587,6 +2587,17 @@ def _download_zip(self, slug: str, version: str) -> Dict[str, str]: logger.warning("ClawHub returned invalid ZIP for %s v%s", slug, version) return files except httpx.HTTPError as exc: + if attempt < max_retries - 1: + logger.debug( + "ClawHub ZIP download failed for %s v%s, retrying (attempt %d/%d): %s", + slug, + version, + attempt + 1, + max_retries, + exc, + ) + time.sleep(1) + continue logger.debug("ClawHub ZIP download failed for %s v%s: %s", slug, version, exc) return files