fix(plugin): honor requested marketplace version (#2555)

This commit is contained in:
RockChinQ
2026-09-20 23:59:50 +08:00
committed by GitHub
parent 942a302808
commit 52f5699533
2 changed files with 78 additions and 10 deletions
+35 -10
View File
@@ -139,6 +139,30 @@ def _decode_json_object(body: bytes, *, subject: str) -> dict[str, Any]:
return payload return payload
def _select_marketplace_plugin_version(
versions: Any,
*,
requested_version: str | None,
plugin_author: str,
plugin_name: str,
) -> str:
if not isinstance(versions, list) or not versions:
raise ValueError(f'Plugin {plugin_author}/{plugin_name} has no versions')
if requested_version is None:
candidate = versions[0]
if not isinstance(candidate, dict) or not candidate.get('version'):
raise ValueError(f'Plugin {plugin_author}/{plugin_name} has no versions')
return str(candidate['version'])
for candidate in versions:
if isinstance(candidate, dict) and str(candidate.get('version') or '') == requested_version:
return requested_version
raise ValueError(
f'Plugin {plugin_author}/{plugin_name} version {requested_version} is not available in marketplace'
)
class PluginRuntimeNotConnectedError(RuntimeError): class PluginRuntimeNotConnectedError(RuntimeError):
"""Raised when plugin runtime operations are requested before connection.""" """Raised when plugin runtime operations are requested before connection."""
@@ -1611,6 +1635,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
execution_context: ExecutionContext, execution_context: ExecutionContext,
plugin_author: str, plugin_author: str,
plugin_name: str, plugin_name: str,
plugin_version: str | None,
task_context: taskmgr.TaskContext | None, task_context: taskmgr.TaskContext | None,
) -> tuple[bytes | None, str | None]: ) -> tuple[bytes | None, str | None]:
"""Return a plugin package, or install an MCP/skill and return none.""" """Return a plugin package, or install an MCP/skill and return none."""
@@ -1675,20 +1700,19 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
subject='Marketplace plugin versions', subject='Marketplace plugin versions',
) )
versions = versions_payload.get('data', {}).get('versions', []) versions = versions_payload.get('data', {}).get('versions', [])
if ( requested_version = str(plugin_version or '').strip()
not isinstance(versions, list) version = _select_marketplace_plugin_version(
or not versions versions,
or not isinstance(versions[0], dict) requested_version=requested_version or None,
or not versions[0].get('version') plugin_author=plugin_author,
): plugin_name=plugin_name,
raise ValueError(f'Plugin {plugin_author}/{plugin_name} has no versions') )
latest_version = str(versions[0]['version'])
_download_status, plugin_package = await _marketplace_get( _download_status, plugin_package = await _marketplace_get(
client, client,
f'{space_url}/api/v1/marketplace/plugins/download/{plugin_author}/{plugin_name}/{latest_version}', f'{space_url}/api/v1/marketplace/plugins/download/{plugin_author}/{plugin_name}/{version}',
max_bytes=_MARKETPLACE_PLUGIN_DOWNLOAD_MAX_BYTES, max_bytes=_MARKETPLACE_PLUGIN_DOWNLOAD_MAX_BYTES,
) )
return plugin_package, latest_version return plugin_package, version
def _admit_plugin_archive( def _admit_plugin_archive(
self, self,
@@ -1746,6 +1770,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
execution_context, execution_context,
plugin_author, plugin_author,
plugin_name, plugin_name,
str(install_info.get('plugin_version') or '') or None,
task_context, task_context,
) )
if file_bytes is None: if file_bytes is None:
@@ -0,0 +1,43 @@
import pytest
from langbot.pkg.plugin.connector import _select_marketplace_plugin_version
@pytest.mark.parametrize(
('requested_version', 'expected'),
[
(None, '0.1.4'),
('0.1.3', '0.1.3'),
],
)
def test_select_marketplace_plugin_version(requested_version, expected):
assert (
_select_marketplace_plugin_version(
[{'version': '0.1.4'}, {'version': '0.1.3'}],
requested_version=requested_version,
plugin_author='langbot-team',
plugin_name='RunnerDemo',
)
== expected
)
def test_select_marketplace_plugin_version_rejects_requested_missing_version():
with pytest.raises(ValueError, match='version 0.1.2 is not available'):
_select_marketplace_plugin_version(
[{'version': '0.1.4'}],
requested_version='0.1.2',
plugin_author='langbot-team',
plugin_name='RunnerDemo',
)
@pytest.mark.parametrize('versions', [[], [{'unexpected': 'value'}], 'not-a-list'])
def test_select_marketplace_plugin_version_rejects_invalid_latest(versions):
with pytest.raises(ValueError, match='has no versions'):
_select_marketplace_plugin_version(
versions,
requested_version=None,
plugin_author='langbot-team',
plugin_name='RunnerDemo',
)