From 9e233b43c82d9a7298c19329771cf659541f1a2f Mon Sep 17 00:00:00 2001 From: GlowLED <3592147348@qq.com> Date: Tue, 9 Jun 2026 22:22:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(cli):=20=E4=BF=AE=E5=A4=8D=E6=95=B0?= =?UTF-8?q?=E5=AD=97=E5=BC=80=E5=A4=B4=E7=94=A8=E6=88=B7=E5=90=8D=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=20init=20=E6=A8=A1=E6=9D=BF=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E7=B1=BB=E5=90=8D=E9=9D=9E=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数字开头的系统用户名经 _generate_template_plugin 派生出以数字开头的 入口类名(如 35921PluginPlugin),生成的 plugin.py 含非法标识符, 加载时抛 SyntaxError。新增 _to_class_name() 保证类名始终为合法标识符 并消除重复的 Plugin 后缀。新增回归测试 CX-21/CX-22。 --- ncatbot/cli/commands/init.py | 16 ++++++++++- tests/README.md | 4 +-- tests/unit/README.md | 2 +- tests/unit/cli/README.md | 7 +++++ tests/unit/cli/test_init.py | 53 ++++++++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/unit/cli/test_init.py diff --git a/ncatbot/cli/commands/init.py b/ncatbot/cli/commands/init.py index 1495a40a..d6590121 100644 --- a/ncatbot/cli/commands/init.py +++ b/ncatbot/cli/commands/init.py @@ -89,6 +89,20 @@ def _sanitize_plugin_name(name: str) -> str: return sanitized or "my_plugin" +def _to_class_name(plugin_name: str) -> str: + """从插件名(合法标识符)派生入口类名。 + + 生成 CamelCase 名称并以 Plugin 结尾,保证结果始终是合法的 Python 标识符 + (数字开头时前置下划线),避免数字开头的用户名生成非法类名。 + """ + camel = "".join(part.capitalize() for part in plugin_name.split("_") if part) + if not camel.endswith("Plugin"): + camel += "Plugin" + if not camel or camel[0].isdigit(): + camel = "_" + camel + return camel + + # --------------------------------------------------------------------------- # 各平台模板插件内容 # --------------------------------------------------------------------------- @@ -216,7 +230,7 @@ def _generate_template_plugin( """在 plugins/ 下生成以当前用户名命名的模板插件。""" username = getpass.getuser() plugin_name = _sanitize_plugin_name(username) + "_plugin" - class_name = plugin_name.title().replace("_", "") + "Plugin" + class_name = _to_class_name(plugin_name) plugins_dir = plugins_path / plugin_name if plugins_dir.exists(): diff --git a/tests/README.md b/tests/README.md index 9f27c6d8..27580e6a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -15,7 +15,7 @@ tests/ │ ├── plugin/ # 插件 Mixin + 导入去重 + Loader (M-01 ~ M-41, ID-01 ~ ID-02, LD-01 ~ LD-05) │ ├── adapter/ # 适配器解析 + 注册表 + 真实数据 + 事件日志格式 (P-01 ~ P-07, RF-01 ~ RF-08, AR-01 ~ AR-05, SL-01 ~ SL-04, GM-01 ~ GM-05, BL-01 ~ BL-25, GH-01 ~ GH-11, LK-01 ~ LK-09, LKP-01 ~ LKP-10, ELS-01 ~ ELS-17) │ ├── config/ # 配置迁移 + 安全 + 分层 + 事件日志格式 (CF-01 ~ CF-05, CS-01 ~ CS-05, CE-01 ~ CE-05, BQ-01 ~ BQ-11, AI-03 ~ AI-20, ELF-01 ~ ELF-06) -│ ├── cli/ # CLI 冒烟 (CX-01 ~ CX-20) +│ ├── cli/ # CLI 冒烟 (CX-01 ~ CX-22) │ └── webui/ # WebUI 单元测试 (WUI-01 ~ WUI-14) ├── integration/ # 集成测试 (I-01 ~ I-21, WUI-I-01 ~ WUI-I-04) ├── e2e/ # 端到端测试 @@ -71,7 +71,7 @@ python tests/e2e/napcat/run.py | AR | AdapterRegistry | AR-01 ~ AR-05 | | CF | Config Migration | CF-01 ~ CF-05 | | CE | Config 分层与运行时覆盖 | CE-01 ~ CE-05 | -| CX | CLI 冒烟 | CX-01 ~ CX-20 | +| CX | CLI 冒烟 | CX-01 ~ CX-22 | | SL | SnowLuma 适配器 / 配置 | SL-01 ~ SL-04 | | D | AsyncEventDispatcher | D-01 ~ D-09 | | K | Hook System | K-01 ~ K-22 | diff --git a/tests/unit/README.md b/tests/unit/README.md index df52431d..f9cfd94e 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -14,7 +14,7 @@ | [plugin/](plugin/) | `ncatbot.plugin.mixin` | M-01 ~ M-41, ID-01 ~ ID-02, LD-01 ~ LD-05 | | [adapter/](adapter/) | `ncatbot.adapter.napcat` | P-01 ~ P-07, RF-01 ~ RF-08, AR-01 ~ AR-05, GM-01 ~ GM-05, BL-01 ~ BL-14, GH-01 ~ GH-11 | | [config/](config/) | `ncatbot.utils.config` | CF-01 ~ CF-05, CS-01 ~ CS-05, CE-01 ~ CE-05 | -| [cli/](cli/) | `ncatbot.cli` | CX-01 ~ CX-14 | +| [cli/](cli/) | `ncatbot.cli` | CX-01 ~ CX-22 | ## 公共 Fixtures (`conftest.py`) diff --git a/tests/unit/cli/README.md b/tests/unit/cli/README.md index 890bb622..12e66dde 100644 --- a/tests/unit/cli/README.md +++ b/tests/unit/cli/README.md @@ -40,3 +40,10 @@ | CX-18 | `snowluma diagnose ws` | `--uri` / `--token` 传入 `_check_ws()` | | CX-19 | `snowluma stop` (Linux) | 调用 `PlatformOps.stop_snowluma()` | | CX-20 | `snowluma install --yes` | `--yes` 绑定到 `SnowLumaInstaller.install(skip_confirm=True)` | + +### init 模板插件生成 (`test_init.py`) + +| 规范 ID | 说明 | 验证点 | +|---------|------|--------| +| CX-21 | 数字开头用户名 | 模拟 `getpass.getuser` 返回数字用户名,生成的 `plugin.py` 可编译、`entry_class` 为合法标识符且与类名一致 | +| CX-22 | `_to_class_name` | 始终产出合法标识符(数字开头前置 `_`),且不重复 `Plugin` 后缀 | diff --git a/tests/unit/cli/test_init.py b/tests/unit/cli/test_init.py new file mode 100644 index 00000000..82484810 --- /dev/null +++ b/tests/unit/cli/test_init.py @@ -0,0 +1,53 @@ +"""init 模板插件生成单元测试。 + +规范: + CX-21: 数字开头的用户名生成的模板插件入口类名合法且可编译加载 + CX-22: _to_class_name 始终产出合法标识符且不重复 Plugin 后缀 +""" + +from __future__ import annotations + +import tomllib +from pathlib import Path +from unittest.mock import patch + +import pytest + +from ncatbot.cli.commands.init import _generate_template_plugin, _to_class_name + + +def test_numeric_username_generates_loadable_plugin(tmp_path: Path): + """CX-21: 数字开头的用户名生成的模板插件入口类名合法且可编译加载。""" + with patch("ncatbot.cli.commands.init.getpass.getuser", return_value="35921"): + _generate_template_plugin(tmp_path) + + plugin_dirs = [p for p in tmp_path.iterdir() if p.is_dir()] + assert len(plugin_dirs) == 1 + pdir = plugin_dirs[0] + + # 生成的 plugin.py 必须是合法 Python(数字开头用户名曾导致 SyntaxError) + source = (pdir / "plugin.py").read_text(encoding="utf-8") + compile(source, "plugin.py", "exec") # 不应抛 SyntaxError + + # manifest 的 entry_class 必须是合法标识符,且与 plugin.py 中的类名一致 + manifest = tomllib.loads((pdir / "manifest.toml").read_text(encoding="utf-8")) + entry_class = manifest["entry_class"] + assert entry_class.isidentifier() + assert f"class {entry_class}(" in source + + +@pytest.mark.parametrize( + "plugin_name, expected", + [ + ("_35921_plugin", "_35921Plugin"), # 纯数字用户名 + ("john_plugin", "JohnPlugin"), # 普通用户名:无重复 Plugin 后缀 + ("_1234abc_plugin", "_1234abcPlugin"), # 数字开头混合 + ("alice_dev_plugin", "AliceDevPlugin"), + ], +) +def test_to_class_name_is_valid_identifier(plugin_name: str, expected: str): + """CX-22: _to_class_name 始终产出合法标识符且不重复 Plugin 后缀。""" + result = _to_class_name(plugin_name) + assert result == expected + assert result.isidentifier() + assert not result.endswith("PluginPlugin")