180 lines
5.8 KiB
Python
180 lines
5.8 KiB
Python
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
MODULE_PATH = Path(__file__).resolve().parents[2] / "scripts" / "install_all_plugins.py"
|
|
SPEC = importlib.util.spec_from_file_location("install_all_plugins", MODULE_PATH)
|
|
install_all_plugins = importlib.util.module_from_spec(SPEC)
|
|
assert SPEC.loader is not None
|
|
sys.modules[SPEC.name] = install_all_plugins
|
|
SPEC.loader.exec_module(install_all_plugins)
|
|
|
|
|
|
PLUGIN_HEADER = '''"""
|
|
title: Example Plugin
|
|
version: 1.0.0
|
|
openwebui_id: 12345678-1234-1234-1234-123456789abc
|
|
description: Example description.
|
|
"""
|
|
'''
|
|
|
|
|
|
def write_plugin(path: Path, header: str) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(header + "\nclass Action:\n pass\n", encoding="utf-8")
|
|
|
|
|
|
def test_should_skip_plugin_file_filters_localized_and_helper_names():
|
|
assert (
|
|
install_all_plugins.should_skip_plugin_file(Path("flash_card_cn.py"))
|
|
== "localized _cn file"
|
|
)
|
|
assert (
|
|
install_all_plugins.should_skip_plugin_file(Path("verify_generation.py"))
|
|
== "test or helper script"
|
|
)
|
|
assert (
|
|
install_all_plugins.should_skip_plugin_file(Path("测试.py"))
|
|
== "non-ascii filename"
|
|
)
|
|
assert install_all_plugins.should_skip_plugin_file(Path("flash_card.py")) is None
|
|
|
|
|
|
def test_build_function_id_prefers_id_then_title_then_filename():
|
|
from_id = install_all_plugins.build_function_id(
|
|
Path("dummy.py"), {"id": "Async Context Compression"}
|
|
)
|
|
from_title = install_all_plugins.build_function_id(
|
|
Path("dummy.py"), {"title": "GitHub Copilot Official SDK Pipe"}
|
|
)
|
|
from_file = install_all_plugins.build_function_id(Path("dummy_plugin.py"), {})
|
|
|
|
assert from_id == "async_context_compression"
|
|
assert from_title == "github_copilot_official_sdk_pipe"
|
|
assert from_file == "dummy_plugin"
|
|
|
|
|
|
def test_build_payload_uses_native_tool_shape_for_tools():
|
|
candidate = install_all_plugins.PluginCandidate(
|
|
plugin_type="tool",
|
|
file_path=Path("plugins/tools/demo/demo_tool.py"),
|
|
metadata={
|
|
"title": "Demo Tool",
|
|
"description": "Demo tool description",
|
|
"openwebui_id": "12345678-1234-1234-1234-123456789abc",
|
|
},
|
|
content="class Tools:\n pass\n",
|
|
function_id="demo_tool",
|
|
)
|
|
|
|
payload = install_all_plugins.build_payload(candidate)
|
|
|
|
assert payload == {
|
|
"id": "demo_tool",
|
|
"name": "Demo Tool",
|
|
"meta": {
|
|
"description": "Demo tool description",
|
|
"manifest": {},
|
|
},
|
|
"content": "class Tools:\n pass\n",
|
|
"access_grants": [],
|
|
}
|
|
|
|
|
|
def test_build_api_urls_uses_tool_endpoints_for_tools():
|
|
candidate = install_all_plugins.PluginCandidate(
|
|
plugin_type="tool",
|
|
file_path=Path("plugins/tools/demo/demo_tool.py"),
|
|
metadata={"title": "Demo Tool"},
|
|
content="class Tools:\n pass\n",
|
|
function_id="demo_tool",
|
|
)
|
|
|
|
update_url, create_url = install_all_plugins.build_api_urls(
|
|
"http://localhost:3000", candidate
|
|
)
|
|
|
|
assert update_url == "http://localhost:3000/api/v1/tools/id/demo_tool/update"
|
|
assert create_url == "http://localhost:3000/api/v1/tools/create"
|
|
|
|
|
|
def test_discover_plugins_only_returns_supported_openwebui_plugins(
|
|
tmp_path, monkeypatch
|
|
):
|
|
actions_dir = tmp_path / "plugins" / "actions"
|
|
filters_dir = tmp_path / "plugins" / "filters"
|
|
pipes_dir = tmp_path / "plugins" / "pipes"
|
|
tools_dir = tmp_path / "plugins" / "tools"
|
|
|
|
write_plugin(actions_dir / "flash-card" / "flash_card.py", PLUGIN_HEADER)
|
|
write_plugin(actions_dir / "flash-card" / "flash_card_cn.py", PLUGIN_HEADER)
|
|
write_plugin(actions_dir / "infographic" / "verify_generation.py", PLUGIN_HEADER)
|
|
write_plugin(
|
|
filters_dir / "missing-id" / "missing_id.py", '"""\ntitle: Missing ID\n"""\n'
|
|
)
|
|
write_plugin(pipes_dir / "sdk" / "github_copilot_sdk.py", PLUGIN_HEADER)
|
|
write_plugin(tools_dir / "skills" / "openwebui_skills_manager.py", PLUGIN_HEADER)
|
|
|
|
monkeypatch.setattr(
|
|
install_all_plugins,
|
|
"PLUGIN_TYPE_DIRS",
|
|
{
|
|
"action": actions_dir,
|
|
"filter": filters_dir,
|
|
"pipe": pipes_dir,
|
|
"tool": tools_dir,
|
|
},
|
|
)
|
|
monkeypatch.setattr(install_all_plugins, "REPO_ROOT", tmp_path)
|
|
|
|
candidates, skipped = install_all_plugins.discover_plugins(
|
|
["action", "filter", "pipe", "tool"]
|
|
)
|
|
|
|
candidate_names = [candidate.file_path.name for candidate in candidates]
|
|
skipped_reasons = {path.name: reason for path, reason in skipped}
|
|
|
|
assert candidate_names == [
|
|
"flash_card.py",
|
|
"github_copilot_sdk.py",
|
|
"openwebui_skills_manager.py",
|
|
]
|
|
assert skipped_reasons["missing_id.py"] == "missing openwebui_id"
|
|
assert skipped_reasons["flash_card_cn.py"] == "localized _cn file"
|
|
assert skipped_reasons["verify_generation.py"] == "test or helper script"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("header", "expected_reason"),
|
|
[
|
|
('"""\ntitle: Missing ID\n"""\n', "missing openwebui_id"),
|
|
("class Action:\n pass\n", "missing plugin header"),
|
|
],
|
|
)
|
|
def test_discover_plugins_reports_missing_metadata(
|
|
tmp_path, monkeypatch, header, expected_reason
|
|
):
|
|
action_dir = tmp_path / "plugins" / "actions"
|
|
plugin_file = action_dir / "demo" / "demo.py"
|
|
write_plugin(plugin_file, header)
|
|
|
|
monkeypatch.setattr(
|
|
install_all_plugins,
|
|
"PLUGIN_TYPE_DIRS",
|
|
{
|
|
"action": action_dir,
|
|
"filter": tmp_path / "plugins" / "filters",
|
|
"pipe": tmp_path / "plugins" / "pipes",
|
|
"tool": tmp_path / "plugins" / "tools",
|
|
},
|
|
)
|
|
monkeypatch.setattr(install_all_plugins, "REPO_ROOT", tmp_path)
|
|
|
|
candidates, skipped = install_all_plugins.discover_plugins(["action"])
|
|
|
|
assert candidates == []
|
|
assert skipped == [(plugin_file, expected_reason)]
|