mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
Tighten external legacy plugin compatibility smoke tests
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
- 2026-03-13: Real legacy `Star` plugins may call compat context helpers on `self` or `self.context` during `__init__()` and `initialize()`, including `self.put_kv_data(...)`, `self.get_kv_data(...)`, and `self.context.get_config()`. Keep proxy methods on `LegacyStar`, expose a safe `LegacyContext.get_config()`, and bind the shared legacy context before lifecycle hooks run.
|
||||
- 2026-03-13: On Windows, `.gitignore` matching is case-insensitive. A broad entry like `astrBot/` will also ignore `src-new/astrbot/...`, which can silently hide real compat alias packages from `git status`. Keep that ignore anchored as `/astrBot/` if it is only meant for a root-level scratch checkout.
|
||||
- 2026-03-13: The reference checkout under `astrBot/astrbot/api` exposes a broader old plugin-facing package surface than the current `src-new/astrbot` alias package. When improving package-name compatibility, compare against those public `api/*` modules as a set instead of only patching the one import path hit by the latest migrated plugin.
|
||||
- 2026-03-13: Some real legacy plugins call `asyncio.create_task()` during object construction. Calling `load_plugin()` outside a running event loop can therefore produce false-negative compat failures even though the real worker path is fine. External compat smoke tests should load plugins under an active loop, and "real compatibility" claims should preferably exercise `SupervisorRuntime` + worker + a real handler invocation instead of import-only checks.
|
||||
|
||||
# 开发命令
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
- 2026-03-13: Real legacy `Star` plugins may call compat context helpers on `self` or `self.context` during `__init__()` and `initialize()`, including `self.put_kv_data(...)`, `self.get_kv_data(...)`, and `self.context.get_config()`. Keep proxy methods on `LegacyStar`, expose a safe `LegacyContext.get_config()`, and bind the shared legacy context before lifecycle hooks run.
|
||||
- 2026-03-13: On Windows, `.gitignore` matching is case-insensitive. A broad entry like `astrBot/` will also ignore `src-new/astrbot/...`, which can silently hide real compat alias packages from `git status`. Keep that ignore anchored as `/astrBot/` if it is only meant for a root-level scratch checkout.
|
||||
- 2026-03-13: The reference checkout under `astrBot/astrbot/api` exposes a broader old plugin-facing package surface than the current `src-new/astrbot` alias package. When improving package-name compatibility, compare against those public `api/*` modules as a set instead of only patching the one import path hit by the latest migrated plugin.
|
||||
- 2026-03-13: Some real legacy plugins call `asyncio.create_task()` during object construction. Calling `load_plugin()` outside a running event loop can therefore produce false-negative compat failures even though the real worker path is fine. External compat smoke tests should load plugins under an active loop, and "real compatibility" claims should preferably exercise `SupervisorRuntime` + worker + a real handler invocation instead of import-only checks.
|
||||
|
||||
# 开发命令
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
from loguru import logger
|
||||
|
||||
from astrbot_sdk._shared_preferences import sp
|
||||
from astrbot_sdk.api.basic import AstrBotConfig
|
||||
|
||||
from . import utils
|
||||
|
||||
__all__ = ["logger", "sp", "utils"]
|
||||
__all__ = ["AstrBotConfig", "logger", "sp", "utils"]
|
||||
|
||||
3
src-new/astrbot/core/message/__init__.py
Normal file
3
src-new/astrbot/core/message/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""旧版 ``astrbot.core.message`` 导入路径兼容入口。"""
|
||||
|
||||
from .components import * # noqa: F403
|
||||
3
src-new/astrbot/core/message/components.py
Normal file
3
src-new/astrbot/core/message/components.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""旧版 ``astrbot.core.message.components`` 导入路径兼容入口。"""
|
||||
|
||||
from astrbot_sdk.api.message.components import * # noqa: F403
|
||||
@@ -240,6 +240,20 @@ class TestAstrbotImportAlias:
|
||||
assert callable(SessionWaiter.trigger)
|
||||
assert callable(session_waiter)
|
||||
|
||||
def test_legacy_astrbot_core_root_exports(self):
|
||||
"""astrbot.core root should expose common legacy root names."""
|
||||
from astrbot.core import AstrBotConfig, logger, sp
|
||||
from astrbot.core.message.components import At, Image, Node, Nodes, Plain
|
||||
|
||||
assert AstrBotConfig is not None
|
||||
assert logger is not None
|
||||
assert sp is not None
|
||||
assert Plain(text="ok").text == "ok"
|
||||
assert Image(file="https://example.com/test.png").file
|
||||
assert At(qq="1").user_id == "1"
|
||||
assert Node(uin="1", content=[]).sender_id == "1"
|
||||
assert Nodes(nodes=[]).nodes == []
|
||||
|
||||
def test_legacy_astrbot_event_filter_module_exports(self):
|
||||
"""astrbot.api.event.filter should be importable from the old module path."""
|
||||
from astrbot.api.event.filter import EventMessageType, command, llm_tool
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
|
||||
默认不跑;手动验证真实外部插件时,设置
|
||||
``ASTRBOT_EXTERNAL_PLUGIN_REPO=https://...`` 即可启用。
|
||||
|
||||
如果还希望验证真实 handler 调用,而不是仅验证可加载,可以额外设置:
|
||||
|
||||
- ``ASTRBOT_EXTERNAL_PLUGIN_COMMAND=<command>``
|
||||
- ``ASTRBOT_EXTERNAL_PLUGIN_EVENT_TEXT=<text>`` (可选,默认等于 command)
|
||||
- ``ASTRBOT_EXTERNAL_PLUGIN_EXPECT_TEXT=<substring>`` (可选)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -14,12 +21,35 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot_sdk.protocol.messages import InitializeOutput, PeerInfo
|
||||
from astrbot_sdk.runtime.bootstrap import SupervisorRuntime
|
||||
from astrbot_sdk.runtime.loader import (
|
||||
PluginEnvironmentManager,
|
||||
load_plugin_spec,
|
||||
)
|
||||
from astrbot_sdk.runtime.peer import Peer
|
||||
|
||||
from tests_v4.helpers import make_transport_pair
|
||||
|
||||
EXTERNAL_PLUGIN_REPO_ENV = "ASTRBOT_EXTERNAL_PLUGIN_REPO"
|
||||
EXTERNAL_PLUGIN_COMMAND_ENV = "ASTRBOT_EXTERNAL_PLUGIN_COMMAND"
|
||||
EXTERNAL_PLUGIN_EVENT_TEXT_ENV = "ASTRBOT_EXTERNAL_PLUGIN_EVENT_TEXT"
|
||||
EXTERNAL_PLUGIN_EXPECT_TEXT_ENV = "ASTRBOT_EXTERNAL_PLUGIN_EXPECT_TEXT"
|
||||
|
||||
|
||||
def _clone_external_plugin(
|
||||
*,
|
||||
project_root: Path,
|
||||
repo_url: str,
|
||||
clone_dir: Path,
|
||||
) -> None:
|
||||
subprocess.run(
|
||||
["git", "clone", "--depth", "1", repo_url, str(clone_dir)],
|
||||
check=True,
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
@@ -33,12 +63,10 @@ def test_external_plugin_load_smoke():
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="astrbot-external-plugin-") as temp_dir:
|
||||
clone_dir = Path(temp_dir) / "plugin"
|
||||
subprocess.run(
|
||||
["git", "clone", "--depth", "1", repo_url, str(clone_dir)],
|
||||
check=True,
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
_clone_external_plugin(
|
||||
project_root=project_root,
|
||||
repo_url=repo_url,
|
||||
clone_dir=clone_dir,
|
||||
)
|
||||
|
||||
spec = load_plugin_spec(clone_dir)
|
||||
@@ -46,6 +74,7 @@ def test_external_plugin_load_smoke():
|
||||
python_path = manager.prepare_environment(spec)
|
||||
script = textwrap.dedent(
|
||||
f"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -55,11 +84,14 @@ def test_external_plugin_load_smoke():
|
||||
|
||||
from astrbot_sdk.runtime.loader import load_plugin, load_plugin_spec
|
||||
|
||||
spec = load_plugin_spec(plugin_dir)
|
||||
loaded = load_plugin(spec)
|
||||
print("PLUGIN", loaded.plugin.name)
|
||||
print("HANDLERS", len(loaded.handlers))
|
||||
print("CAPS", len(loaded.capabilities))
|
||||
async def main():
|
||||
spec = load_plugin_spec(plugin_dir)
|
||||
loaded = load_plugin(spec)
|
||||
print("PLUGIN", loaded.plugin.name)
|
||||
print("HANDLERS", len(loaded.handlers))
|
||||
print("CAPS", len(loaded.capabilities))
|
||||
|
||||
asyncio.run(main())
|
||||
"""
|
||||
)
|
||||
env = os.environ.copy()
|
||||
@@ -78,3 +110,100 @@ def test_external_plugin_load_smoke():
|
||||
)
|
||||
assert "HANDLERS" in result.stdout
|
||||
assert "PLUGIN" in result.stdout
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not (
|
||||
os.getenv(EXTERNAL_PLUGIN_REPO_ENV) and os.getenv(EXTERNAL_PLUGIN_COMMAND_ENV)
|
||||
),
|
||||
reason=(
|
||||
f"set {EXTERNAL_PLUGIN_REPO_ENV} and {EXTERNAL_PLUGIN_COMMAND_ENV} "
|
||||
"to enable external plugin runtime command smoke tests"
|
||||
),
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_external_plugin_runtime_command_smoke():
|
||||
"""按需拉起真实 supervisor/worker 链路并执行一个外部插件命令。"""
|
||||
repo_url = os.environ[EXTERNAL_PLUGIN_REPO_ENV]
|
||||
command_name = os.environ[EXTERNAL_PLUGIN_COMMAND_ENV]
|
||||
event_text = os.getenv(EXTERNAL_PLUGIN_EVENT_TEXT_ENV) or command_name
|
||||
expected_text = os.getenv(EXTERNAL_PLUGIN_EXPECT_TEXT_ENV)
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="astrbot-external-runtime-") as temp_dir:
|
||||
plugins_root = Path(temp_dir) / "plugins"
|
||||
plugin_root = plugins_root / "external_plugin"
|
||||
_clone_external_plugin(
|
||||
project_root=project_root,
|
||||
repo_url=repo_url,
|
||||
clone_dir=plugin_root,
|
||||
)
|
||||
|
||||
left, right = make_transport_pair()
|
||||
core = Peer(
|
||||
transport=left,
|
||||
peer_info=PeerInfo(name="outer-core", role="core", version="v4"),
|
||||
)
|
||||
core.set_initialize_handler(
|
||||
lambda _message: asyncio.sleep(
|
||||
0,
|
||||
result=InitializeOutput(
|
||||
peer=PeerInfo(name="outer-core", role="core", version="v4"),
|
||||
capabilities=[],
|
||||
metadata={},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
runtime = SupervisorRuntime(
|
||||
transport=right,
|
||||
plugins_dir=plugins_root,
|
||||
env_manager=PluginEnvironmentManager(project_root),
|
||||
)
|
||||
await core.start()
|
||||
try:
|
||||
await runtime.start()
|
||||
await core.wait_until_remote_initialized()
|
||||
|
||||
handler = next(
|
||||
(
|
||||
item
|
||||
for item in core.remote_handlers
|
||||
if getattr(item.trigger, "command", None) == command_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert handler is not None, (
|
||||
f"command handler not found: {command_name}; "
|
||||
f"available={[getattr(item.trigger, 'command', None) for item in core.remote_handlers]}"
|
||||
)
|
||||
|
||||
await core.invoke(
|
||||
"handler.invoke",
|
||||
{
|
||||
"handler_id": handler.id,
|
||||
"event": {
|
||||
"text": event_text,
|
||||
"session_id": "external-smoke-session",
|
||||
"user_id": "user-1",
|
||||
"platform": "test",
|
||||
},
|
||||
},
|
||||
request_id="external-runtime-command",
|
||||
)
|
||||
|
||||
sent_messages = list(runtime.capability_router.sent_messages)
|
||||
assert sent_messages, (
|
||||
"external plugin command completed but did not emit any platform "
|
||||
"message; this usually means the command path was not really exercised"
|
||||
)
|
||||
|
||||
if expected_text is not None:
|
||||
assert any(
|
||||
expected_text in item.get("text", "")
|
||||
for item in sent_messages
|
||||
if "text" in item
|
||||
), sent_messages
|
||||
finally:
|
||||
await runtime.stop()
|
||||
await core.stop()
|
||||
|
||||
Reference in New Issue
Block a user