Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion ncatbot/cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


# ---------------------------------------------------------------------------
# 各平台模板插件内容
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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():
Expand Down
4 changes: 2 additions & 2 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/ # 端到端测试
Expand Down Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
7 changes: 7 additions & 0 deletions tests/unit/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 后缀 |
53 changes: 53 additions & 0 deletions tests/unit/cli/test_init.py
Original file line number Diff line number Diff line change
@@ -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")
Loading