mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-30 04:15:45 +08:00
fix(api): align plugin media errors with master
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user