mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-01 01:10:21 +08:00
* feat: support local plugin install * fix: make editable plugin install symlink * fix: harden local plugin install * Update tests/test_cli_plugin.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update astrbot/cli/commands/cmd_plug.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
191 lines
5.7 KiB
Python
191 lines
5.7 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
from click import ClickException
|
|
from click.testing import CliRunner
|
|
|
|
import astrbot.cli.utils.plugin as plugin_utils
|
|
from astrbot.cli.commands.cmd_plug import plug
|
|
|
|
|
|
def _write_plugin(path: Path, name: str = "astrbot_plugin_local_demo") -> None:
|
|
path.mkdir(parents=True)
|
|
(path / "metadata.yaml").write_text(
|
|
"\n".join(
|
|
[
|
|
f"name: {name}",
|
|
"desc: Local plugin",
|
|
"version: 1.0.0",
|
|
"author: AstrBot",
|
|
"repo: https://example.com/local-plugin",
|
|
],
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
(path / "main.py").write_text("PLUGIN_LOADED = True\n", encoding="utf-8")
|
|
|
|
|
|
def _write_ignored_plugin_files(path: Path) -> None:
|
|
for ignored_dir in [".git", ".venv", "__pycache__", ".idea", ".vscode", ".zed"]:
|
|
ignored_path = path / ignored_dir
|
|
ignored_path.mkdir()
|
|
(ignored_path / "ignored.txt").write_text("ignored\n", encoding="utf-8")
|
|
(path / "__pycache__" / "main.pyc").write_bytes(b"ignored")
|
|
|
|
|
|
def _write_astrbot_root(path: Path) -> None:
|
|
(path / ".astrbot").touch()
|
|
(path / "data" / "plugins").mkdir(parents=True)
|
|
|
|
|
|
def test_plugin_install_editable_symlinks_local_plugin(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "root"
|
|
source = tmp_path / "source-plugin"
|
|
root.mkdir()
|
|
_write_astrbot_root(root)
|
|
_write_plugin(source)
|
|
monkeypatch.chdir(root)
|
|
|
|
result = CliRunner().invoke(
|
|
plug,
|
|
["install", "-e", str(source)],
|
|
catch_exceptions=False,
|
|
)
|
|
|
|
target = root / "data" / "plugins" / "astrbot_plugin_local_demo"
|
|
target = root / "data" / "plugins" / "astrbot_plugin_local_demo"
|
|
assert result.exit_code == 0
|
|
assert target.is_symlink()
|
|
assert (target / "metadata.yaml").exists()
|
|
assert (target / "main.py").read_text(encoding="utf-8") == "PLUGIN_LOADED = True\n"
|
|
|
|
|
|
def test_plugin_install_accepts_local_path_without_editable_flag(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "root"
|
|
source = tmp_path / "source-plugin"
|
|
root.mkdir()
|
|
_write_astrbot_root(root)
|
|
_write_plugin(source)
|
|
_write_ignored_plugin_files(source)
|
|
monkeypatch.chdir(root)
|
|
|
|
result = CliRunner().invoke(plug, ["install", str(source)])
|
|
|
|
target = root / "data" / "plugins" / "astrbot_plugin_local_demo"
|
|
assert result.exit_code == 0
|
|
assert not target.is_symlink()
|
|
assert (target / "metadata.yaml").exists()
|
|
assert not (target / ".git").exists()
|
|
assert not (target / ".venv").exists()
|
|
assert not (target / "__pycache__").exists()
|
|
assert not (target / ".idea").exists()
|
|
assert not (target / ".vscode").exists()
|
|
assert not (target / ".zed").exists()
|
|
|
|
|
|
def test_plugin_install_editable_rejects_existing_plugin(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "root"
|
|
source = tmp_path / "source-plugin"
|
|
root.mkdir()
|
|
_write_astrbot_root(root)
|
|
_write_plugin(source)
|
|
_write_plugin(root / "data" / "plugins" / "astrbot_plugin_local_demo")
|
|
monkeypatch.chdir(root)
|
|
|
|
result = CliRunner().invoke(plug, ["install", "-e", str(source)])
|
|
|
|
assert result.exit_code != 0
|
|
assert "already exists" in result.output
|
|
|
|
|
|
def test_plugin_install_rejects_plugin_name_with_path_separator(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "root"
|
|
source = tmp_path / "source-plugin"
|
|
root.mkdir()
|
|
_write_astrbot_root(root)
|
|
_write_plugin(source, name="../bad_plugin")
|
|
monkeypatch.chdir(root)
|
|
|
|
result = CliRunner().invoke(plug, ["install", str(source)])
|
|
|
|
assert result.exit_code != 0
|
|
assert "invalid name" in result.output
|
|
assert not (root / "data" / "bad_plugin").exists()
|
|
|
|
|
|
def test_plugin_install_copy_does_not_delete_existing_target_on_race(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "root"
|
|
source = tmp_path / "source-plugin"
|
|
root.mkdir()
|
|
_write_astrbot_root(root)
|
|
_write_plugin(source)
|
|
monkeypatch.chdir(root)
|
|
|
|
target = root / "data" / "plugins" / "astrbot_plugin_local_demo"
|
|
target.mkdir()
|
|
marker = target / "keep.txt"
|
|
marker.write_text("keep\n", encoding="utf-8")
|
|
|
|
result = CliRunner().invoke(plug, ["install", str(source)])
|
|
|
|
assert result.exit_code != 0
|
|
assert "already exists" in result.output
|
|
assert marker.read_text(encoding="utf-8") == "keep\n"
|
|
|
|
|
|
def test_plugin_install_copy_does_not_delete_concurrently_created_target(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
source = tmp_path / "source-plugin"
|
|
plugins_dir = tmp_path / "plugins"
|
|
_write_plugin(source)
|
|
|
|
target = plugins_dir / "astrbot_plugin_local_demo"
|
|
|
|
def create_target_then_fail(
|
|
_source_path: Path,
|
|
_plugins_dir: Path,
|
|
_target_path: Path,
|
|
) -> None:
|
|
target.mkdir(parents=True)
|
|
(target / "keep.txt").write_text("keep\n", encoding="utf-8")
|
|
raise FileExistsError
|
|
|
|
monkeypatch.setattr(plugin_utils, "_copy_local_plugin", create_target_then_fail)
|
|
|
|
with pytest.raises(ClickException, match="already exists"):
|
|
plugin_utils.install_local_plugin(source, plugins_dir)
|
|
|
|
assert (target / "keep.txt").read_text(encoding="utf-8") == "keep\n"
|
|
|
|
|
|
def test_plugin_install_requires_name_or_editable_path(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "root"
|
|
root.mkdir()
|
|
_write_astrbot_root(root)
|
|
monkeypatch.chdir(root)
|
|
|
|
result = CliRunner().invoke(plug, ["install"])
|
|
|
|
assert result.exit_code != 0
|
|
assert "Missing plugin name or local plugin path" in result.output
|