Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion scripts/build_skills_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,12 @@ def main():
"official": OptionalSkillSource(),
"well-known": WellKnownSkillSource(),
"github": GitHubSource(auth=auth),
"clawhub": ClawHubSource(),
# The interactive 12s walk budget truncates the full ~50k catalog at
# ~16 pages (~3.2k skills), which trips the EXPECTED_FLOORS health
# check below and blocks the deploy. The offline builder walks to
# exhaustion (~250 sequential pages, ~3-4 min), so give it a budget
# sized for that.
"clawhub": ClawHubSource(catalog_walk_budget_seconds=600),
"claude-marketplace": ClaudeMarketplaceSource(auth=auth),
"lobehub": LobeHubSource(),
"browse-sh": BrowseShSource(),
Expand Down
6 changes: 5 additions & 1 deletion tests/scripts/test_build_skills_index_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ def _install_fake_sources(monkeypatch, *, github_count, claude_count=40,
build_mod, "GitHubSource",
lambda auth: _FakeSource("github", github_count, rate_limited=github_rate_limited),
)
monkeypatch.setattr(build_mod, "ClawHubSource", lambda: _FakeSource("clawhub", 69000))
monkeypatch.setattr(
build_mod,
"ClawHubSource",
lambda catalog_walk_budget_seconds=None: _FakeSource("clawhub", 69000),
)
monkeypatch.setattr(
build_mod, "ClaudeMarketplaceSource",
lambda auth: _FakeSource("claude-marketplace", claude_count, rate_limited=github_rate_limited),
Expand Down
42 changes: 42 additions & 0 deletions tests/tools/test_skills_hub_clawhub.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,48 @@ def side_effect(url, *args, **kwargs):
self.assertEqual(results[0].identifier, "only-skill")
mock_write_cache.assert_called_once()

@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
def test_catalog_walk_budget_instance_override_wins_over_class_default(
self, mock_get, _mock_read_cache, mock_write_cache
):
"""The offline index builder passes an explicit walk budget so the
interactive 12s default cannot truncate the full-catalog walk (which
shipped a degenerate ~3.2k-skill index and failed the EXPECTED_FLOORS
deploy check). With the class default forced to an already-expired
deadline, an instance constructed with its own budget must still walk
to natural termination and cache the result."""
pages = {"n": 0}

def side_effect(url, *args, **kwargs):
if url.endswith("/skills"):
idx = pages["n"]
pages["n"] += 1
last = idx == 2
return _MockResponse(
status_code=200,
json_data={
"items": [
{"slug": f"skill-{idx}", "displayName": f"Skill {idx}"}
],
**({} if last else {"nextCursor": f"cursor-{idx + 1}"}),
},
)
return _MockResponse(status_code=404, json_data={})

mock_get.side_effect = side_effect

with patch.object(ClawHubSource, "CATALOG_WALK_BUDGET_SECONDS", -1):
src = ClawHubSource(catalog_walk_budget_seconds=60)
results = src._load_catalog_index()

# Walked all three pages to natural termination despite the expired
# class-level deadline, and cached the complete catalog.
self.assertEqual(pages["n"], 3)
self.assertEqual(len(results), 3)
mock_write_cache.assert_called_once()


class TestClawHubCatalogWalkBounded(unittest.TestCase):
"""max_items bounds the walk so browse's cold-start fallback renders one
Expand Down
9 changes: 9 additions & 0 deletions tools/skills_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -1952,6 +1952,15 @@ class ClawHubSource(SkillSource):
# minutes. Bound it so a slow/large catalog cannot hang the caller.
CATALOG_WALK_BUDGET_SECONDS = 12

def __init__(self, catalog_walk_budget_seconds: Optional[float] = None):
# Interactive callers (browse/search cold start) keep the tight class
# default so a slow catalog cannot hang them. Walk-to-exhaustion
# callers — the offline index builder — need ~250 sequential pages for
# the full 50k+ catalog, which no interactive budget can cover, so
# they pass an explicit larger budget here.
if catalog_walk_budget_seconds is not None:
self.CATALOG_WALK_BUDGET_SECONDS = catalog_walk_budget_seconds

def source_id(self) -> str:
return "clawhub"

Expand Down