Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/tests/cli/root/test_plugin_sdk_commands.py
T
insistence 4bd83bc7cd feat: 支持 v2 插件、独立前端和签名制品发布 (#130)
* feat: 支持 v2 插件、原生扩展与签名发布

* fix: 修复测试

* feat: 增强宿主 SDK、运行观测与制品维护能力

* feat: 增强文件传输、配置观测与本地开发能力

* feat: 支持 SSE 实时事件流并完善开发脚手架

* feat: 完善长连接鉴权复核与关闭回收

* feat: 增强生命周期与任务关闭可靠性

* feat: 收紧健康检查契约并补齐前端与真实网络验收

* feat: 增加SDK溯源与离线更新工具并验证跨版本兼容

* feat: 增加持久化业务示例与受控 Python 交付

* feat: 增加统一运行诊断与多worker状态观测

* feat: 增加固定目标发布等待与自动验收

* fix: 修复多环境插件部署前缀与代理配置不一致

* fix: 修复测试
2026-10-09 23:49:45 +08:00

150 lines
5.5 KiB
Python

import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
import typer
from typer.testing import CliRunner
from cli.exit_codes import ARGUMENT_ERROR, DEPENDENCY_ERROR, SUCCESS
from cli.groups.plugin.commands.sdk import register_sdk_commands
BACKEND = Path(__file__).resolve().parents[3]
FRONTEND = BACKEND.parent / 'ruoyi-fastapi-frontend'
def sdk_app() -> typer.Typer:
"""
构造独立 SDK 命令入口,保留真实参数解析和结果输出。
:return: 包含 sdk 子命令的 Typer 应用
"""
app = typer.Typer()
register_sdk_commands(app)
return app
@pytest.fixture
def project(tmp_path: Path) -> Path:
"""
创建未跟踪 SDK 的旧 bundle 工程,避免读取业务插件。
:param tmp_path: 隔离测试目录
:return: 工程根目录
"""
root = tmp_path / 'example'
vendor = root / 'web' / 'vendor'
vendor.mkdir(parents=True)
(root / 'plugin.yaml').write_text(
'manifestVersion: 2\nid: example\nname: Example\nversion: 1.0.0\n'
'backend:\n module: plugins.example\n entrypoint: plugins.example:create_plugin\n integration: asgi\n'
'frontend:\n delivery:\n type: bundle\n',
encoding='utf-8',
)
(root / '__init__.py').write_text('raise RuntimeError("must never import this project")\n', encoding='utf-8')
for name in ('pluginBridge.js', 'pluginBridge.d.ts'):
(vendor / name).write_text('old untracked SDK\n', encoding='utf-8')
return root
def invoke_sdk(operation: str, project: Path, *options: str) -> tuple[int, dict]:
"""
使用真实离线 SDK 服务执行命令并解析结构化结果。
:param operation: check 或 update
:param project: 待检查工程目录
:param options: 额外 CLI 选项
:return: 退出码和 JSON 结果
"""
result = CliRunner().invoke(
sdk_app(), ['sdk', operation, str(project), '--frontend-root', str(FRONTEND), '--output', 'json', *options]
)
assert result.stdout, result.exception
return result.exit_code, json.loads(result.stdout)
def test_sdk_cli_requires_explicit_force_for_untracked_copy_and_dry_run_does_not_write(project: Path) -> None:
"""
验证旧工程纳管前可预演,默认拒绝写入且强制更新只修改 SDK。
:param project: 未跟踪 SDK 的隔离工程
:return: None
"""
before = {path.relative_to(project): path.read_bytes() for path in project.rglob('*') if path.is_file()}
code, payload = invoke_sdk('check', project)
assert code == DEPENDENCY_ERROR and payload['status'] == 'untracked'
code, payload = invoke_sdk('update', project)
assert code == DEPENDENCY_ERROR and not payload['ok']
code, payload = invoke_sdk('update', project, '--force', '--dry-run')
assert code == SUCCESS and payload['dryRun']
assert before == {path.relative_to(project): path.read_bytes() for path in project.rglob('*') if path.is_file()}
code, payload = invoke_sdk('update', project, '--force')
assert code == SUCCESS and payload['ok']
assert (project / '__init__.py').read_bytes() == before[Path('__init__.py')]
assert (project / '.plugin-sdk-backups').is_dir()
code, payload = invoke_sdk('check', project)
assert code == SUCCESS and payload['status'] == 'current'
assert payload['sdkVersion'] == payload['currentSdkVersion'] == '1.0.0'
def test_sdk_cli_bad_source_is_json_and_nonzero(project: Path, tmp_path: Path) -> None:
"""
验证来源 SDK 缺失时返回单份 JSON 和非零状态。
:param project: 独立测试工程
:param tmp_path: 不包含 SDK 的来源目录
:return: None
"""
result = CliRunner().invoke(
sdk_app(), ['sdk', 'check', str(project), '--frontend-root', str(tmp_path), '--output', 'json']
)
assert result.exit_code != SUCCESS
assert json.loads(result.stdout)['ok'] is False
def test_sdk_cli_invalid_output_is_argument_error(project: Path) -> None:
"""
验证 SDK 子命令复用 CLI 输出格式约束。
:param project: 独立测试工程
:return: None
"""
result = CliRunner().invoke(sdk_app(), ['sdk', 'check', str(project), '--output', 'xml'])
assert result.exit_code == ARGUMENT_ERROR
def test_sdk_check_cold_process_does_not_load_host_configuration_or_project(project: Path) -> None:
"""
验证实际根命令能在无宿主配置条件下离线运行,不导入业务或数据库。
:param project: 包含禁止执行代码的独立工程
:return: None
"""
script = """
import json, sys
from typer.testing import CliRunner
from cli.main import CLI_MAIN_RUNNER
app = CLI_MAIN_RUNNER.build_cli()
result = CliRunner().invoke(app, ['plugin', 'sdk', 'check', sys.argv[1], '--output', 'json'])
for name in ('config.env', 'config.database', 'plugins.core.runtime.explicit', 'plugins.example'):
assert name not in sys.modules, name
print(json.dumps({'exitCode': result.exit_code, 'payload': json.loads(result.stdout)}))
"""
environment = {**os.environ, 'RUOYI_PLUGIN_FRONTEND_ROOT': str(FRONTEND), 'DB_SOURCES': 'invalid-do-not-read'}
result = subprocess.run(
[sys.executable, '-X', 'utf8', '-c', script, str(project)],
cwd=BACKEND,
env=environment,
capture_output=True,
text=True,
encoding='utf-8',
timeout=30,
check=False,
)
assert result.returncode == SUCCESS, result.stderr
observed = json.loads(result.stdout)
assert observed['exitCode'] == DEPENDENCY_ERROR
assert observed['payload']['status'] == 'untracked'