fix(plugin): fall back to generic listing when category route 404s

Fixes #40683

`PluginInstaller.list_plugins_by_category` calls
`GET /plugin/{tenant_id}/management/{category}/list` on the plugin
daemon. Local plugin daemons (e.g. 0.6.x) don't expose the
category-segmented route and return 404. The current code raises
this 404, which then breaks the model-provider integration page
even though the generic listing route is available.

On 404, fall back to
`GET /plugin/{tenant_id}/management/list` and filter the result
locally by `declaration.category`. The generic route is the same
one used by the unrestricted `list_plugins_with_total`. Apply the
caller's page/page_size on the filtered result (cap page_size at
1000 for the fallback fetch — adequate for any reasonable category).

Added a regression test in `test_plugin_manager.py` that
asserts the 404-fallback path produces the correct filtered list
with the right `has_more` flag.
This commit is contained in:
Taranum01 2026-08-14 02:53:38 +05:30 committed by Taranum Wasu
parent dfac3e524e
commit dcdca603e7
2 changed files with 68 additions and 13 deletions

View File

@ -98,19 +98,40 @@ class PluginInstaller(BasePluginClient):
tags: Sequence[str] = (),
language: str = "en_US",
) -> PluginListWithoutTotalResponse:
return self._request_with_plugin_daemon_response(
"GET",
f"plugin/{tenant_id}/management/{category.value}/list",
PluginListWithoutTotalResponse,
params={
"page": page,
"page_size": page_size,
"response_type": "paged",
"query": query,
"tags": list(tags),
"language": language,
},
)
try:
return self._request_with_plugin_daemon_response(
"GET",
f"plugin/{tenant_id}/management/{category.value}/list",
PluginListWithoutTotalResponse,
params={
"page": page,
"page_size": page_size,
"response_type": "paged",
"query": query,
"tags": list(tags),
"language": language,
},
)
except HTTPError as e:
# Local plugin daemons (e.g. 0.6.x) don't expose the
# category-segmented route. Fall back to the generic listing and
# filter by declaration.category on the API side.
message = e.args[0] if e.args else ""
if "404" not in message:
raise
all_plugins = self.list_plugins_with_total(
tenant_id,
page=1,
page_size=1000,
)
filtered = [p for p in all_plugins.list if p.declaration.category == category]
start = (page - 1) * page_size
end = start + page_size
return PluginListWithoutTotalResponse(
list=filtered[start:end],
has_more=end < len(filtered),
)
def upload_pkg(
self,

View File

@ -154,6 +154,40 @@ class TestPluginDiscovery:
assert result.list == [mock_plugin_entity]
assert result.has_more is True
def test_list_plugins_by_category_falls_back_on_404(self, plugin_installer, mock_plugin_entity):
"""When the category-segmented route 404s (local plugin daemon < 0.7),
list_plugins_by_category should fall back to the generic listing and
filter by declaration.category on the API side. Regression for #40683.
"""
from requests import HTTPError
# 404 from the category route, then a generic listing with three plugins
# — two Tool, one Model — across two pages.
p_tool_1 = mock_plugin_entity
p_tool_2 = mock_plugin_entity
p_model = mock_plugin_entity
p_model.declaration.category = PluginCategory.Model
with patch.object(
plugin_installer,
"_request_with_plugin_daemon_response",
side_effect=[
HTTPError("404"),
PluginListResponse(
list=[p_tool_1, p_model, p_tool_2],
total=3,
),
],
):
result = plugin_installer.list_plugins_by_category(
"test-tenant", category=PluginCategory.Tool, page=1, page_size=10
)
# Filters out the Model entry, returns both Tool entries.
assert result.list == [p_tool_1, p_tool_2]
assert result.has_more is False
assert result.has_more == (10 < 2) # page_size 10 < 2 items
def test_list_plugins_empty_result(self, plugin_installer):
"""Test plugin listing when no plugins are installed."""
# Arrange: Mock empty response