mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-16 06:58:51 +08:00
CLI 的 _normalize_init_plugin_name 函数现在自动添加 astrbot_plugin_ 前缀, 测试期望的目录名从 demo_plugin 更新为 astrbot_plugin_demo_plugin。
316 lines
9.2 KiB
Python
316 lines
9.2 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import io
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def _repo_root() -> Path:
|
|
return Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _source_env() -> dict[str, str]:
|
|
env = os.environ.copy()
|
|
src_new = str(_repo_root() / "src-new")
|
|
current = env.get("PYTHONPATH")
|
|
env["PYTHONPATH"] = f"{src_new}{os.pathsep}{current}" if current else src_new
|
|
return env
|
|
|
|
|
|
def test_testing_module_importable() -> None:
|
|
from astrbot_sdk import testing
|
|
|
|
assert testing.PluginHarness is not None
|
|
assert testing.MockContext is not None
|
|
|
|
|
|
def test_cli_help_works_from_source_tree() -> None:
|
|
process = subprocess.run(
|
|
[sys.executable, "-m", "astrbot_sdk", "--help"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
env=_source_env(),
|
|
)
|
|
|
|
assert process.returncode == 0, process.stderr
|
|
assert "Usage" in process.stdout
|
|
|
|
|
|
def test_dev_help_lists_watch_option() -> None:
|
|
process = subprocess.run(
|
|
[sys.executable, "-m", "astrbot_sdk", "dev", "--help"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
env=_source_env(),
|
|
)
|
|
|
|
assert process.returncode == 0, process.stderr
|
|
assert "--watch" in process.stdout
|
|
|
|
|
|
def test_init_plugin_template_includes_readme(tmp_path: Path, monkeypatch) -> None:
|
|
from astrbot_sdk.cli import _init_plugin
|
|
|
|
target = tmp_path / "astrbot_plugin_demo_plugin"
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
_init_plugin("demo_plugin")
|
|
|
|
assert (target / "README.md").exists()
|
|
readme = (target / "README.md").read_text(encoding="utf-8")
|
|
test_file = (target / "tests" / "test_plugin.py").read_text(encoding="utf-8")
|
|
|
|
assert "astrbot-sdk dev --local --watch --event-text hello" in readme
|
|
assert "PluginHarness.from_plugin_dir" in test_file
|
|
assert "test_hello_dispatch" in test_file
|
|
|
|
|
|
def test_mock_context_accepts_plugin_metadata() -> None:
|
|
from astrbot_sdk.testing import MockContext
|
|
|
|
ctx = MockContext(
|
|
plugin_id="demo_plugin",
|
|
plugin_metadata={
|
|
"display_name": "Demo Plugin",
|
|
"author": "tester",
|
|
"version": "1.2.3",
|
|
},
|
|
)
|
|
|
|
plugin = ctx.router._plugins["demo_plugin"].metadata
|
|
assert plugin["display_name"] == "Demo Plugin"
|
|
assert plugin["author"] == "tester"
|
|
assert plugin["version"] == "1.2.3"
|
|
|
|
|
|
def test_plugin_harness_from_plugin_dir_builds_expected_config() -> None:
|
|
from astrbot_sdk.testing import PluginHarness
|
|
|
|
plugin_dir = _repo_root() / "examples" / "hello_plugin"
|
|
|
|
harness = PluginHarness.from_plugin_dir(
|
|
plugin_dir,
|
|
session_id="custom-session",
|
|
platform="qq",
|
|
)
|
|
|
|
assert harness.config.plugin_dir == plugin_dir
|
|
assert harness.config.session_id == "custom-session"
|
|
assert harness.config.platform == "qq"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_plugin_harness_dispatches_sample_plugin() -> None:
|
|
from astrbot_sdk.testing import LocalRuntimeConfig, PluginHarness
|
|
|
|
plugin_dir = _repo_root() / "test_plugin" / "new"
|
|
|
|
async with PluginHarness(LocalRuntimeConfig(plugin_dir=plugin_dir)) as harness:
|
|
records = await harness.dispatch_text("hello")
|
|
|
|
assert any(record.text == "Echo: hello" for record in records)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_plugin_harness_supports_metadata_and_http_commands() -> None:
|
|
from astrbot_sdk.testing import LocalRuntimeConfig, PluginHarness
|
|
|
|
plugin_dir = _repo_root() / "test_plugin" / "new"
|
|
|
|
async with PluginHarness(LocalRuntimeConfig(plugin_dir=plugin_dir)) as harness:
|
|
plugin_records = await harness.dispatch_text("plugins")
|
|
api_records = await harness.dispatch_text("register_api")
|
|
|
|
assert any(
|
|
"astrbot_plugin_v4demo" in (record.text or "") for record in plugin_records
|
|
)
|
|
assert any(
|
|
"已注册 API,当前共 1 个" in (record.text or "") for record in api_records
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_example_hello_plugin_dispatches_commands() -> None:
|
|
from astrbot_sdk.testing import PluginHarness
|
|
|
|
plugin_dir = _repo_root() / "examples" / "hello_plugin"
|
|
|
|
async with PluginHarness.from_plugin_dir(plugin_dir) as harness:
|
|
hello_records = await harness.dispatch_text("hello")
|
|
about_records = await harness.dispatch_text("about")
|
|
|
|
assert any(record.text == "Hello, World!" for record in hello_records)
|
|
# about 命令返回 display_name "Hello Plugin",不是 name "hello_plugin"
|
|
assert any("Hello Plugin" in (record.text or "") for record in about_records)
|
|
|
|
|
|
def test_dev_infers_plugin_dir_from_current_directory() -> None:
|
|
plugin_dir = _repo_root() / "examples" / "hello_plugin"
|
|
process = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"astrbot_sdk",
|
|
"dev",
|
|
"--local",
|
|
"--event-text",
|
|
"hello",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
env=_source_env(),
|
|
cwd=plugin_dir,
|
|
)
|
|
|
|
assert process.returncode == 0, process.stderr
|
|
assert "[text][local-session] Hello, World!" in process.stdout
|
|
|
|
|
|
def test_dev_requires_plugin_dir_or_plugin_yaml_in_cwd(tmp_path: Path) -> None:
|
|
process = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"astrbot_sdk",
|
|
"dev",
|
|
"--local",
|
|
"--event-text",
|
|
"hello",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
env=_source_env(),
|
|
cwd=tmp_path,
|
|
)
|
|
|
|
assert process.returncode != 0
|
|
assert "当前目录未找到 plugin.yaml" in process.stderr
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_plugin_harness_reports_component_load_errors(tmp_path: Path) -> None:
|
|
from astrbot_sdk.testing import LocalRuntimeConfig, PluginHarness, _PluginLoadError
|
|
|
|
plugin_dir = tmp_path / "broken-plugin"
|
|
plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
(plugin_dir / "requirements.txt").write_text("", encoding="utf-8")
|
|
(plugin_dir / "plugin.yaml").write_text(
|
|
"\n".join(
|
|
[
|
|
"name: broken_demo",
|
|
"display_name: Broken Demo",
|
|
"author: test",
|
|
"version: 0.1.0",
|
|
"runtime:",
|
|
' python: "3.13"',
|
|
"components:",
|
|
" - class: main:MissingComponent",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
(plugin_dir / "main.py").write_text(
|
|
"\n".join(
|
|
[
|
|
"from astrbot_sdk import Star",
|
|
"",
|
|
"class PresentComponent(Star):",
|
|
" pass",
|
|
"",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
harness = PluginHarness(LocalRuntimeConfig(plugin_dir=plugin_dir))
|
|
with pytest.raises(_PluginLoadError) as raised:
|
|
await harness.start()
|
|
message = str(raised.value)
|
|
assert "插件 'broken_demo'" in message
|
|
assert "components[0].class='main:MissingComponent'" in message
|
|
assert "加载失败" in message
|
|
|
|
|
|
def _write_watch_plugin(plugin_dir: Path, *, reply_text: str) -> None:
|
|
plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
(plugin_dir / "requirements.txt").write_text("", encoding="utf-8")
|
|
(plugin_dir / "plugin.yaml").write_text(
|
|
"\n".join(
|
|
[
|
|
"name: watch_demo",
|
|
"display_name: Watch Demo",
|
|
"author: test",
|
|
"version: 0.1.0",
|
|
"runtime:",
|
|
' python: "3.13"',
|
|
"components:",
|
|
" - class: main:WatchDemo",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
(plugin_dir / "main.py").write_text(
|
|
"\n".join(
|
|
[
|
|
"from astrbot_sdk import Context, MessageEvent, Star, on_command",
|
|
"",
|
|
"class WatchDemo(Star):",
|
|
' @on_command("hello")',
|
|
" async def hello(self, event: MessageEvent, ctx: Context) -> None:",
|
|
f' await event.reply("{reply_text}")',
|
|
"",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_local_dev_watch_reloads_on_file_change(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
from astrbot_sdk.cli import _run_local_dev
|
|
|
|
plugin_dir = tmp_path / "watch-plugin"
|
|
_write_watch_plugin(plugin_dir, reply_text="v1")
|
|
|
|
stdout = io.StringIO()
|
|
monkeypatch.setattr(sys, "stdout", stdout)
|
|
|
|
task = asyncio.create_task(
|
|
_run_local_dev(
|
|
plugin_dir=plugin_dir,
|
|
event_text="hello",
|
|
interactive=False,
|
|
watch=True,
|
|
session_id="local-session",
|
|
user_id="local-user",
|
|
platform="test",
|
|
group_id=None,
|
|
event_type="message",
|
|
watch_poll_interval=0.05,
|
|
max_watch_reloads=1,
|
|
)
|
|
)
|
|
|
|
await asyncio.sleep(0.2)
|
|
_write_watch_plugin(plugin_dir, reply_text="v2")
|
|
|
|
await asyncio.wait_for(task, timeout=3.0)
|
|
|
|
output = stdout.getvalue()
|
|
assert "watch 模式已启动" in output
|
|
assert "检测到文件变更" in output
|
|
assert "[text][local-session] v1" in output
|
|
assert "[text][local-session] v2" in output
|