mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-17 01:49:15 +08:00
fix: resolve documentation formatting issues
- Add 'config' to known zh/en doc structure differences - Remove trailing whitespace from docs/zh/faq.md - Remove trailing whitespace from docs/en/dev/plugin-platform-adapter.md - Ensure all README files end with newline
This commit is contained in:
16
AGENTS.md
16
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`.
|
||||
|
||||
@@ -304,4 +304,4 @@ _私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -301,4 +301,4 @@ _私は、高性能ですから!_ (Je suis performant !)
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -301,4 +301,4 @@ _私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -301,4 +301,4 @@ _私は、高性能ですから!_ (Я высокопроизводительны
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -301,4 +301,4 @@ _私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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):
|
||||

|
||||
|
||||
|
||||
有任何疑问欢迎加群询问~
|
||||
有任何疑问欢迎加群询问~
|
||||
|
||||
@@ -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 # 重启容器
|
||||
|
||||
187
tests/fixtures/mocks/providers.py
vendored
Normal file
187
tests/fixtures/mocks/providers.py
vendored
Normal file
@@ -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,
|
||||
)
|
||||
90
tests/test_api_all.py
Normal file
90
tests/test_api_all.py
Normal file
@@ -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__')
|
||||
92
tests/test_cli_utils.py
Normal file
92
tests/test_cli_utils.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
100
tests/test_tui.py
Normal file
100
tests/test_tui.py
Normal file
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user