mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-22 12:26:08 +00:00
fix(ci): resolve langbot-plugin from PyPI and clear lint failures
CI on feat/sandbox failed across Unit Tests, Lint and Build Dev Image.
Root causes and fixes:
- pyproject.toml had a [tool.uv.sources] editable override pinning
langbot-plugin to ../langbot-plugin-sdk. That path only exists in a
paired local checkout, so `uv sync` failed on every CI runner
("Distribution not found"). Remove the override and regenerate uv.lock
so langbot-plugin==0.4.0b1 resolves from PyPI, matching master.
- tests/integration/api/test_pipelines.py: the pipeline extensions
endpoint now calls ap.skill_service.list_skills(); add the missing
skill_service mock to the fake_pipeline_app fixture (the test came
from master, the endpoint change from feat/sandbox).
- Apply ruff format to three src files and prettier to three web files
that had committed formatting drift, failing `ruff format --check`
and `pnpm lint`.
This commit is contained in:
@@ -130,9 +130,6 @@ dev = [
|
|||||||
"ruff>=0.11.9",
|
"ruff>=0.11.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
langbot-plugin = { path = "../langbot-plugin-sdk", editable = true }
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
# Exclude a variety of commonly ignored directories.
|
# Exclude a variety of commonly ignored directories.
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class KnowledgeBaseRouterGroup(group.RouterGroup):
|
|||||||
@self.route(
|
@self.route(
|
||||||
'/<knowledge_base_uuid>',
|
'/<knowledge_base_uuid>',
|
||||||
methods=['GET', 'DELETE', 'PUT'],
|
methods=['GET', 'DELETE', 'PUT'],
|
||||||
auth_type=group.AuthType. USER_TOKEN_OR_API_KEY,
|
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||||
)
|
)
|
||||||
async def handle_specific_knowledge_base(knowledge_base_uuid: str) -> quart.Response:
|
async def handle_specific_knowledge_base(knowledge_base_uuid: str) -> quart.Response:
|
||||||
if quart.request.method == 'GET':
|
if quart.request.method == 'GET':
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class MCPRouterGroup(group.RouterGroup):
|
|||||||
async def _(server_name: str) -> str:
|
async def _(server_name: str) -> str:
|
||||||
"""获取、更新或删除MCP服务器配置"""
|
"""获取、更新或删除MCP服务器配置"""
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
server_name = unquote(server_name)
|
server_name = unquote(server_name)
|
||||||
|
|
||||||
server_data = await self.ap.mcp_service.get_mcp_server_by_name(server_name)
|
server_data = await self.ap.mcp_service.get_mcp_server_by_name(server_name)
|
||||||
@@ -60,6 +61,7 @@ class MCPRouterGroup(group.RouterGroup):
|
|||||||
async def _(server_name: str) -> str:
|
async def _(server_name: str) -> str:
|
||||||
"""测试MCP服务器连接"""
|
"""测试MCP服务器连接"""
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
server_name = unquote(server_name)
|
server_name = unquote(server_name)
|
||||||
server_data = await quart.request.json
|
server_data = await quart.request.json
|
||||||
task_id = await self.ap.mcp_service.test_mcp_server(server_name=server_name, server_data=server_data)
|
task_id = await self.ap.mcp_service.test_mcp_server(server_name=server_name, server_data=server_data)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ if typing.TYPE_CHECKING:
|
|||||||
# This file is kept for potential future extensions but the text marker
|
# This file is kept for potential future extensions but the text marker
|
||||||
# detection mechanism has been removed.
|
# detection mechanism has been removed.
|
||||||
|
|
||||||
|
|
||||||
def register_activated_skill(
|
def register_activated_skill(
|
||||||
ap: app.Application,
|
ap: app.Application,
|
||||||
query: pipeline_query.Query,
|
query: pipeline_query.Query,
|
||||||
@@ -31,4 +32,4 @@ def register_activated_skill(
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
skill_loader.register_activated_skill(query, skill_data)
|
skill_loader.register_activated_skill(query, skill_data)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pytestmark = pytest.mark.integration
|
|||||||
|
|
||||||
# ============== FIXTURE FOR SYS.MODULES ISOLATION ==============
|
# ============== FIXTURE FOR SYS.MODULES ISOLATION ==============
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope='module')
|
||||||
def mock_circular_import_chain():
|
def mock_circular_import_chain():
|
||||||
"""Break circular import chain for API controller."""
|
"""Break circular import chain for API controller."""
|
||||||
@@ -53,21 +54,25 @@ def mock_circular_import_chain():
|
|||||||
):
|
):
|
||||||
# Import groups after mocking to populate preregistered_groups
|
# Import groups after mocking to populate preregistered_groups
|
||||||
import langbot.pkg.api.http.controller.groups.pipelines.pipelines as _pipelines # noqa: E402, F401
|
import langbot.pkg.api.http.controller.groups.pipelines.pipelines as _pipelines # noqa: E402, F401
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
# ============== FAKE APPLICATION WITH PIPELINE SERVICES ==============
|
# ============== FAKE APPLICATION WITH PIPELINE SERVICES ==============
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope='module')
|
||||||
def fake_pipeline_app():
|
def fake_pipeline_app():
|
||||||
"""Create FakeApp with pipeline-specific services (module scope for reuse)."""
|
"""Create FakeApp with pipeline-specific services (module scope for reuse)."""
|
||||||
app = FakeApp()
|
app = FakeApp()
|
||||||
|
|
||||||
# Pipeline config
|
# Pipeline config
|
||||||
app.instance_config.data.update({
|
app.instance_config.data.update(
|
||||||
'api': {'port': 5300},
|
{
|
||||||
'system': {'allow_modify_login_info': True, 'limitation': {}},
|
'api': {'port': 5300},
|
||||||
})
|
'system': {'allow_modify_login_info': True, 'limitation': {}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Auth services
|
# Auth services
|
||||||
app.user_service = Mock()
|
app.user_service = Mock()
|
||||||
@@ -79,25 +84,31 @@ def fake_pipeline_app():
|
|||||||
|
|
||||||
# Pipeline service
|
# Pipeline service
|
||||||
app.pipeline_service = Mock()
|
app.pipeline_service = Mock()
|
||||||
app.pipeline_service.get_pipeline_metadata = AsyncMock(return_value=[
|
app.pipeline_service.get_pipeline_metadata = AsyncMock(
|
||||||
{'name': 'trigger', 'stages': []},
|
return_value=[
|
||||||
{'name': 'ai', 'stages': []},
|
{'name': 'trigger', 'stages': []},
|
||||||
])
|
{'name': 'ai', 'stages': []},
|
||||||
app.pipeline_service.get_pipelines = AsyncMock(return_value=[
|
]
|
||||||
{
|
)
|
||||||
|
app.pipeline_service.get_pipelines = AsyncMock(
|
||||||
|
return_value=[
|
||||||
|
{
|
||||||
|
'uuid': 'test-pipeline-uuid',
|
||||||
|
'name': 'Test Pipeline',
|
||||||
|
'description': 'Test description',
|
||||||
|
'created_at': '2024-01-01T00:00:00',
|
||||||
|
'updated_at': '2024-01-01T00:00:00',
|
||||||
|
'is_default': False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
app.pipeline_service.get_pipeline = AsyncMock(
|
||||||
|
return_value={
|
||||||
'uuid': 'test-pipeline-uuid',
|
'uuid': 'test-pipeline-uuid',
|
||||||
'name': 'Test Pipeline',
|
'name': 'Test Pipeline',
|
||||||
'description': 'Test description',
|
'config': {},
|
||||||
'created_at': '2024-01-01T00:00:00',
|
|
||||||
'updated_at': '2024-01-01T00:00:00',
|
|
||||||
'is_default': False,
|
|
||||||
}
|
}
|
||||||
])
|
)
|
||||||
app.pipeline_service.get_pipeline = AsyncMock(return_value={
|
|
||||||
'uuid': 'test-pipeline-uuid',
|
|
||||||
'name': 'Test Pipeline',
|
|
||||||
'config': {},
|
|
||||||
})
|
|
||||||
app.pipeline_service.create_pipeline = AsyncMock(return_value={'uuid': 'new-pipeline-uuid'})
|
app.pipeline_service.create_pipeline = AsyncMock(return_value={'uuid': 'new-pipeline-uuid'})
|
||||||
app.pipeline_service.update_pipeline = AsyncMock(return_value={})
|
app.pipeline_service.update_pipeline = AsyncMock(return_value={})
|
||||||
app.pipeline_service.delete_pipeline = AsyncMock()
|
app.pipeline_service.delete_pipeline = AsyncMock()
|
||||||
@@ -112,6 +123,10 @@ def fake_pipeline_app():
|
|||||||
app.mcp_service = Mock()
|
app.mcp_service = Mock()
|
||||||
app.mcp_service.get_mcp_servers = AsyncMock(return_value=[])
|
app.mcp_service.get_mcp_servers = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
# Skill service (for extensions endpoint)
|
||||||
|
app.skill_service = Mock()
|
||||||
|
app.skill_service.list_skills = AsyncMock(return_value=[])
|
||||||
|
|
||||||
# Plugin connector (for extensions endpoint)
|
# Plugin connector (for extensions endpoint)
|
||||||
app.plugin_connector.list_plugins = AsyncMock(return_value=[])
|
app.plugin_connector.list_plugins = AsyncMock(return_value=[])
|
||||||
|
|
||||||
@@ -130,6 +145,7 @@ async def quart_test_client(fake_pipeline_app, http_controller_cls):
|
|||||||
|
|
||||||
# ============== PIPELINE ENDPOINT TESTS ==============
|
# ============== PIPELINE ENDPOINT TESTS ==============
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||||
class TestPipelineMetadataEndpoint:
|
class TestPipelineMetadataEndpoint:
|
||||||
"""Tests for /api/v1/pipelines/_/metadata endpoint."""
|
"""Tests for /api/v1/pipelines/_/metadata endpoint."""
|
||||||
@@ -138,8 +154,7 @@ class TestPipelineMetadataEndpoint:
|
|||||||
async def test_get_pipeline_metadata_success(self, quart_test_client):
|
async def test_get_pipeline_metadata_success(self, quart_test_client):
|
||||||
"""GET /api/v1/pipelines/_/metadata returns metadata list."""
|
"""GET /api/v1/pipelines/_/metadata returns metadata list."""
|
||||||
response = await quart_test_client.get(
|
response = await quart_test_client.get(
|
||||||
'/api/v1/pipelines/_/metadata',
|
'/api/v1/pipelines/_/metadata', headers={'Authorization': 'Bearer test_token'}
|
||||||
headers={'Authorization': 'Bearer test_token'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -162,10 +177,7 @@ class TestPipelinesListEndpoint:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_pipelines_success(self, quart_test_client):
|
async def test_get_pipelines_success(self, quart_test_client):
|
||||||
"""GET /api/v1/pipelines returns pipeline list."""
|
"""GET /api/v1/pipelines returns pipeline list."""
|
||||||
response = await quart_test_client.get(
|
response = await quart_test_client.get('/api/v1/pipelines', headers={'Authorization': 'Bearer test_token'})
|
||||||
'/api/v1/pipelines',
|
|
||||||
headers={'Authorization': 'Bearer test_token'}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = await response.get_json()
|
data = await response.get_json()
|
||||||
@@ -176,8 +188,7 @@ class TestPipelinesListEndpoint:
|
|||||||
async def test_get_pipelines_with_sort_param(self, quart_test_client):
|
async def test_get_pipelines_with_sort_param(self, quart_test_client):
|
||||||
"""GET pipelines with sort parameter."""
|
"""GET pipelines with sort parameter."""
|
||||||
response = await quart_test_client.get(
|
response = await quart_test_client.get(
|
||||||
'/api/v1/pipelines?sort_by=created_at&sort_order=DESC',
|
'/api/v1/pipelines?sort_by=created_at&sort_order=DESC', headers={'Authorization': 'Bearer test_token'}
|
||||||
headers={'Authorization': 'Bearer test_token'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -193,8 +204,7 @@ class TestPipelinesCRUDEndpoints:
|
|||||||
async def test_get_single_pipeline_success(self, quart_test_client):
|
async def test_get_single_pipeline_success(self, quart_test_client):
|
||||||
"""GET /api/v1/pipelines/{uuid} returns pipeline."""
|
"""GET /api/v1/pipelines/{uuid} returns pipeline."""
|
||||||
response = await quart_test_client.get(
|
response = await quart_test_client.get(
|
||||||
'/api/v1/pipelines/test-pipeline-uuid',
|
'/api/v1/pipelines/test-pipeline-uuid', headers={'Authorization': 'Bearer test_token'}
|
||||||
headers={'Authorization': 'Bearer test_token'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -208,7 +218,7 @@ class TestPipelinesCRUDEndpoints:
|
|||||||
response = await quart_test_client.post(
|
response = await quart_test_client.post(
|
||||||
'/api/v1/pipelines',
|
'/api/v1/pipelines',
|
||||||
headers={'Authorization': 'Bearer test_token'},
|
headers={'Authorization': 'Bearer test_token'},
|
||||||
json={'name': 'New Pipeline', 'config': {}}
|
json={'name': 'New Pipeline', 'config': {}},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -222,7 +232,7 @@ class TestPipelinesCRUDEndpoints:
|
|||||||
response = await quart_test_client.put(
|
response = await quart_test_client.put(
|
||||||
'/api/v1/pipelines/test-pipeline-uuid',
|
'/api/v1/pipelines/test-pipeline-uuid',
|
||||||
headers={'Authorization': 'Bearer test_token'},
|
headers={'Authorization': 'Bearer test_token'},
|
||||||
json={'name': 'Updated Pipeline'}
|
json={'name': 'Updated Pipeline'},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -233,8 +243,7 @@ class TestPipelinesCRUDEndpoints:
|
|||||||
async def test_delete_pipeline_success(self, quart_test_client):
|
async def test_delete_pipeline_success(self, quart_test_client):
|
||||||
"""DELETE /api/v1/pipelines/{uuid} deletes pipeline."""
|
"""DELETE /api/v1/pipelines/{uuid} deletes pipeline."""
|
||||||
response = await quart_test_client.delete(
|
response = await quart_test_client.delete(
|
||||||
'/api/v1/pipelines/test-pipeline-uuid',
|
'/api/v1/pipelines/test-pipeline-uuid', headers={'Authorization': 'Bearer test_token'}
|
||||||
headers={'Authorization': 'Bearer test_token'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -245,8 +254,7 @@ class TestPipelinesCRUDEndpoints:
|
|||||||
async def test_copy_pipeline_success(self, quart_test_client):
|
async def test_copy_pipeline_success(self, quart_test_client):
|
||||||
"""POST /api/v1/pipelines/{uuid}/copy copies pipeline."""
|
"""POST /api/v1/pipelines/{uuid}/copy copies pipeline."""
|
||||||
response = await quart_test_client.post(
|
response = await quart_test_client.post(
|
||||||
'/api/v1/pipelines/test-pipeline-uuid/copy',
|
'/api/v1/pipelines/test-pipeline-uuid/copy', headers={'Authorization': 'Bearer test_token'}
|
||||||
headers={'Authorization': 'Bearer test_token'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -263,8 +271,7 @@ class TestPipelineExtensionsEndpoint:
|
|||||||
async def test_get_extensions(self, quart_test_client):
|
async def test_get_extensions(self, quart_test_client):
|
||||||
"""GET /api/v1/pipelines/{uuid}/extensions."""
|
"""GET /api/v1/pipelines/{uuid}/extensions."""
|
||||||
response = await quart_test_client.get(
|
response = await quart_test_client.get(
|
||||||
'/api/v1/pipelines/test-pipeline-uuid/extensions',
|
'/api/v1/pipelines/test-pipeline-uuid/extensions', headers={'Authorization': 'Bearer test_token'}
|
||||||
headers={'Authorization': 'Bearer test_token'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should return 200 if pipeline found
|
# Should return 200 if pipeline found
|
||||||
|
|||||||
@@ -58,4 +58,4 @@ export class ExtensionCardVO implements IExtensionCardVO {
|
|||||||
this.tools = prop.tools;
|
this.tools = prop.tools;
|
||||||
this.mode = prop.mode;
|
this.mode = prop.mode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -67,15 +67,15 @@ export default function PluginCardComponent({
|
|||||||
cardVO.type === 'mcp'
|
cardVO.type === 'mcp'
|
||||||
? 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300'
|
? 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300'
|
||||||
: cardVO.type === 'skill'
|
: cardVO.type === 'skill'
|
||||||
? 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300'
|
? 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300'
|
||||||
: 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300'
|
: 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{cardVO.type === 'mcp'
|
{cardVO.type === 'mcp'
|
||||||
? 'MCP'
|
? 'MCP'
|
||||||
: cardVO.type === 'skill'
|
: cardVO.type === 'skill'
|
||||||
? t('common.skill')
|
? t('common.skill')
|
||||||
: t('market.typePlugin')}
|
: t('market.typePlugin')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{cardVO.debug && (
|
{cardVO.debug && (
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ export default function PluginComponentList({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!useBadge && (
|
{!useBadge && (
|
||||||
<div
|
<div className="flex flex-row items-center justify-start gap-[0.2rem]">
|
||||||
className="flex flex-row items-center justify-start gap-[0.2rem]"
|
|
||||||
>
|
|
||||||
{kindIconMap[kind]}
|
{kindIconMap[kind]}
|
||||||
{responsive ? (
|
{responsive ? (
|
||||||
<span className="hidden md:inline">
|
<span className="hidden md:inline">
|
||||||
@@ -74,4 +72,4 @@ export default function PluginComponentList({
|
|||||||
{componentKindList.length === 0 && <div>{t('market.noComponents')}</div>}
|
{componentKindList.length === 0 && <div>{t('market.noComponents')}</div>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user