fix(api): align plugin media errors with master

This commit is contained in:
huanghuoguoguo
2026-10-29 12:50:15 +08:00
parent 7295639423
commit 211f4e93f4
3 changed files with 51 additions and 6 deletions
+3 -2
View File
@@ -101,8 +101,9 @@ consumer's context from escaping into another consumer's task.
## Scope and adoption
This is the full **existing Workspace and execution business capability**
foundation. Instance administration, account login/bootstrap, public ingress,
This foundation covers the Workspace and execution business capabilities listed
in its explicit catalogs. Plugin publishing to Space and Git synchronization
remain in their existing HTTP routes. Instance administration, account login/bootstrap, public ingress,
OAuth callbacks, transport assets and internal Runtime handshake/chunk/ping
messages remain with their owning boundaries. Composite HTTP response layouts
(such as a combined monitoring page) are consumer projections of these
@@ -14,6 +14,7 @@ from urllib.parse import urlparse
import httpx
import sqlalchemy
from langbot_plugin.entities.io.errors import ActionCallError
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
from pydantic import Field
@@ -232,13 +233,15 @@ async def icon(ap, context, payload):
await _fence(ap, context)
try:
result = await ap.plugin_connector.get_plugin_icon(payload.author, payload.plugin_name)
except ValueError as exc:
if 'is not installed in this workspace' in str(exc).lower():
except (ValueError, ActionCallError) as exc:
if isinstance(exc, ActionCallError) or 'is not installed in this workspace' in str(exc).lower():
raise WorkspaceNotFoundError('Icon not found') from exc
raise
if not result or not result.get('plugin_icon_base64'):
raise WorkspaceNotFoundError('Icon not found')
return {
'content': await asyncio.to_thread(base64.b64decode, result['plugin_icon_base64']),
'mime_type': result['mime_type'],
'mime_type': result.get('mime_type', 'image/svg+xml'),
}
@@ -247,7 +250,10 @@ async def asset(ap, context, payload):
path = normalize_plugin_asset_path(payload.filepath)
if path is None:
raise WorkspaceNotFoundError('Asset not found')
result = await ap.plugin_connector.get_plugin_assets(payload.author, payload.plugin_name, path)
try:
result = await ap.plugin_connector.get_plugin_assets(payload.author, payload.plugin_name, path)
except (ValueError, ActionCallError) as exc:
raise WorkspaceNotFoundError('Asset not found') from exc
if not result.get('asset_base64'):
raise WorkspaceNotFoundError('Asset not found')
return {
@@ -7,9 +7,11 @@ from unittest.mock import AsyncMock, Mock
from zipfile import ZipFile
import pytest
from langbot_plugin.entities.io.errors import ActionCallError
from pydantic import TypeAdapter, ValidationError
from langbot.pkg.api.application import plugin_operations as ops
from langbot.pkg.api.application.errors import error_from_exception
from langbot.pkg.api.context import ExecutionContext
from langbot.pkg.workspace.errors import WorkspaceGenerationMismatchError, WorkspaceNotFoundError
@@ -116,6 +118,42 @@ async def test_asset_path_rejects_escape_before_runtime_read_and_returns_exact_b
assert ap.plugin_connector.get_plugin_assets.await_args.args == ('a', 'b', 'assets/image.png')
@pytest.mark.asyncio
@pytest.mark.parametrize('operation', ['icon', 'asset'])
@pytest.mark.parametrize(
'error', [ActionCallError('Plugin asset unavailable'), ValueError('Plugin a/b is not installed in this Workspace')]
)
async def test_missing_plugin_media_maps_to_not_found(operation, error):
ap = app()
ap.plugin_connector.get_plugin_icon = AsyncMock(side_effect=error)
ap.plugin_connector.get_plugin_assets = AsyncMock(side_effect=error)
payload = ops.AssetInput(author='a', plugin_name='b', filepath='image.png')
with pytest.raises(WorkspaceNotFoundError) as failure:
await getattr(ops, operation)(ap, CONTEXT, payload)
result = error_from_exception(failure.value)
assert (result.code, result.status_code) == ('resource_not_found', 404)
@pytest.mark.asyncio
@pytest.mark.parametrize('result', [None, {}, {'plugin_icon_base64': ''}])
async def test_empty_plugin_icon_is_not_found(result):
ap = app()
ap.plugin_connector.get_plugin_icon = AsyncMock(return_value=result)
with pytest.raises(WorkspaceNotFoundError):
await ops.icon(ap, CONTEXT, ops.PluginInput(author='a', plugin_name='b'))
@pytest.mark.asyncio
async def test_plugin_icon_defaults_to_svg_and_preserves_other_validation_errors():
ap = app()
ap.plugin_connector.get_plugin_icon = AsyncMock(return_value={'plugin_icon_base64': 'PHN2Zy8+'})
payload = ops.PluginInput(author='a', plugin_name='b')
assert await ops.icon(ap, CONTEXT, payload) == {'content': b'<svg/>', 'mime_type': 'image/svg+xml'}
ap.plugin_connector.get_plugin_icon.side_effect = ValueError('Invalid plugin identity')
with pytest.raises(ValueError, match='Invalid plugin identity'):
await ops.icon(ap, CONTEXT, payload)
@pytest.mark.asyncio
async def test_config_files_are_workspace_scoped_and_size_checked():
ap = app()