Files
xiaoli-cli/tests/test_plugins.py
T
xiaoli-cli 9a711dc809 feat: 新增浏览器自动化插件 (browser_auto)
基于 Playwright 的无头浏览器操控,33 个操作:
- 连接: start/stop/status
- 导航: open/back/forward/reload/title/url
- 交互: click/fill/select/check/hover/press
- 提取: text/html/attr/value/eval/query
- 截图: screenshot (全页/元素)
- 等待: wait/wait-gone/wait-url
- 高级: pdf/cookies/set-cookie/viewport/download

测试: 101 个用例全部通过
2026-04-26 05:46:45 +08:00

425 lines
15 KiB
Python

"""插件加载与基础功能测试"""
import importlib.util
import os
import sys
import pytest
# 项目根目录
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PLUGINS_DIR = os.path.join(PROJECT_ROOT, "plugins")
ENGINES_DIR = os.path.join(PROJECT_ROOT, "ai_engines")
def discover_plugins():
"""发现所有插件文件"""
plugins = []
if not os.path.isdir(PLUGINS_DIR):
return plugins
for f in sorted(os.listdir(PLUGINS_DIR)):
if f.endswith(".py") and not f.startswith("__"):
plugins.append(f[:-3])
return plugins
def load_module(name, path):
"""动态加载模块"""
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# ══════════════════════════════════════
# 插件加载测试
# ══════════════════════════════════════
class TestPluginLoading:
"""测试所有插件能否正常加载"""
@pytest.mark.parametrize("plugin_name", discover_plugins())
def test_plugin_imports(self, plugin_name):
"""插件文件能被正常导入"""
path = os.path.join(PLUGINS_DIR, f"{plugin_name}.py")
mod = load_module(plugin_name, path)
assert mod is not None
@pytest.mark.parametrize("plugin_name", discover_plugins())
def test_plugin_has_class(self, plugin_name):
"""插件包含 Plugin 或 Liugin 类"""
path = os.path.join(PLUGINS_DIR, f"{plugin_name}.py")
mod = load_module(plugin_name, path)
cls = getattr(mod, "Plugin", None) or getattr(mod, "Liugin", None)
assert cls is not None, f"{plugin_name}: 缺少 Plugin/Liugin 类"
@pytest.mark.parametrize("plugin_name", discover_plugins())
def test_plugin_has_required_methods(self, plugin_name):
"""插件类包含必要的方法"""
path = os.path.join(PLUGINS_DIR, f"{plugin_name}.py")
mod = load_module(plugin_name, path)
cls = getattr(mod, "Plugin", None) or getattr(mod, "Liugin", None)
inst = cls()
assert hasattr(inst, "handle"), f"{plugin_name}: 缺少 handle 方法"
assert hasattr(inst, "get_tool_info"), f"{plugin_name}: 缺少 get_tool_info 方法"
assert hasattr(inst, "set_cli"), f"{plugin_name}: 缺少 set_cli 方法"
@pytest.mark.parametrize("plugin_name", discover_plugins())
def test_plugin_get_tool_info(self, plugin_name):
"""get_tool_info 返回正确的结构"""
path = os.path.join(PLUGINS_DIR, f"{plugin_name}.py")
mod = load_module(plugin_name, path)
cls = getattr(mod, "Plugin", None) or getattr(mod, "Liugin", None)
inst = cls()
info = inst.get_tool_info()
assert isinstance(info, dict)
assert "name" in info, f"{plugin_name}: tool_info 缺少 name"
assert "description" in info, f"{plugin_name}: tool_info 缺少 description"
assert "keywords" in info, f"{plugin_name}: tool_info 缺少 keywords"
assert isinstance(info["keywords"], list)
# ══════════════════════════════════════
# 插件功能测试
# ══════════════════════════════════════
class TestCodeEditor:
"""code_editor 功能测试"""
@pytest.fixture
def editor(self):
mod = load_module("code_editor", os.path.join(PLUGINS_DIR, "code_editor.py"))
return mod.Plugin()
@pytest.fixture
def tmp_file(self, tmp_path):
f = tmp_path / "test.py"
f.write_text("def hello():\n print('hello')\n\ndef world():\n print('world')\n")
return str(f)
def test_read_range(self, editor, tmp_file):
result = editor.handle(f"read_range {tmp_file} 1 2")
assert "hello" in result
assert "1 |" in result
def test_edit(self, editor, tmp_file):
result = editor.handle(f"edit {tmp_file} print('hello') <<<>>> print('hi')")
assert "已编辑" in result
content = open(tmp_file).read()
assert "print('hi')" in content
def test_edit_not_found(self, editor, tmp_file):
result = editor.handle(f"edit {tmp_file} nonexistent_code <<<>>> new_code")
assert "错误" in result
def test_create(self, editor, tmp_path):
new_file = str(tmp_path / "new.py")
result = editor.handle(f"create {new_file} import os")
assert "已创建" in result
assert os.path.exists(new_file)
def test_diff_text(self, editor):
result = editor.handle("diff_text old <<<>>> new")
assert "变更预览" in result
def test_ast_info(self, editor, tmp_file):
result = editor.handle(f"ast_info {tmp_file}")
assert "def hello" in result
assert "def world" in result
def test_stats(self, editor, tmp_path):
result = editor.handle(f"stats {tmp_path}")
assert "代码统计" in result
assert "文件" in result
def test_structure(self, editor, tmp_path):
result = editor.handle(f"structure {tmp_path} 1")
assert "📁" in result
def test_find(self, editor, tmp_path):
(tmp_path / "a.py").write_text("x = 1\ny = 2\n")
result = editor.handle(f"find {tmp_path} x *.py")
assert "x = 1" in result
class TestGitTools:
"""git_tools 功能测试"""
@pytest.fixture
def git(self):
mod = load_module("git_tools", os.path.join(PLUGINS_DIR, "git_tools.py"))
return mod.Plugin()
def test_status(self, git):
result = git.handle("status")
assert "Git" in result or "干净" in result or "分支" in result
def test_log(self, git):
result = git.handle("log --oneline 3")
assert isinstance(result, str)
assert len(result) > 0
def test_branch(self, git):
result = git.handle("branch")
assert "master" in result or "main" in result
class TestCmdExecutor:
"""cmd_executor 功能测试"""
@pytest.fixture
def cmd(self):
mod = load_module("cmd_executor", os.path.join(PLUGINS_DIR, "cmd_executor.py"))
return mod.Plugin()
def test_echo(self, cmd):
result = cmd.handle("run echo test_ok")
assert "test_ok" in result
def test_python_version(self, cmd):
result = cmd.handle("run python3 --version")
assert "Python" in result
def test_blocked_command(self, cmd):
result = cmd.handle("run rm -rf /")
assert "拦截" in result
def test_timeout(self, cmd):
result = cmd.handle("run echo fast --timeout 5")
assert "fast" in result
def test_empty_command(self, cmd):
result = cmd.handle("run")
assert "错误" in result
class TestFileManager:
"""file_manager 功能测试"""
@pytest.fixture
def fm(self):
mod = load_module("file_manager", os.path.join(PLUGINS_DIR, "file_manager.py"))
return mod.Plugin()
def test_list(self, fm, tmp_path):
(tmp_path / "a.txt").write_text("hi")
result = fm.handle(f"list {tmp_path}")
assert "a.txt" in result
def test_read(self, fm, tmp_path):
f = tmp_path / "test.txt"
f.write_text("hello world")
result = fm.handle(f"read {f}")
assert "hello world" in result
def test_write_and_read(self, fm, tmp_path):
f = tmp_path / "new.txt"
result = fm.handle(f"write {f} -c test_content")
assert "已写入" in result
result = fm.handle(f"read {f}")
assert "test_content" in result
def test_copy(self, fm, tmp_path):
src = tmp_path / "src.txt"
src.write_text("data")
dst = tmp_path / "dst.txt"
result = fm.handle(f"copy {src} {dst}")
assert "已复制" in result
assert dst.exists()
def test_delete(self, fm, tmp_path):
f = tmp_path / "del.txt"
f.write_text("bye")
result = fm.handle(f"delete {f}")
assert "已删除" in result
assert not f.exists()
def test_mkdir(self, fm, tmp_path):
d = tmp_path / "newdir"
result = fm.handle(f"mkdir {d}")
assert "已创建" in result
assert d.is_dir()
def test_info(self, fm, tmp_path):
f = tmp_path / "info.txt"
f.write_text("x")
result = fm.handle(f"info {f}")
assert "路径" in result
assert "大小" in result
class TestAutoEngineer:
"""auto_engineer 功能测试"""
@pytest.fixture
def eng(self):
mod = load_module("auto_engineer", os.path.join(PLUGINS_DIR, "auto_engineer.py"))
return mod.Plugin()
def test_metrics(self, eng, tmp_path):
(tmp_path / "a.py").write_text("x = 1\n# comment\n\ny = 2\n")
result = eng.handle(f"metrics {tmp_path}")
assert "代码指标" in result
assert "文件" in result
def test_security_clean(self, eng, tmp_path):
(tmp_path / "safe.py").write_text("x = 1\nprint(x)\n")
result = eng.handle(f"security {tmp_path}")
assert "没有发现" in result or "风险" in result
def test_security_risk(self, eng, tmp_path):
(tmp_path / "risk.py").write_text('password = "secret123"\neval("1+1")\n')
result = eng.handle(f"security {tmp_path}")
assert "风险" in result
def test_complexity(self, eng, tmp_path):
code = "def f():\n if True:\n if True:\n if True:\n pass\n"
(tmp_path / "complex.py").write_text(code)
result = eng.handle(f"complexity {tmp_path}")
assert isinstance(result, str)
class TestTaskManager:
"""task_manager 功能测试"""
@pytest.fixture
def tm(self, tmp_path, monkeypatch):
mod = load_module("task_manager", os.path.join(PLUGINS_DIR, "task_manager.py"))
inst = getattr(mod, "Liugin")()
inst.tasks = []
inst.tasks_file = str(tmp_path / "tasks.json")
return inst
def test_add(self, tm):
result = tm.handle("add 测试任务")
assert "已添加" in result
def test_list_empty(self, tm):
result = tm.handle("list")
assert "没有任务" in result
def test_add_and_list(self, tm):
tm.handle("add 任务A")
tm.handle("add 任务B")
result = tm.handle("list")
assert "任务A" in result
assert "任务B" in result
def test_complete(self, tm):
tm.handle("add 任务X")
result = tm.handle("complete 1")
assert "已更新" in result
def test_delete(self, tm):
tm.handle("add 任务Y")
result = tm.handle("delete 1")
assert "已删除" in result
def test_clear(self, tm):
tm.handle("add A")
tm.handle("complete 1")
result = tm.handle("clear")
assert "已清除" in result
# ══════════════════════════════════════
# 引擎加载测试
# ══════════════════════════════════════
class TestBrowserAuto:
"""browser_auto 功能测试"""
@pytest.fixture
def browser(self):
mod = load_module("browser_auto", os.path.join(PLUGINS_DIR, "browser_auto.py"))
return mod.Plugin()
def test_status_stopped(self, browser):
result = browser.handle("status")
assert "未运行" in result
def test_invalid_operation(self, browser):
result = browser.handle("nonexistent")
assert "错误" in result
def test_empty_args(self, browser):
result = browser.handle("")
assert "错误" in result
def test_tool_info(self, browser):
info = browser.get_tool_info()
assert info["name"] == "browser_auto"
assert "浏览器" in info["description"]
assert len(info["keywords"]) > 0
# ══════════════════════════════════════
# 引擎加载测试
# ══════════════════════════════════════
class TestEngines:
"""AI 引擎加载测试"""
def test_engines_dir_exists(self):
assert os.path.isdir(ENGINES_DIR), "ai_engines 目录不存在"
def test_ollama_engine_loads(self):
path = os.path.join(ENGINES_DIR, "ollama_engine.py")
if not os.path.exists(path):
pytest.skip("ollama_engine.py 不存在")
mod = load_module("ollama_engine", path)
cls = getattr(mod, "OllamaAI", None)
assert cls is not None, "缺少 OllamaAI 类"
inst = cls()
assert inst.name == "ollama"
def test_openai_engine_loads(self):
path = os.path.join(ENGINES_DIR, "openai_engine.py")
if not os.path.exists(path):
pytest.skip("openai_engine.py 不存在")
mod = load_module("openai_engine", path)
cls = getattr(mod, "OpenaiAI", None)
assert cls is not None, "缺少 OpenaiAI 类"
inst = cls()
assert inst.name == "openai"
def test_no_old_engines(self):
"""确认旧引擎文件已被移除"""
old_engines = [
"mimo_engine.py", "GLM_http_engine.py", "qwq_engine.py",
"xfyun_spark_engine.py", "iflow_api_engine.py",
"maas_http_engine.py", "SDK_openAI.py",
]
for name in old_engines:
path = os.path.join(ENGINES_DIR, name)
assert not os.path.exists(path), f"旧引擎未删除: {name}"
# ══════════════════════════════════════
# 核心模块导入测试
# ══════════════════════════════════════
class TestCoreImports:
"""核心模块能否正常导入"""
def test_import_config(self):
sys.path.insert(0, os.path.join(PROJECT_ROOT, "xcli_core"))
from config import load_config, save_config
assert callable(load_config)
def test_import_constants(self):
sys.path.insert(0, os.path.join(PROJECT_ROOT, "xcli_core"))
import constants
assert hasattr(constants, "DEFAULT_MAX_HISTORY")
def test_import_cli_base(self):
sys.path.insert(0, PROJECT_ROOT)
sys.path.insert(0, os.path.join(PROJECT_ROOT, "xcli_core"))
# 只测试语法,不实例化(需要完整环境)
spec = importlib.util.spec_from_file_location(
"cli_base", os.path.join(PROJECT_ROOT, "xcli_core", "cli_base.py")
)
mod = importlib.util.module_from_spec(spec)
# 不执行,只检查语法
assert spec is not None