Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
87b5bd3
feat(tests): enhance test suite with comprehensive fixtures and integ…
whatevertogo Feb 20, 2026
ce5b7d7
feat(tests): 更新测试命令以简化配置并添加集成测试
whatevertogo Feb 20, 2026
eeb2ab2
feat(tests): 更新测试命令以移除 pytest.ini 配置并简化运行指令
whatevertogo Feb 20, 2026
fdfbc19
Merge branch 'AstrBotDevs:master' into feat/add-comprehensive-tests
whatevertogo Feb 20, 2026
4bbd82a
fix(tests): address review feedback on test isolation
whatevertogo Feb 20, 2026
14bf651
refactor(tests): improve async fixtures and address review feedback
whatevertogo Feb 20, 2026
bb63839
fix(tests): 修复测试需求清单中的格式问题
whatevertogo Feb 20, 2026
e867112
fix(tests): 在测试需求清单中补充 event_loop fixture 的作用域说明
whatevertogo Feb 20, 2026
d01f29d
test(tests): Clean up the warning filter and add test cases for updat…
whatevertogo Feb 21, 2026
6cd4389
delete(tests): 移除集成测试的烟雾测试文件
whatevertogo Feb 21, 2026
8ff63e6
docs(tests): update TEST_REQUIREMENTS.md with coverage progress
whatevertogo Feb 21, 2026
16b91fe
test(core): add unit tests for core modules
whatevertogo Feb 21, 2026
42ae660
docs(tests): update progress tracking for core modules
whatevertogo Feb 21, 2026
cd779c2
fix(tests): fix linting issues in core module tests
whatevertogo Feb 21, 2026
a2b057b
docs(tests): correct core module count in progress tracking
whatevertogo Feb 21, 2026
5157e84
chore: exclude local docs from PR
whatevertogo Feb 21, 2026
9943654
chore: remove local docs from PR branch
whatevertogo Feb 21, 2026
348416a
Merge branch 'feat/add-comprehensive-tests' of github.com:whatevertog…
whatevertogo Feb 21, 2026
379af35
fix: resolve pipeline import cycles and add unit coverage
whatevertogo Feb 21, 2026
cb4be2d
fix: use utcnow for JWT expiration time
whatevertogo Feb 21, 2026
09cd78a
refactor: update pytest fixtures to use module scope and improve asyn…
whatevertogo Feb 21, 2026
abfb2ba
fix: enhance proxy handling in initialization tests
whatevertogo Feb 21, 2026
0fb045c
$(cat <<'EOF'
whatevertogo Feb 21, 2026
10f3d11
style: fix formatting and remove unnecessary newline in star module
whatevertogo Feb 22, 2026
455e8b8
test: add comprehensive tests for command registration and alias hand…
whatevertogo Feb 22, 2026
0bc6606
test: add unit test for handling empty command list in Telegram comma…
whatevertogo Feb 22, 2026
f85d1c3
fix: update command name validation for Discord and Telegram platforms
whatevertogo Feb 22, 2026
1aa8d34
test: enhance tests for sender name handling and command registration…
whatevertogo Feb 22, 2026
714166a
Merge branch 'master' into feat/add-comprehensive-tests
whatevertogo Feb 22, 2026
ec3fdc7
test: add comprehensive tests and improve test structure for adapters
whatevertogo Feb 22, 2026
513e0e3
fix: update coverage path in test workflow to target the correct module
whatevertogo Feb 22, 2026
cca96b9
feat: expand exports in star module to include Context, PluginManager…
whatevertogo Feb 23, 2026
5ac4f58
test: add comprehensive mock utilities and refactor tests for aiocqht…
whatevertogo Feb 23, 2026
484646e
fix: reorder import statements for consistency in star module
whatevertogo Feb 23, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/coverage_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
mkdir -p data/temp
export TESTING=true
export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }}
pytest --cov=. -v -o log_cli=true -o log_level=DEBUG
pytest --cov=astrbot -v -o log_cli=true -o log_level=DEBUG
- name: Upload results to Codecov
uses: codecov/codecov-action@v5
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Runs on `http://localhost:3000` by default.
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.

## Testing

When you modify functionality, add or update a corresponding test and run it locally (e.g. `uv run pytest tests/path/to/test_xxx.py --cov=astrbot.xxx`).
Use `--cov-report term-missing` or similar to generate coverage information.


## PR instructions

1. Title format: use conventional commit messages
Expand Down
31 changes: 30 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: worktree worktree-add worktree-rm
.PHONY: worktree worktree-add worktree-rm test test-unit test-integration test-cov test-quick

WORKTREE_DIR ?= ../astrbot_worktree
BRANCH ?= $(word 2,$(MAKECMDGOALS))
Expand Down Expand Up @@ -30,3 +30,32 @@ endif
# Swallow extra args (branch/base) so make doesn't treat them as targets
%:
@true

# ============================================================
# 测试命令
# ============================================================

# 运行所有测试
test:
uv run pytest tests/ -v

# 运行单元测试
test-unit:
uv run pytest tests/ -v -m "unit and not integration"

# 运行集成测试
test-integration:
uv run pytest tests/integration/ -v -m integration

# 运行测试并生成覆盖率报告
test-cov:
uv run pytest tests/ --cov=astrbot --cov-report=term-missing --cov-report=html -v

# 快速测试(跳过慢速测试和集成测试)
test-quick:
uv run pytest tests/ -v -m "not slow and not integration" --tb=short

# 运行特定测试文件
test-file:
@echo "Usage: uv run pytest tests/path/to/test_file.py -v"
@echo "Example: uv run pytest tests/test_main.py -v"
4 changes: 2 additions & 2 deletions astrbot/api/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
CommandResult,
EventResultType,
)
from astrbot.core.platform import AstrMessageEvent

# star register
from astrbot.core.star.register import (
Expand All @@ -31,8 +30,9 @@
from astrbot.core.star.register import (
register_star as register, # 注册插件(Star)
)
from astrbot.core.star import Context, Star
from astrbot.core.star.base import Star
from astrbot.core.star.config import *
from astrbot.core.star.context import Context


# provider
Expand Down
4 changes: 3 additions & 1 deletion astrbot/api/star/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from astrbot.core.star import Context, Star, StarTools
from astrbot.core.star.base import Star
from astrbot.core.star.config import *
from astrbot.core.star.context import Context
from astrbot.core.star.register import (
register_star as register, # 注册插件(Star)
)
from astrbot.core.star.star_tools import StarTools

__all__ = ["Context", "Star", "StarTools", "register"]
30 changes: 18 additions & 12 deletions astrbot/core/astr_main_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,17 +783,23 @@ async def _handle_webchat(
if not user_prompt or not chatui_session_id or not session or session.display_name:
return

llm_resp = await prov.text_chat(
system_prompt=(
"You are a conversation title generator. "
"Generate a concise title in the same language as the user’s input, "
"no more than 10 words, capturing only the core topic."
"If the input is a greeting, small talk, or has no clear topic, "
"(e.g., “hi”, “hello”, “haha”), return <None>. "
"Output only the title itself or <None>, with no explanations."
),
prompt=f"Generate a concise title for the following user query:\n{user_prompt}",
)
try:
llm_resp = await prov.text_chat(
system_prompt=(
"You are a conversation title generator. "
"Generate a concise title in the same language as the user’s input, "
"no more than 10 words, capturing only the core topic."
"If the input is a greeting, small talk, or has no clear topic, "
"(e.g., “hi”, “hello”, “haha”), return <None>. "
"Output only the title itself or <None>, with no explanations."
),
prompt=f"Generate a concise title for the following user query:\n{user_prompt}",
)
except Exception:
logger.exception(
"Failed to generate webchat title for session %s", chatui_session_id
)
return
if llm_resp and llm_resp.completion_text:
title = llm_resp.completion_text.strip()
if not title or "<None>" in title:
Expand Down Expand Up @@ -836,7 +842,7 @@ def _apply_sandbox_tools(
req.func_tool.add_tool(PYTHON_TOOL)
req.func_tool.add_tool(FILE_UPLOAD_TOOL)
req.func_tool.add_tool(FILE_DOWNLOAD_TOOL)
req.system_prompt += f"\n{SANDBOX_MODE_PROMPT}\n"
req.system_prompt = f"{req.system_prompt or ''}\n{SANDBOX_MODE_PROMPT}\n"


def _proactive_cron_job_tools(req: ProviderRequest) -> None:
Expand Down
2 changes: 1 addition & 1 deletion astrbot/core/core_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
from astrbot.core.platform.manager import PlatformManager
from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryManager
from astrbot.core.provider.manager import ProviderManager
from astrbot.core.star import PluginManager
from astrbot.core.star.context import Context
from astrbot.core.star.star_handler import EventType, star_handlers_registry, star_map
from astrbot.core.star.star_manager import PluginManager
from astrbot.core.subagent_orchestrator import SubAgentOrchestrator
from astrbot.core.umop_config_router import UmopConfigRouter
from astrbot.core.updator import AstrBotUpdator
Expand Down
21 changes: 20 additions & 1 deletion astrbot/core/cron/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
from .manager import CronJobManager
"""Cron package exports.

Keep `CronJobManager` import-compatible while avoiding hard import failure when
`apscheduler` is partially mocked in test environments.
"""

try:
from .manager import CronJobManager
except ModuleNotFoundError as exc:
if not (exc.name and exc.name.startswith("apscheduler")):
raise

_IMPORT_ERROR = exc

class CronJobManager:
def __init__(self, *args, **kwargs) -> None:
raise ModuleNotFoundError(
"CronJobManager requires a complete `apscheduler` installation."
) from _IMPORT_ERROR


__all__ = ["CronJobManager"]
20 changes: 17 additions & 3 deletions astrbot/core/event_bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,25 @@ async def dispatch(self) -> None:
while True:
event: AstrMessageEvent = await self.event_queue.get()
conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin)
self._print_event(event, conf_info["name"])
scheduler = self.pipeline_scheduler_mapping.get(conf_info["id"])
if not isinstance(conf_info, dict):
logger.error(
f"Invalid conf_info for origin {event.unified_msg_origin}: {conf_info}"
)
continue

conf_id = conf_info.get("id")
if not conf_id:
logger.error(
f"Incomplete conf_info for origin {event.unified_msg_origin}: {conf_info}"
)
continue

conf_name = conf_info.get("name") or str(conf_id)
self._print_event(event, conf_name)
scheduler = self.pipeline_scheduler_mapping.get(conf_id)
if not scheduler:
logger.error(
f"PipelineScheduler not found for id: {conf_info['id']}, event ignored."
f"PipelineScheduler not found for id: {conf_id}, event ignored."
)
continue
asyncio.create_task(scheduler.execute(event))
Expand Down
89 changes: 67 additions & 22 deletions astrbot/core/pipeline/__init__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,60 @@
"""Pipeline package exports.

This module intentionally avoids eager imports of all pipeline stage modules to
prevent import-time cycles. Stage classes remain available via lazy attribute
resolution for backward compatibility.
"""

from __future__ import annotations

from importlib import import_module
from typing import Any

from astrbot.core.message.message_event_result import (
EventResultType,
MessageEventResult,
)

from .content_safety_check.stage import ContentSafetyCheckStage
from .preprocess_stage.stage import PreProcessStage
from .process_stage.stage import ProcessStage
from .rate_limit_check.stage import RateLimitStage
from .respond.stage import RespondStage
from .result_decorate.stage import ResultDecorateStage
from .session_status_check.stage import SessionStatusCheckStage
from .waking_check.stage import WakingCheckStage
from .whitelist_check.stage import WhitelistCheckStage

# 管道阶段顺序
STAGES_ORDER = [
"WakingCheckStage", # 检查是否需要唤醒
"WhitelistCheckStage", # 检查是否在群聊/私聊白名单
"SessionStatusCheckStage", # 检查会话是否整体启用
"RateLimitStage", # 检查会话是否超过频率限制
"ContentSafetyCheckStage", # 检查内容安全
"PreProcessStage", # 预处理
"ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用
"ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等
"RespondStage", # 发送消息
]
from .stage_order import STAGES_ORDER

_LAZY_EXPORTS = {
"ContentSafetyCheckStage": (
"astrbot.core.pipeline.content_safety_check.stage",
"ContentSafetyCheckStage",
),
"PreProcessStage": (
"astrbot.core.pipeline.preprocess_stage.stage",
"PreProcessStage",
),
"ProcessStage": (
"astrbot.core.pipeline.process_stage.stage",
"ProcessStage",
),
"RateLimitStage": (
"astrbot.core.pipeline.rate_limit_check.stage",
"RateLimitStage",
),
"RespondStage": (
"astrbot.core.pipeline.respond.stage",
"RespondStage",
),
"ResultDecorateStage": (
"astrbot.core.pipeline.result_decorate.stage",
"ResultDecorateStage",
),
"SessionStatusCheckStage": (
"astrbot.core.pipeline.session_status_check.stage",
"SessionStatusCheckStage",
),
"WakingCheckStage": (
"astrbot.core.pipeline.waking_check.stage",
"WakingCheckStage",
),
"WhitelistCheckStage": (
"astrbot.core.pipeline.whitelist_check.stage",
"WhitelistCheckStage",
),
}

__all__ = [
"ContentSafetyCheckStage",
Expand All @@ -36,6 +66,21 @@
"RespondStage",
"ResultDecorateStage",
"SessionStatusCheckStage",
"STAGES_ORDER",
"WakingCheckStage",
"WhitelistCheckStage",
]


def __getattr__(name: str) -> Any:
if name not in _LAZY_EXPORTS:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
module_path, attr_name = _LAZY_EXPORTS[name]
module = import_module(module_path)
value = getattr(module, attr_name)
globals()[name] = value
return value


def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__))
52 changes: 52 additions & 0 deletions astrbot/core/pipeline/bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Pipeline bootstrap utilities."""

from importlib import import_module

from .stage import registered_stages

_BUILTIN_STAGE_MODULES = (
"astrbot.core.pipeline.waking_check.stage",
"astrbot.core.pipeline.whitelist_check.stage",
"astrbot.core.pipeline.session_status_check.stage",
"astrbot.core.pipeline.rate_limit_check.stage",
"astrbot.core.pipeline.content_safety_check.stage",
"astrbot.core.pipeline.preprocess_stage.stage",
"astrbot.core.pipeline.process_stage.stage",
"astrbot.core.pipeline.result_decorate.stage",
"astrbot.core.pipeline.respond.stage",
)

_EXPECTED_STAGE_NAMES = {
"WakingCheckStage",
"WhitelistCheckStage",
"SessionStatusCheckStage",
"RateLimitStage",
"ContentSafetyCheckStage",
"PreProcessStage",
"ProcessStage",
"ResultDecorateStage",
"RespondStage",
}

_builtin_stages_registered = False


def ensure_builtin_stages_registered() -> None:
"""Ensure built-in pipeline stages are imported and registered."""
global _builtin_stages_registered

if _builtin_stages_registered:
return

stage_names = {stage_cls.__name__ for stage_cls in registered_stages}
if _EXPECTED_STAGE_NAMES.issubset(stage_names):
_builtin_stages_registered = True
return

for module_path in _BUILTIN_STAGE_MODULES:
import_module(module_path)

_builtin_stages_registered = True


__all__ = ["ensure_builtin_stages_registered"]
6 changes: 4 additions & 2 deletions astrbot/core/pipeline/context.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from astrbot.core.config import AstrBotConfig
from astrbot.core.star import PluginManager

from .context_utils import call_event_hook, call_handler

Expand All @@ -11,7 +13,7 @@ class PipelineContext:
"""上下文对象,包含管道执行所需的上下文信息"""

astrbot_config: AstrBotConfig # AstrBot 配置对象
plugin_manager: PluginManager # 插件管理器对象
plugin_manager: Any # 插件管理器对象
astrbot_config_id: str
call_handler = call_handler
call_event_hook = call_event_hook
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
MessageEventResult,
ResultContentType,
)
from astrbot.core.pipeline.stage import Stage
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.provider.entities import (
LLMResponse,
Expand All @@ -30,7 +31,6 @@

from .....astr_agent_run_util import run_agent, run_live_agent
from ....context import PipelineContext, call_event_hook
from ...stage import Stage


class InternalAgentSubStage(Stage):
Expand Down
Loading