diff --git a/AGENTS.md b/AGENTS.md
index 97de21fb2..7c07957bb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -29,7 +29,21 @@ Runs on `http://localhost:3000` by default.
4. When committing, ensure to use conventional commits messages, such as `feat: add new agent for data analysis` or `fix: resolve bug in provider manager`.
5. Use English for all new comments.
6. For path handling, use `pathlib.Path` instead of string paths, and use `astrbot.core.utils.path_utils` to get the AstrBot data and temp directory.
-7. Use Python 3.12+ type hinting syntax (e.g., `list[str]` over `List[str]`, `int | None` over `Optional[int]`). Avoid using `Any` and ensure comprehensive type annotations are provided.
+7. Use Python 3.12+ type hinting syntax (e.g., `list[str]` over `List[str]`, `int | None` over `Optional[int]`). Avoid using `Any` and `cast()` - use proper TypedDict, dataclass, or Protocol instead. When encountering dict access issues (e.g., `msg.get("key")` where ty infers wrong type), define a TypedDict with `total=False` to explicitly declare allowed keys.
+
+ Good example:
+ ```python
+ class MessageComponent(TypedDict, total=False):
+ type: str
+ text: str
+ path: str
+ ```
+
+ Bad example (avoid):
+ ```python
+ msg: Any = something
+ msg = cast(dict, msg)
+ ```
8. When introducing new environment variables:
- Use the `ASTRBOT_` prefix for naming (e.g., `ASTRBOT_ENABLE_FEATURE`).
- Add the variable and description to `.env.example`.
diff --git a/README.md b/README.md
index 1bf6b6f7c..d509f33ed 100644
--- a/README.md
+++ b/README.md
@@ -304,4 +304,4 @@ _私は、高性能ですから!_
-
\ No newline at end of file
+
diff --git a/README_fr.md b/README_fr.md
index 28749bd6d..3be44dfc3 100644
--- a/README_fr.md
+++ b/README_fr.md
@@ -301,4 +301,4 @@ _私は、高性能ですから!_ (Je suis performant !)
-
\ No newline at end of file
+
diff --git a/README_ja.md b/README_ja.md
index 85da2f1ef..b724ab3ae 100644
--- a/README_ja.md
+++ b/README_ja.md
@@ -301,4 +301,4 @@ _私は、高性能ですから!_
-
\ No newline at end of file
+
diff --git a/README_ru.md b/README_ru.md
index 41ee40c3c..6c5af3e9b 100644
--- a/README_ru.md
+++ b/README_ru.md
@@ -301,4 +301,4 @@ _私は、高性能ですから!_ (Я высокопроизводительны
-
\ No newline at end of file
+
diff --git a/README_zh-TW.md b/README_zh-TW.md
index a754aa7e9..2688c956c 100644
--- a/README_zh-TW.md
+++ b/README_zh-TW.md
@@ -301,4 +301,4 @@ _私は、高性能ですから!_
-
\ No newline at end of file
+
diff --git a/astrbot/cli/utils/dashboard.py b/astrbot/cli/utils/dashboard.py
index c1875bd61..e6a435194 100644
--- a/astrbot/cli/utils/dashboard.py
+++ b/astrbot/cli/utils/dashboard.py
@@ -1,4 +1,5 @@
import os
+import sys
from importlib import resources
from pathlib import Path
@@ -24,10 +25,10 @@ class DashboardManager:
match dashboard_version:
case None:
click.echo("Dashboard is not installed")
- # Skip interactive prompt when running under systemd
- if os.environ.get("ASTRBOT_SYSTEMD") == "1":
+ # Skip interactive prompt in non-interactive environments
+ if not sys.stdin.isatty():
click.echo(
- "Skipping interactive dashboard installation in systemd mode."
+ "Skipping interactive dashboard installation in non-interactive mode."
)
return
if click.confirm(
diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py
index 1c3e5df2f..eeaec8234 100644
--- a/astrbot/core/agent/tool.py
+++ b/astrbot/core/agent/tool.py
@@ -72,7 +72,7 @@ class FunctionTool(ToolSchema, Generic[TContext]):
def __repr__(self) -> str:
return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
- async def call(self, context: ContextWrapper[TContext], **kwargs) -> ToolExecResult:
+ async def call(self, context: ContextWrapper[TContext], **kwargs: Any) -> ToolExecResult:
"""Run the tool with the given arguments. The handler field has priority."""
raise NotImplementedError(
"FunctionTool.call() must be implemented by subclasses or set a handler."
diff --git a/astrbot/core/computer/booters/bwrap.py b/astrbot/core/computer/booters/bwrap.py
index f4bbc8d05..5b0f6f947 100644
--- a/astrbot/core/computer/booters/bwrap.py
+++ b/astrbot/core/computer/booters/bwrap.py
@@ -262,7 +262,9 @@ class HostBackedFileSystemComponent(FileSystemComponent):
class BwrapBooter(ComputerBooter):
- def __init__(self, rw_binds: list[str] | None = None, ro_binds: list[str] | None = None):
+ def __init__(
+ self, rw_binds: list[str] | None = None, ro_binds: list[str] | None = None
+ ):
self._rw_binds = rw_binds or []
self._ro_binds = ro_binds or []
self._fs: HostBackedFileSystemComponent | None = None
diff --git a/astrbot/core/tools/send_message.py b/astrbot/core/tools/send_message.py
index dfaa28dcc..f62971ed5 100644
--- a/astrbot/core/tools/send_message.py
+++ b/astrbot/core/tools/send_message.py
@@ -8,6 +8,7 @@ from __future__ import annotations
import json
import os
import uuid
+from typing import Any, TypedDict
import anyio
from pydantic import Field
@@ -24,6 +25,16 @@ from astrbot.core.platform.message_session import MessageSession
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
+class MessageComponent(TypedDict, total=False):
+ """Type-safe message component structure."""
+
+ type: str
+ text: str
+ path: str
+ url: str
+ mention_user_id: str
+
+
@dataclass
class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
name: str = "send_message_to_user"
@@ -125,7 +136,8 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
if not isinstance(msg, dict):
return f"error: messages[{idx}] should be an object."
- msg_type = str(msg.get("type", "")).lower()
+ typed_msg: MessageComponent = msg
+ msg_type = str(typed_msg.get("type", "")).lower()
if not msg_type:
return f"error: messages[{idx}].type is required."
diff --git a/docs/en/dev/plugin-platform-adapter.md b/docs/en/dev/plugin-platform-adapter.md
index 8e65528e2..acbe77a41 100644
--- a/docs/en/dev/plugin-platform-adapter.md
+++ b/docs/en/dev/plugin-platform-adapter.md
@@ -23,7 +23,7 @@ class FakeClient():
self.token = token
self.username = username
# ...
-
+
async def start_polling(self):
while True:
await asyncio.sleep(5)
@@ -35,10 +35,10 @@ class FakeClient():
'message_id': 'asdhoashd',
'group_id': 'group123',
})
-
+
async def send_text(self, to: str, message: str):
print('发了消息:', to, message)
-
+
async def send_image(self, to: str, image_path: str):
print('发了消息:', to, image_path)
```
@@ -56,7 +56,7 @@ from astrbot.api.platform import register_platform_adapter
from astrbot import logger
from .client import FakeClient
from .fake_platform_event import FakePlatformEvent
-
+
# 注册平台适配器。第一个参数为平台名,第二个为描述。第三个为默认配置。
@register_platform_adapter("fake", "fake 适配器", default_config_tmpl={
"token": "your_token",
@@ -68,11 +68,11 @@ class FakePlatformAdapter(Platform):
super().__init__(event_queue)
self.config = platform_config # 上面的默认配置,用户填写后会传到这里
self.settings = platform_settings # platform_settings 平台设置。
-
+
async def send_by_session(self, session: MessageSesion, message_chain: MessageChain):
# 必须实现
await super().send_by_session(session, message_chain)
-
+
def meta(self) -> PlatformMetadata:
# 必须实现,直接像下面一样返回即可。
return PlatformMetadata(
@@ -87,8 +87,8 @@ class FakePlatformAdapter(Platform):
async def on_received(data):
logger.info(data)
abm = await self.convert_message(data=data) # 转换成 AstrBotMessage
- await self.handle_msg(abm)
-
+ await self.handle_msg(abm)
+
# 初始化 FakeClient
self.client = FakeClient(self.config['token'], self.config['username'])
self.client.on_message_received = on_received
@@ -107,9 +107,9 @@ class FakePlatformAdapter(Platform):
abm.self_id = data['bot_id']
abm.session_id = data['userid'] # 会话 ID。重要!
abm.message_id = data['message_id'] # 消息 ID。
-
+
return abm
-
+
async def handle_msg(self, message: AstrBotMessage):
# 处理消息
message_event = FakePlatformEvent(
@@ -136,12 +136,12 @@ class FakePlatformEvent(AstrMessageEvent):
def __init__(self, message_str: str, message_obj: AstrBotMessage, platform_meta: PlatformMetadata, session_id: str, client: FakeClient):
super().__init__(message_str, message_obj, platform_meta, session_id)
self.client = client
-
+
async def send(self, message: MessageChain):
for i in message.chain: # 遍历消息链
if isinstance(i, Plain): # 如果是文字类型的
await self.client.send_text(to=self.get_sender_id(), message=i.text)
- elif isinstance(i, Image): # 如果是图片类型的
+ elif isinstance(i, Image): # 如果是图片类型的
img_url = i.file
img_path = ""
# 下面的三个条件可以直接参考一下。
@@ -153,7 +153,7 @@ class FakePlatformEvent(AstrMessageEvent):
img_path = img_url
# 请善于 Debug!
-
+
await self.client.send_image(to=self.get_sender_id(), image_path=img_path)
await super().send(message) # 需要最后加上这一段,执行父类的 send 方法。
@@ -182,4 +182,4 @@ class MyPlugin(Star):

-有任何疑问欢迎加群询问~
\ No newline at end of file
+有任何疑问欢迎加群询问~
diff --git a/docs/zh/faq.md b/docs/zh/faq.md
index f9657884c..b78311862 100644
--- a/docs/zh/faq.md
+++ b/docs/zh/faq.md
@@ -103,8 +103,8 @@
1. 如果你两个**全都**是使用 Docker 部署,请尝试在终端运行:
```bash
-sudo docker network create newnet # 创建新网络
-sudo docker network connect newnet astrbot
+sudo docker network create newnet # 创建新网络
+sudo docker network connect newnet astrbot
sudo docker network connect newnet napcat # 让两个容器连到一起
sudo docker restart astrbot
sudo docker restart napcat # 重启容器
diff --git a/tests/fixtures/mocks/providers.py b/tests/fixtures/mocks/providers.py
new file mode 100644
index 000000000..11860b305
--- /dev/null
+++ b/tests/fixtures/mocks/providers.py
@@ -0,0 +1,187 @@
+"""Mock providers for testing LLM interactions."""
+
+from typing import Any, AsyncGenerator
+from dataclasses import dataclass, field
+
+from astrbot.core.provider.entities import (
+ ProviderRequest,
+ ProviderResponse,
+ ProviderMeta,
+ ProviderType,
+ LLMResponse,
+)
+
+
+class MockChatCompletionProvider:
+ """Mock provider that returns predefined responses for testing."""
+
+ def __init__(
+ self,
+ responses: list[LLMResponse] | None = None,
+ streaming_responses: list[list[str]] | None = None,
+ ):
+ self.responses = responses or []
+ self.streaming_responses = streaming_responses or []
+ self.response_index = 0
+ self.call_count = 0
+ self.last_request: ProviderRequest | None = None
+
+ self.meta = ProviderMeta(
+ id="mock_provider",
+ model="mock-model",
+ type="mock",
+ provider_type=ProviderType.CHAT_COMPLETION,
+ )
+
+ async def chat(
+ self, request: ProviderRequest
+ ) -> LLMResponse | AsyncGenerator[LLMResponse, None]:
+ """Return mock chat completion."""
+ self.call_count += 1
+ self.last_request = request
+
+ if self.responses:
+ response = self.responses[self.response_index % len(self.responses)]
+ self.response_index += 1
+ return response
+
+ # Default mock response
+ return LLMResponse(
+ role="assistant",
+ completion_text="This is a mock response from the test provider.",
+ )
+
+ async def stream_chat(self, request: ProviderRequest) -> AsyncGenerator[LLMResponse, None]:
+ """Return mock streaming chat completion."""
+ self.call_count += 1
+ self.last_request = request
+
+ text = "This is a mock streaming response."
+ if self.streaming_responses:
+ text = self.streaming_responses[self.response_index % len(self.streaming_responses)]
+ self.response_index += 1
+
+ # Stream word by word
+ for word in text.split():
+ yield LLMResponse(
+ role="assistant",
+ completion_text=word + " ",
+ )
+
+ def get_model(self) -> str:
+ return "mock-model"
+
+ def get_provider_type(self) -> ProviderType:
+ return ProviderType.CHAT_COMPLETION
+
+
+class MockToolCallProvider(MockChatCompletionProvider):
+ """Mock provider that returns tool calls."""
+
+ def __init__(
+ self,
+ tool_calls: list[dict] | None = None,
+ tool_response: str = "Tool executed successfully",
+ ):
+ super().__init__()
+ self.tool_calls = tool_calls or []
+ self.tool_response = tool_response
+ self.tool_call_count = 0
+
+ async def chat(self, request: ProviderRequest) -> LLMResponse:
+ """Return mock tool call."""
+ self.call_count += 1
+
+ if self.tool_calls:
+ tool_call = self.tool_calls[self.tool_call_count % len(self.tool_calls)]
+ self.tool_call_count += 1
+ return LLMResponse(
+ role="assistant",
+ completion_text="",
+ tool_calls=[tool_call],
+ )
+
+ # Default tool call
+ return LLMResponse(
+ role="assistant",
+ completion_text="",
+ tool_calls=[
+ {
+ "id": "call_1",
+ "type": "function",
+ "function": {
+ "name": "mock_tool",
+ "arguments": '{"input": "test"}',
+ },
+ }
+ ],
+ )
+
+
+class MockErrorProvider(MockChatCompletionProvider):
+ """Mock provider that raises errors for testing error handling."""
+
+ def __init__(self, error_message: str = "Mock provider error"):
+ super().__init__()
+ self.error_message = error_message
+
+ async def chat(self, request: ProviderRequest) -> LLMResponse:
+ """Raise mock error."""
+ raise RuntimeError(self.error_message)
+
+ async def stream_chat(self, request: ProviderRequest) -> AsyncGenerator[LLMResponse, None]:
+ """Raise mock error."""
+ raise RuntimeError(self.error_message)
+
+
+class MockMultiModalProvider(MockChatCompletionProvider):
+ """Mock provider for multimodal interactions."""
+
+ def __init__(
+ self,
+ has_image_support: bool = True,
+ has_audio_support: bool = False,
+ ):
+ super().__init__()
+ self.has_image_support = has_image_support
+ self.has_audio_support = has_audio_support
+
+ async def chat(self, request: ProviderRequest) -> LLMResponse:
+ """Return response based on request content."""
+ has_images = bool(request.image_urls)
+
+ if has_images and not self.has_image_support:
+ return LLMResponse(
+ role="assistant",
+ completion_text="Image support not enabled.",
+ )
+
+ return LLMResponse(
+ role="assistant",
+ completion_text=f"Received request with {len(request.image_urls)} images.",
+ )
+
+
+def create_mock_provider_request(
+ prompt: str = "Hello, test!",
+ session_id: str = "test-session-1",
+ image_urls: list[str] | None = None,
+) -> ProviderRequest:
+ """Create a mock provider request for testing."""
+ return ProviderRequest(
+ prompt=prompt,
+ session_id=session_id,
+ image_urls=image_urls or [],
+ )
+
+
+def create_mock_llm_response(
+ text: str = "Mock response",
+ tool_calls: list[dict] | None = None,
+) -> LLMResponse:
+ """Create a mock LLM response for testing."""
+ return LLMResponse(
+ role="assistant",
+ completion_text=text,
+ tool_calls=tool_calls,
+ )
diff --git a/tests/test_api_all.py b/tests/test_api_all.py
new file mode 100644
index 000000000..596e99b28
--- /dev/null
+++ b/tests/test_api_all.py
@@ -0,0 +1,90 @@
+"""Comprehensive tests for API modules."""
+
+import pytest
+from unittest.mock import MagicMock, AsyncMock, patch
+
+
+class TestApiAll:
+ """Tests for API re-exports."""
+
+ def test_api_all_imports(self):
+ """Test that API module imports work."""
+ from astrbot.api.all import AstrBotConfig
+ from astrbot.api.all import html_renderer
+ from astrbot.api.all import llm_tool
+
+ def test_api_message_event_imports(self):
+ """Test message event imports."""
+ from astrbot.api.all import MessageEventResult
+ from astrbot.api.all import MessageChain
+ from astrbot.api.all import CommandResult
+ from astrbot.api.all import EventResultType
+
+ def test_api_platform_imports(self):
+ """Test platform imports."""
+ from astrbot.api.all import AstrMessageEvent
+ from astrbot.api.all import Platform
+ from astrbot.api.all import AstrBotMessage
+ from astrbot.api.all import MessageMember
+ from astrbot.api.all import MessageType
+ from astrbot.api.all import PlatformMetadata
+
+ def test_api_star_register_imports(self):
+ """Test star register imports."""
+ from astrbot.api.all import command
+ from astrbot.api.all import command_group
+ from astrbot.api.all import event_message_type
+ from astrbot.api.all import regex
+ from astrbot.api.all import platform_adapter_type
+
+ def test_api_filter_imports(self):
+ """Test filter imports."""
+ from astrbot.api.all import EventMessageTypeFilter
+ from astrbot.api.all import EventMessageType
+ from astrbot.api.all import PlatformAdapterTypeFilter
+ from astrbot.api.all import PlatformAdapterType
+
+ def test_api_provider_imports(self):
+ """Test provider imports."""
+ from astrbot.api.all import Provider
+ from astrbot.api.all import ProviderMetaData
+ from astrbot.api.all import Personality
+
+ def test_api_star_imports(self):
+ """Test star imports."""
+ from astrbot.api.all import Context
+ from astrbot.api.all import Star
+
+
+class TestApiMessageComponents:
+ """Tests for API message components."""
+
+ def test_message_components_imports(self):
+ """Test that message components can be imported."""
+ import astrbot.api.message_components as message_components
+ # Module should be importable
+ assert message_components is not None
+
+ def test_message_component_types(self):
+ """Test message component types exist."""
+ # Just ensure imports work
+ from astrbot.api import message_components
+ assert message_components is not None
+
+
+class TestApiEvent:
+ """Tests for API event module."""
+
+ def test_event_filter_imports(self):
+ """Test event filter imports."""
+ from astrbot.api.event import filter as event_filter
+ assert hasattr(event_filter, '__all__')
+
+
+class TestApiUtil:
+ """Tests for API util module."""
+
+ def test_api_util_exists(self):
+ """Test API util module exists."""
+ from astrbot.api import util
+ assert hasattr(util, '__all__')
diff --git a/tests/test_cli_utils.py b/tests/test_cli_utils.py
new file mode 100644
index 000000000..e508064b5
--- /dev/null
+++ b/tests/test_cli_utils.py
@@ -0,0 +1,92 @@
+"""Comprehensive tests for CLI utilities."""
+
+import pytest
+from unittest.mock import MagicMock, patch, AsyncMock
+from pathlib import Path
+
+from astrbot.cli.utils.version_comparator import VersionComparator
+
+
+class TestVersionComparator:
+ """Tests for version comparison utilities."""
+
+ def test_compare_versions_equal(self):
+ """Test comparing equal versions."""
+ assert VersionComparator.compare_version("1.0.0", "1.0.0") == 0
+
+ def test_compare_versions_greater(self):
+ """Test comparing greater version."""
+ assert VersionComparator.compare_version("2.0.0", "1.0.0") > 0
+
+ def test_compare_versions_less(self):
+ """Test comparing lesser version."""
+ assert VersionComparator.compare_version("1.0.0", "2.0.0") < 0
+
+ def test_compare_versions_with_patch(self):
+ """Test comparing versions with patch numbers."""
+ assert VersionComparator.compare_version("1.0.1", "1.0.0") > 0
+ assert VersionComparator.compare_version("1.0.0", "1.0.1") < 0
+
+ def test_compare_versions_with_prerelease(self):
+ """Test comparing versions with prerelease."""
+ assert VersionComparator.compare_version("1.0.0-alpha", "1.0.0") < 0
+ assert VersionComparator.compare_version("1.0.0-beta", "1.0.0-alpha") > 0
+
+ def test_compare_versions_with_v_prefix(self):
+ """Test comparing versions with v prefix."""
+ assert VersionComparator.compare_version("v1.0.0", "v1.0.0") == 0
+ assert VersionComparator.compare_version("v2.0.0", "v1.0.0") > 0
+
+ def test_compare_versions_more_digits(self):
+ """Test comparing versions with more than 3 digits."""
+ assert VersionComparator.compare_version("1.0.0.1", "1.0.0.0") > 0
+ assert VersionComparator.compare_version("1.0.0.0", "1.0.0.1") < 0
+
+ def test_compare_versions_case_insensitive(self):
+ """Test version comparison is case insensitive."""
+ assert VersionComparator.compare_version("V1.0.0", "v1.0.0") == 0
+
+ def test_split_prerelease_alpha(self):
+ """Test splitting prerelease alpha."""
+ result = VersionComparator._split_prerelease("alpha")
+ assert result == ["alpha"]
+
+ def test_split_prerelease_with_number(self):
+ """Test splitting prerelease with number."""
+ result = VersionComparator._split_prerelease("alpha.1")
+ assert result == ["alpha", 1]
+
+ def test_split_prerelease_multiple(self):
+ """Test splitting multiple prerelease parts."""
+ result = VersionComparator._split_prerelease("alpha.1.beta.2")
+ assert result == ["alpha", 1, "beta", 2]
+
+ def test_split_prerelease_none(self):
+ """Test splitting None prerelease."""
+ result = VersionComparator._split_prerelease(None)
+ assert result is None
+
+
+class TestDashboardManager:
+ """Tests for DashboardManager."""
+
+ @pytest.mark.asyncio
+ async def test_ensure_installed_bundled(self):
+ """Test dashboard installation when bundled."""
+ from astrbot.cli.utils.dashboard import DashboardManager
+
+ with patch.object(DashboardManager, '_bundled_dist', Path(__file__).parent):
+ manager = DashboardManager()
+ # Should not raise and should return early
+ await manager.ensure_installed(Path("/tmp"))
+
+ @pytest.mark.asyncio
+ async def test_ensure_installed_no_bundled(self):
+ """Test dashboard installation when not bundled."""
+ from astrbot.cli.utils.dashboard import DashboardManager
+
+ with patch.object(DashboardManager, '_bundled_dist', Path("/nonexistent")):
+ with patch("os.environ.get", return_value="1"): # systemd mode
+ manager = DashboardManager()
+ await manager.ensure_installed(Path("/tmp"))
+ # Should skip in systemd mode
diff --git a/tests/test_docs.py b/tests/test_docs.py
index b654f474c..0bd2e0d10 100644
--- a/tests/test_docs.py
+++ b/tests/test_docs.py
@@ -53,6 +53,7 @@ class TestDocStructure:
Path("use/knowledge-base-old.md"),
Path("config/model-config.md"),
Path("use/astrbot-sandbox.md"),
+ Path("config"), # en has config dir, zh doesn't
}
unexpected_missing_en = missing_in_en - known_diffs
diff --git a/tests/test_tui.py b/tests/test_tui.py
new file mode 100644
index 000000000..0bfd0efb0
--- /dev/null
+++ b/tests/test_tui.py
@@ -0,0 +1,100 @@
+"""Comprehensive tests for TUI modules."""
+
+import pytest
+from unittest.mock import MagicMock, patch
+import sys
+
+
+class TestTuiColorPair:
+ """Tests for TUI color constants."""
+
+ def test_color_pair_enum(self):
+ """Test ColorPair enum values exist."""
+ from astrbot.tui.screen import ColorPair
+ assert ColorPair.WHITE is not None
+ assert ColorPair.CYAN is not None
+ assert ColorPair.GREEN is not None
+ assert ColorPair.YELLOW is not None
+ assert ColorPair.RED is not None
+ assert ColorPair.MAGENTA is not None
+ assert ColorPair.DIM is not None
+
+ def test_color_pair_header(self):
+ """Test header color pairs."""
+ from astrbot.tui.screen import ColorPair
+ assert ColorPair.HEADER_BG is not None
+ assert ColorPair.HEADER_FG is not None
+
+ def test_color_pair_messages(self):
+ """Test message color pairs."""
+ from astrbot.tui.screen import ColorPair
+ assert ColorPair.USER_MSG is not None
+ assert ColorPair.BOT_MSG is not None
+ assert ColorPair.SYSTEM_MSG is not None
+
+ def test_color_pair_ui(self):
+ """Test UI element color pairs."""
+ from astrbot.tui.screen import ColorPair
+ assert ColorPair.INPUT_BG is not None
+ assert ColorPair.STATUS_BG is not None
+ assert ColorPair.BORDER is not None
+
+
+class TestTuiBoxDrawing:
+ """Tests for TUI box drawing characters."""
+
+ def test_box_vertical(self):
+ """Test vertical box character."""
+ from astrbot.tui.screen import BOX_VERT
+ assert BOX_VERT == "│"
+
+ def test_box_horizontal(self):
+ """Test horizontal box character."""
+ from astrbot.tui.screen import BOX_HORIZ
+ assert BOX_HORIZ == "─"
+
+ def test_box_corners(self):
+ """Test box corner characters."""
+ from astrbot.tui.screen import BOX_TL, BOX_TR, BOX_BL, BOX_BR
+ assert BOX_TL == "┌"
+ assert BOX_TR == "┐"
+ assert BOX_BL == "└"
+ assert BOX_BR == "┘"
+
+ def test_box_junctions(self):
+ """Test box junction characters."""
+ from astrbot.tui.screen import BOX_LT, BOX_RT, BOX_BT, BOX_TT, BOX_CROSS
+ assert BOX_LT is not None
+ assert BOX_RT is not None
+ assert BOX_BT is not None
+ assert BOX_TT is not None
+ assert BOX_CROSS is not None
+
+
+class TestTuiInit:
+ """Tests for TUI __init__ module."""
+
+ def test_tui_init_imports(self):
+ """Test that TUI modules can be imported."""
+ # These should not raise
+ from astrbot.tui import Screen
+ from astrbot.tui import run_curses
+
+ def test_tui_exports(self):
+ """Test TUI module exports."""
+ from astrbot.tui import __all__
+ assert isinstance(__all__, list)
+
+
+class TestTuiApp:
+ """Tests for TUI application."""
+
+ def test_tui_app_exists(self):
+ """Test TUI app module exists."""
+ from astrbot.tui import tui_app
+ assert tui_app is not None
+
+ def test_tui_main_exists(self):
+ """Test TUI main module exists."""
+ from astrbot.tui import __main__
+ assert __main__ is not None
diff --git a/tests/unit/test_event_bus.py b/tests/unit/test_event_bus.py
index 1ecdbf1e3..fa826336d 100644
--- a/tests/unit/test_event_bus.py
+++ b/tests/unit/test_event_bus.py
@@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from astrbot.core.event_bus import EventBus
+from astrbot.core.pipeline.scheduler import PipelineScheduler
@pytest.fixture
@@ -276,13 +277,13 @@ class TestEventSubscription:
async def test_subscriber_registration(self, event_queue, mock_config_manager):
"""Test registering a subscriber (scheduler) to the event bus."""
# Create multiple schedulers as subscribers
- scheduler1 = MagicMock()
+ scheduler1 = MagicMock(spec=PipelineScheduler)
scheduler1.execute = AsyncMock()
- scheduler2 = MagicMock()
+ scheduler2 = MagicMock(spec=PipelineScheduler)
scheduler2.execute = AsyncMock()
# Create EventBus with multiple subscribers
- pipeline_mapping = {
+ pipeline_mapping: dict[str, PipelineScheduler] = {
"conf-id-1": scheduler1,
"conf-id-2": scheduler2,
}
@@ -310,7 +311,7 @@ class TestEventSubscription:
"name": "Test Config",
}
- scheduler1 = MagicMock()
+ scheduler1 = MagicMock(spec=PipelineScheduler)
scheduler1.execute = AsyncMock()
async def execute_scheduler1(event): # noqa: ARG001
@@ -319,7 +320,7 @@ class TestEventSubscription:
scheduler1.execute.side_effect = execute_scheduler1
- scheduler2 = MagicMock()
+ scheduler2 = MagicMock(spec=PipelineScheduler)
scheduler2.execute = AsyncMock()
async def execute_scheduler2(event): # noqa: ARG001
@@ -327,7 +328,7 @@ class TestEventSubscription:
scheduler2.execute.side_effect = execute_scheduler2
- pipeline_mapping = {
+ pipeline_mapping: dict[str, PipelineScheduler] = {
"conf-id-1": scheduler1,
"conf-id-2": scheduler2,
}