ci: 添加 GitHub Actions CI + 93 个测试
CI 工作流 (.github/workflows/ci.yml): - lint: 语法检查 + ruff 代码检查 - test: 插件功能测试 (Python 3.10/3.11/3.12 矩阵) - structure: 项目结构完整性 + 旧文件清理检查 测试覆盖 (tests/test_plugins.py, 93 个用例): - 插件加载: 13个插件 × 4项检查 (导入/类名/方法/tool_info) - code_editor: 9 个功能测试 (edit/read_range/find/ast/stats/diff...) - git_tools: 3 个测试 (status/log/branch) - cmd_executor: 5 个测试 (echo/python/拦截/超时/空命令) - file_manager: 7 个测试 (list/read/write/copy/delete/mkdir/info) - auto_engineer: 4 个测试 (metrics/security/complexity) - task_manager: 6 个测试 (add/list/complete/delete/clear) - 引擎: 4 个测试 (加载/旧文件清理) - 核心模块: 3 个导入测试 修复: code_editor diff_text 解析器被 split 拆坏的问题
This commit is contained in:
1 parent
9aae8b2ca2
commit
d57517197f
4 files changed
+562
-10
No files matched your search
@@ -0,0 +1,148 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, main]
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: 代码检查
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 安装 Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: pip install ruff
|
||||||
|
|
||||||
|
- name: 语法检查 (py_compile)
|
||||||
|
run: |
|
||||||
|
python -c "
|
||||||
|
import py_compile, os, sys
|
||||||
|
errors = 0
|
||||||
|
for root, dirs, files in os.walk('.'):
|
||||||
|
dirs[:] = [d for d in dirs if d not in {'.git', '__pycache__', 'node_modules', 'embedded_python'}]
|
||||||
|
for f in files:
|
||||||
|
if f.endswith('.py'):
|
||||||
|
path = os.path.join(root, f)
|
||||||
|
try:
|
||||||
|
py_compile.compile(path, doraise=True)
|
||||||
|
except py_compile.PyCompileError as e:
|
||||||
|
print(f'ERROR: {e}')
|
||||||
|
errors += 1
|
||||||
|
if errors:
|
||||||
|
print(f'\n{errors} 个文件有语法错误')
|
||||||
|
sys.exit(1)
|
||||||
|
print('所有文件语法正确')
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Ruff lint
|
||||||
|
run: ruff check . --select E,F,W --ignore E501,E402,W503 --exclude .git,__pycache__,embedded_python
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: 插件测试
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 安装 Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: 安装测试依赖
|
||||||
|
run: pip install pytest colorama
|
||||||
|
|
||||||
|
- name: 运行测试
|
||||||
|
run: pytest tests/ -v --tb=short
|
||||||
|
|
||||||
|
structure:
|
||||||
|
name: 项目结构检查
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 安装 Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: 检查必要文件
|
||||||
|
run: |
|
||||||
|
echo "检查项目结构..."
|
||||||
|
files=(
|
||||||
|
"ai_cli.py"
|
||||||
|
"launcher.py"
|
||||||
|
"xcli_core/cli_core.py"
|
||||||
|
"xcli_core/cli_base.py"
|
||||||
|
"xcli_core/config.py"
|
||||||
|
"xcli_core/constants.py"
|
||||||
|
"ai_engines/ollama_engine.py"
|
||||||
|
"ai_engines/openai_engine.py"
|
||||||
|
"plugins/code_editor.py"
|
||||||
|
"plugins/git_tools.py"
|
||||||
|
"plugins/cmd_executor.py"
|
||||||
|
"plugins/file_manager.py"
|
||||||
|
"plugins/auto_engineer.py"
|
||||||
|
)
|
||||||
|
missing=0
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
if [ -f "$f" ]; then
|
||||||
|
echo " ✓ $f"
|
||||||
|
else
|
||||||
|
echo " ✗ $f (缺失!)"
|
||||||
|
missing=$((missing + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ $missing -gt 0 ]; then
|
||||||
|
echo "\n${missing} 个文件缺失"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "\n项目结构完整"
|
||||||
|
|
||||||
|
- name: 检查旧文件已清理
|
||||||
|
run: |
|
||||||
|
old_files=(
|
||||||
|
"ai_engines/mimo_engine.py"
|
||||||
|
"ai_engines/GLM_http_engine.py"
|
||||||
|
"ai_engines/qwq_engine.py"
|
||||||
|
"ai_engines/xfyun_spark_engine.py"
|
||||||
|
"ai_engines/iflow_api_engine.py"
|
||||||
|
"ai_engines/maas_http_engine.py"
|
||||||
|
"ai_engines/SDK_openAI.py"
|
||||||
|
"plugins/git_workflow.py"
|
||||||
|
"plugins/code_search.py"
|
||||||
|
"plugins/project_analyzer.py"
|
||||||
|
"plugins/scheduler.py"
|
||||||
|
)
|
||||||
|
found=0
|
||||||
|
for f in "${old_files[@]}"; do
|
||||||
|
if [ -f "$f" ]; then
|
||||||
|
echo " ⚠️ 旧文件未清理: $f"
|
||||||
|
found=$((found + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ $found -gt 0 ]; then
|
||||||
|
echo "\n${found} 个旧文件未清理"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "旧文件已全部清理"
|
||||||
|
|
||||||
|
- name: 检查无重复引擎
|
||||||
|
run: |
|
||||||
|
engines=$(ls ai_engines/*_engine.py 2>/dev/null | wc -l)
|
||||||
|
echo "AI 引擎数量: $engines"
|
||||||
|
if [ "$engines" -gt 2 ]; then
|
||||||
|
echo "警告: 引擎数量超过 2 个 (ollama + openai)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "引擎数量正常"
|
||||||
+20
-10
@@ -88,13 +88,25 @@ class Plugin:
|
|||||||
|
|
||||||
def handle(self, args: str) -> str:
|
def handle(self, args: str) -> str:
|
||||||
try:
|
try:
|
||||||
parts = args.strip().split(maxsplit=2)
|
# 先提取操作名
|
||||||
if not parts:
|
first_space = args.strip().find(' ')
|
||||||
return "错误:请提供操作类型"
|
if first_space == -1:
|
||||||
|
operation = args.strip().lower()
|
||||||
|
arg1, arg2 = "", ""
|
||||||
|
else:
|
||||||
|
operation = args.strip()[:first_space].lower()
|
||||||
|
rest = args.strip()[first_space + 1:]
|
||||||
|
|
||||||
operation = parts[0].lower()
|
# diff_text 特殊处理:用 <<<>>> 分割
|
||||||
arg1 = parts[1] if len(parts) > 1 else ""
|
if operation == "diff_text" and " <<<>>> " in rest:
|
||||||
arg2 = parts[2] if len(parts) > 2 else ""
|
arg1, arg2 = rest.split(" <<<>>> ", 1)
|
||||||
|
else:
|
||||||
|
parts = rest.split(maxsplit=1)
|
||||||
|
arg1 = parts[0] if parts else ""
|
||||||
|
arg2 = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
if not operation:
|
||||||
|
return "错误:请提供操作类型"
|
||||||
|
|
||||||
handlers = {
|
handlers = {
|
||||||
# 编辑
|
# 编辑
|
||||||
@@ -384,11 +396,9 @@ class Plugin:
|
|||||||
diff = '\n'.join(difflib.unified_diff(l1, l2, fromfile=f1, tofile=f2, lineterm=''))
|
diff = '\n'.join(difflib.unified_diff(l1, l2, fromfile=f1, tofile=f2, lineterm=''))
|
||||||
return diff or "两个文件完全相同"
|
return diff or "两个文件完全相同"
|
||||||
|
|
||||||
def _op_diff_text(self, path: str, rest: str) -> str:
|
def _op_diff_text(self, old: str, new: str) -> str:
|
||||||
sep = " <<<>>> "
|
if not old and not new:
|
||||||
if sep not in rest:
|
|
||||||
return "错误:格式: diff_text <old> <<<>>> <new>"
|
return "错误:格式: diff_text <old> <<<>>> <new>"
|
||||||
old, new = rest.split(sep, 1)
|
|
||||||
return self._make_diff(old.strip(), new.strip())
|
return self._make_diff(old.strip(), new.strip())
|
||||||
|
|
||||||
def _op_ast_info(self, path: str, rest: str) -> str:
|
def _op_ast_info(self, path: str, rest: str) -> str:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# tests
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
"""插件加载与基础功能测试"""
|
||||||
|
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 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
|
||||||
Reference in new issue
Block a user