core/cli: plugins list/enable/disable commands and /plugins slash

This commit is contained in:
2026-07-24 16:44:44 +08:00
parent 1d718582ac
commit 03e04f9a38
9 changed files with 588 additions and 9 deletions
+105
View File
@@ -0,0 +1,105 @@
"""CLI ``plyngent plugins`` and slash ``/plugins``."""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import patch
import pytest
import tomlkit
from click.testing import CliRunner
from plyngent.cli.app import main
from plyngent.cli.slash import handle_slash
from plyngent.cli.state import ReplState
from plyngent.config.models import DatabaseConfig, OpenAIProvider
from plyngent.config.store import ConfigStore
from plyngent.memory import MemoryStore
from plyngent.tools.plugins import DiscoveredPlugin, PluginStatus
if TYPE_CHECKING:
from collections.abc import AsyncIterator
from pathlib import Path
@pytest.fixture
async def state(tmp_path: Path) -> AsyncIterator[ReplState]:
memory = await MemoryStore.open(DatabaseConfig())
provider = OpenAIProvider(access_key_or_token="sk-test")
config = ConfigStore(path=tmp_path / "plyngent.toml", document=tomlkit.document())
config.providers = {"local": provider}
st = ReplState(
config=config,
memory=memory,
workspace=tmp_path,
provider_name="local",
provider=provider,
model="gpt-test",
tools_enabled=False,
)
# ReplState builds a real client; slash plugin tests only need config + agent.
await st.new_session("t")
yield st
await memory.close()
def _fake_statuses(*, enable: list[str], disable: list[str]) -> list[PluginStatus]:
del enable, disable
return [
PluginStatus(
plugin=DiscoveredPlugin(id="acme", value="acme_pkg:load", package="acme-pkg", version="1.0"),
enabled=True,
disabled=False,
),
PluginStatus(
plugin=DiscoveredPlugin(id="other", value="other:load", package=None, version=None),
enabled=False,
disabled=True,
),
]
def test_plugins_list_cmd(tmp_path: Path) -> None:
config = tmp_path / "plyngent.toml"
_ = config.write_text(
"""
[plugins]
enable = ["acme"]
disable = ["other"]
""",
encoding="utf-8",
)
runner = CliRunner()
with patch("plyngent.tools.plugins.list_plugin_statuses", side_effect=_fake_statuses):
result = runner.invoke(main, ["plugins", "list", "--config", str(config)])
assert result.exit_code == 0
assert "acme" in result.output
assert "enabled" in result.output
assert "other" in result.output
assert "disabled" in result.output
def test_plugins_enable_disable_write(tmp_path: Path) -> None:
config = tmp_path / "plyngent.toml"
_ = config.write_text("", encoding="utf-8")
runner = CliRunner()
result = runner.invoke(main, ["plugins", "enable", "acme", "--config", str(config)])
assert result.exit_code == 0
text = config.read_text(encoding="utf-8")
assert "acme" in text
result = runner.invoke(main, ["plugins", "disable", "acme", "--config", str(config)])
assert result.exit_code == 0
text = config.read_text(encoding="utf-8")
assert "disable" in text
async def test_slash_plugins_enable_reload(state: ReplState) -> None:
with patch("plyngent.tools.plugins.list_plugin_statuses", return_value=[]):
assert await handle_slash(state, "/plugins list") is True
assert await handle_slash(state, "/plugins enable acme") is True
assert "acme" in state.config.plugins_config.enable
assert await handle_slash(state, "/plugins disable acme") is True
assert "acme" in state.config.plugins_config.disable
assert await handle_slash(state, "/plugins undeny acme") is True
assert "acme" not in state.config.plugins_config.disable
assert await handle_slash(state, "/plugins reload") is True