From 72acba5d274b7148d147f3ad7e60d88932b5aeb4 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Tue, 24 Mar 2026 13:37:06 +0800 Subject: [PATCH] refactor(tests): optimize unit test structure --- .github/workflows/ci.yml | 11 ++++++----- nanobot/agent/tools/shell.py | 10 ++++++---- pyproject.toml | 5 +---- tests/{ => agent}/test_consolidate_offset.py | 0 tests/{ => agent}/test_context_prompt_cache.py | 0 tests/{ => agent}/test_evaluator.py | 0 tests/{ => agent}/test_gemini_thought_signature.py | 0 tests/{ => agent}/test_heartbeat_service.py | 0 tests/{ => agent}/test_loop_consolidation_tokens.py | 0 tests/{ => agent}/test_loop_save_turn.py | 0 tests/{ => agent}/test_memory_consolidation_types.py | 0 tests/{ => agent}/test_onboard_logic.py | 0 tests/{ => agent}/test_session_manager_history.py | 0 tests/{ => agent}/test_skill_creator_scripts.py | 0 tests/{ => agent}/test_task_cancel.py | 0 tests/{ => channels}/test_base_channel.py | 0 tests/{ => channels}/test_channel_plugins.py | 0 tests/{ => channels}/test_dingtalk_channel.py | 10 ++++++++++ tests/{ => channels}/test_email_channel.py | 0 .../{ => channels}/test_feishu_markdown_rendering.py | 11 +++++++++++ tests/{ => channels}/test_feishu_post_content.py | 11 +++++++++++ tests/{ => channels}/test_feishu_reply.py | 10 ++++++++++ tests/{ => channels}/test_feishu_table_split.py | 11 +++++++++++ .../test_feishu_tool_hint_code_block.py | 10 ++++++++++ tests/{ => channels}/test_matrix_channel.py | 6 ++++++ tests/{ => channels}/test_qq_channel.py | 10 ++++++++++ tests/{ => channels}/test_slack_channel.py | 6 ++++++ tests/{ => channels}/test_telegram_channel.py | 6 ++++++ tests/{ => channels}/test_weixin_channel.py | 0 tests/{ => channels}/test_whatsapp_channel.py | 0 tests/{ => cli}/test_cli_input.py | 0 tests/{ => cli}/test_commands.py | 0 tests/{ => cli}/test_restart_command.py | 0 tests/{ => config}/test_config_migration.py | 0 tests/{ => config}/test_config_paths.py | 0 tests/{ => cron}/test_cron_service.py | 0 tests/{ => cron}/test_cron_tool_list.py | 0 tests/{ => providers}/test_azure_openai_provider.py | 0 tests/{ => providers}/test_custom_provider.py | 0 tests/{ => providers}/test_litellm_kwargs.py | 0 tests/{ => providers}/test_mistral_provider.py | 0 tests/{ => providers}/test_provider_retry.py | 0 tests/{ => providers}/test_providers_init.py | 0 tests/{ => security}/test_security_network.py | 0 tests/{ => tools}/test_exec_security.py | 0 tests/{ => tools}/test_filesystem_tools.py | 0 tests/{ => tools}/test_mcp_tool.py | 0 tests/{ => tools}/test_message_tool.py | 0 tests/{ => tools}/test_message_tool_suppress.py | 0 tests/{ => tools}/test_tool_validation.py | 0 tests/{ => tools}/test_web_fetch_security.py | 0 tests/{ => tools}/test_web_search_tool.py | 0 52 files changed, 104 insertions(+), 13 deletions(-) rename tests/{ => agent}/test_consolidate_offset.py (100%) rename tests/{ => agent}/test_context_prompt_cache.py (100%) rename tests/{ => agent}/test_evaluator.py (100%) rename tests/{ => agent}/test_gemini_thought_signature.py (100%) rename tests/{ => agent}/test_heartbeat_service.py (100%) rename tests/{ => agent}/test_loop_consolidation_tokens.py (100%) rename tests/{ => agent}/test_loop_save_turn.py (100%) rename tests/{ => agent}/test_memory_consolidation_types.py (100%) rename tests/{ => agent}/test_onboard_logic.py (100%) rename tests/{ => agent}/test_session_manager_history.py (100%) rename tests/{ => agent}/test_skill_creator_scripts.py (100%) rename tests/{ => agent}/test_task_cancel.py (100%) rename tests/{ => channels}/test_base_channel.py (100%) rename tests/{ => channels}/test_channel_plugins.py (100%) rename tests/{ => channels}/test_dingtalk_channel.py (95%) rename tests/{ => channels}/test_email_channel.py (100%) rename tests/{ => channels}/test_feishu_markdown_rendering.py (81%) rename tests/{ => channels}/test_feishu_post_content.py (82%) rename tests/{ => channels}/test_feishu_reply.py (97%) rename tests/{ => channels}/test_feishu_table_split.py (89%) rename tests/{ => channels}/test_feishu_tool_hint_code_block.py (93%) rename tests/{ => channels}/test_matrix_channel.py (99%) rename tests/{ => channels}/test_qq_channel.py (93%) rename tests/{ => channels}/test_slack_channel.py (95%) rename tests/{ => channels}/test_telegram_channel.py (99%) rename tests/{ => channels}/test_weixin_channel.py (100%) rename tests/{ => channels}/test_whatsapp_channel.py (100%) rename tests/{ => cli}/test_cli_input.py (100%) rename tests/{ => cli}/test_commands.py (100%) rename tests/{ => cli}/test_restart_command.py (100%) rename tests/{ => config}/test_config_migration.py (100%) rename tests/{ => config}/test_config_paths.py (100%) rename tests/{ => cron}/test_cron_service.py (100%) rename tests/{ => cron}/test_cron_tool_list.py (100%) rename tests/{ => providers}/test_azure_openai_provider.py (100%) rename tests/{ => providers}/test_custom_provider.py (100%) rename tests/{ => providers}/test_litellm_kwargs.py (100%) rename tests/{ => providers}/test_mistral_provider.py (100%) rename tests/{ => providers}/test_provider_retry.py (100%) rename tests/{ => providers}/test_providers_init.py (100%) rename tests/{ => security}/test_security_network.py (100%) rename tests/{ => tools}/test_exec_security.py (100%) rename tests/{ => tools}/test_filesystem_tools.py (100%) rename tests/{ => tools}/test_mcp_tool.py (100%) rename tests/{ => tools}/test_message_tool.py (100%) rename tests/{ => tools}/test_message_tool_suppress.py (100%) rename tests/{ => tools}/test_tool_validation.py (100%) rename tests/{ => tools}/test_web_fetch_security.py (100%) rename tests/{ => tools}/test_web_search_tool.py (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67a4d9b..e00362d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,13 +21,14 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Install uv + uses: astral-sh/setup-uv@v4 + - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libolm-dev build-essential - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install .[dev] + - name: Install all dependencies + run: uv sync --all-extras - name: Run tests - run: python -m pytest tests/ -v + run: uv run pytest tests/ diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index 5b46412..ed552b3 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -3,6 +3,7 @@ import asyncio import os import re +import sys from pathlib import Path from typing import Any @@ -113,10 +114,11 @@ class ExecTool(Tool): except asyncio.TimeoutError: pass finally: - try: - os.waitpid(process.pid, os.WNOHANG) - except (ProcessLookupError, ChildProcessError) as e: - logger.debug("Process already reaped or not found: {}", e) + if sys.platform != "win32": + try: + os.waitpid(process.pid, os.WNOHANG) + except (ProcessLookupError, ChildProcessError) as e: + logger.debug("Process already reaped or not found: {}", e) return f"Error: Command timed out after {effective_timeout} seconds" output_parts = [] diff --git a/pyproject.toml b/pyproject.toml index a941ab1..be367a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,11 +70,8 @@ langsmith = [ dev = [ "pytest>=9.0.0,<10.0.0", "pytest-asyncio>=1.3.0,<2.0.0", + "pytest-cov>=6.0.0,<7.0.0", "ruff>=0.1.0", - "matrix-nio[e2e]>=0.25.2", - "mistune>=3.0.0,<4.0.0", - "nh3>=0.2.17,<1.0.0", - "mypy>=1.19.1", ] [project.scripts] diff --git a/tests/test_consolidate_offset.py b/tests/agent/test_consolidate_offset.py similarity index 100% rename from tests/test_consolidate_offset.py rename to tests/agent/test_consolidate_offset.py diff --git a/tests/test_context_prompt_cache.py b/tests/agent/test_context_prompt_cache.py similarity index 100% rename from tests/test_context_prompt_cache.py rename to tests/agent/test_context_prompt_cache.py diff --git a/tests/test_evaluator.py b/tests/agent/test_evaluator.py similarity index 100% rename from tests/test_evaluator.py rename to tests/agent/test_evaluator.py diff --git a/tests/test_gemini_thought_signature.py b/tests/agent/test_gemini_thought_signature.py similarity index 100% rename from tests/test_gemini_thought_signature.py rename to tests/agent/test_gemini_thought_signature.py diff --git a/tests/test_heartbeat_service.py b/tests/agent/test_heartbeat_service.py similarity index 100% rename from tests/test_heartbeat_service.py rename to tests/agent/test_heartbeat_service.py diff --git a/tests/test_loop_consolidation_tokens.py b/tests/agent/test_loop_consolidation_tokens.py similarity index 100% rename from tests/test_loop_consolidation_tokens.py rename to tests/agent/test_loop_consolidation_tokens.py diff --git a/tests/test_loop_save_turn.py b/tests/agent/test_loop_save_turn.py similarity index 100% rename from tests/test_loop_save_turn.py rename to tests/agent/test_loop_save_turn.py diff --git a/tests/test_memory_consolidation_types.py b/tests/agent/test_memory_consolidation_types.py similarity index 100% rename from tests/test_memory_consolidation_types.py rename to tests/agent/test_memory_consolidation_types.py diff --git a/tests/test_onboard_logic.py b/tests/agent/test_onboard_logic.py similarity index 100% rename from tests/test_onboard_logic.py rename to tests/agent/test_onboard_logic.py diff --git a/tests/test_session_manager_history.py b/tests/agent/test_session_manager_history.py similarity index 100% rename from tests/test_session_manager_history.py rename to tests/agent/test_session_manager_history.py diff --git a/tests/test_skill_creator_scripts.py b/tests/agent/test_skill_creator_scripts.py similarity index 100% rename from tests/test_skill_creator_scripts.py rename to tests/agent/test_skill_creator_scripts.py diff --git a/tests/test_task_cancel.py b/tests/agent/test_task_cancel.py similarity index 100% rename from tests/test_task_cancel.py rename to tests/agent/test_task_cancel.py diff --git a/tests/test_base_channel.py b/tests/channels/test_base_channel.py similarity index 100% rename from tests/test_base_channel.py rename to tests/channels/test_base_channel.py diff --git a/tests/test_channel_plugins.py b/tests/channels/test_channel_plugins.py similarity index 100% rename from tests/test_channel_plugins.py rename to tests/channels/test_channel_plugins.py diff --git a/tests/test_dingtalk_channel.py b/tests/channels/test_dingtalk_channel.py similarity index 95% rename from tests/test_dingtalk_channel.py rename to tests/channels/test_dingtalk_channel.py index a0b866f..6894c86 100644 --- a/tests/test_dingtalk_channel.py +++ b/tests/channels/test_dingtalk_channel.py @@ -3,6 +3,16 @@ from types import SimpleNamespace import pytest +# Check optional dingtalk dependencies before running tests +try: + from nanobot.channels import dingtalk + DINGTALK_AVAILABLE = getattr(dingtalk, "DINGTALK_AVAILABLE", False) +except ImportError: + DINGTALK_AVAILABLE = False + +if not DINGTALK_AVAILABLE: + pytest.skip("DingTalk dependencies not installed (dingtalk-stream)", allow_module_level=True) + from nanobot.bus.queue import MessageBus import nanobot.channels.dingtalk as dingtalk_module from nanobot.channels.dingtalk import DingTalkChannel, NanobotDingTalkHandler diff --git a/tests/test_email_channel.py b/tests/channels/test_email_channel.py similarity index 100% rename from tests/test_email_channel.py rename to tests/channels/test_email_channel.py diff --git a/tests/test_feishu_markdown_rendering.py b/tests/channels/test_feishu_markdown_rendering.py similarity index 81% rename from tests/test_feishu_markdown_rendering.py rename to tests/channels/test_feishu_markdown_rendering.py index 6812a21..efcd207 100644 --- a/tests/test_feishu_markdown_rendering.py +++ b/tests/channels/test_feishu_markdown_rendering.py @@ -1,3 +1,14 @@ +# Check optional Feishu dependencies before running tests +try: + from nanobot.channels import feishu + FEISHU_AVAILABLE = getattr(feishu, "FEISHU_AVAILABLE", False) +except ImportError: + FEISHU_AVAILABLE = False + +if not FEISHU_AVAILABLE: + import pytest + pytest.skip("Feishu dependencies not installed (lark-oapi)", allow_module_level=True) + from nanobot.channels.feishu import FeishuChannel diff --git a/tests/test_feishu_post_content.py b/tests/channels/test_feishu_post_content.py similarity index 82% rename from tests/test_feishu_post_content.py rename to tests/channels/test_feishu_post_content.py index 7b1cb9d..a4c5bae 100644 --- a/tests/test_feishu_post_content.py +++ b/tests/channels/test_feishu_post_content.py @@ -1,3 +1,14 @@ +# Check optional Feishu dependencies before running tests +try: + from nanobot.channels import feishu + FEISHU_AVAILABLE = getattr(feishu, "FEISHU_AVAILABLE", False) +except ImportError: + FEISHU_AVAILABLE = False + +if not FEISHU_AVAILABLE: + import pytest + pytest.skip("Feishu dependencies not installed (lark-oapi)", allow_module_level=True) + from nanobot.channels.feishu import FeishuChannel, _extract_post_content diff --git a/tests/test_feishu_reply.py b/tests/channels/test_feishu_reply.py similarity index 97% rename from tests/test_feishu_reply.py rename to tests/channels/test_feishu_reply.py index b2072b3..0753653 100644 --- a/tests/test_feishu_reply.py +++ b/tests/channels/test_feishu_reply.py @@ -7,6 +7,16 @@ from unittest.mock import MagicMock, patch import pytest +# Check optional Feishu dependencies before running tests +try: + from nanobot.channels import feishu + FEISHU_AVAILABLE = getattr(feishu, "FEISHU_AVAILABLE", False) +except ImportError: + FEISHU_AVAILABLE = False + +if not FEISHU_AVAILABLE: + pytest.skip("Feishu dependencies not installed (lark-oapi)", allow_module_level=True) + from nanobot.bus.events import OutboundMessage from nanobot.bus.queue import MessageBus from nanobot.channels.feishu import FeishuChannel, FeishuConfig diff --git a/tests/test_feishu_table_split.py b/tests/channels/test_feishu_table_split.py similarity index 89% rename from tests/test_feishu_table_split.py rename to tests/channels/test_feishu_table_split.py index af8fa16..030b891 100644 --- a/tests/test_feishu_table_split.py +++ b/tests/channels/test_feishu_table_split.py @@ -6,6 +6,17 @@ list of card elements into groups so that each group contains at most one table, allowing nanobot to send multiple cards instead of failing. """ +# Check optional Feishu dependencies before running tests +try: + from nanobot.channels import feishu + FEISHU_AVAILABLE = getattr(feishu, "FEISHU_AVAILABLE", False) +except ImportError: + FEISHU_AVAILABLE = False + +if not FEISHU_AVAILABLE: + import pytest + pytest.skip("Feishu dependencies not installed (lark-oapi)", allow_module_level=True) + from nanobot.channels.feishu import FeishuChannel diff --git a/tests/test_feishu_tool_hint_code_block.py b/tests/channels/test_feishu_tool_hint_code_block.py similarity index 93% rename from tests/test_feishu_tool_hint_code_block.py rename to tests/channels/test_feishu_tool_hint_code_block.py index 2a1b812..a65f1d9 100644 --- a/tests/test_feishu_tool_hint_code_block.py +++ b/tests/channels/test_feishu_tool_hint_code_block.py @@ -6,6 +6,16 @@ from unittest.mock import MagicMock, patch import pytest from pytest import mark +# Check optional Feishu dependencies before running tests +try: + from nanobot.channels import feishu + FEISHU_AVAILABLE = getattr(feishu, "FEISHU_AVAILABLE", False) +except ImportError: + FEISHU_AVAILABLE = False + +if not FEISHU_AVAILABLE: + pytest.skip("Feishu dependencies not installed (lark-oapi)", allow_module_level=True) + from nanobot.bus.events import OutboundMessage from nanobot.channels.feishu import FeishuChannel diff --git a/tests/test_matrix_channel.py b/tests/channels/test_matrix_channel.py similarity index 99% rename from tests/test_matrix_channel.py rename to tests/channels/test_matrix_channel.py index 1f3b69c..dd5e97d 100644 --- a/tests/test_matrix_channel.py +++ b/tests/channels/test_matrix_channel.py @@ -4,6 +4,12 @@ from types import SimpleNamespace import pytest +# Check optional matrix dependencies before importing +try: + import nh3 # noqa: F401 +except ImportError: + pytest.skip("Matrix dependencies not installed (nh3)", allow_module_level=True) + import nanobot.channels.matrix as matrix_module from nanobot.bus.events import OutboundMessage from nanobot.bus.queue import MessageBus diff --git a/tests/test_qq_channel.py b/tests/channels/test_qq_channel.py similarity index 93% rename from tests/test_qq_channel.py rename to tests/channels/test_qq_channel.py index ab9afcb..729442a 100644 --- a/tests/test_qq_channel.py +++ b/tests/channels/test_qq_channel.py @@ -4,6 +4,16 @@ from types import SimpleNamespace import pytest +# Check optional QQ dependencies before running tests +try: + from nanobot.channels import qq + QQ_AVAILABLE = getattr(qq, "QQ_AVAILABLE", False) +except ImportError: + QQ_AVAILABLE = False + +if not QQ_AVAILABLE: + pytest.skip("QQ dependencies not installed (qq-botpy)", allow_module_level=True) + from nanobot.bus.events import OutboundMessage from nanobot.bus.queue import MessageBus from nanobot.channels.qq import QQChannel, QQConfig diff --git a/tests/test_slack_channel.py b/tests/channels/test_slack_channel.py similarity index 95% rename from tests/test_slack_channel.py rename to tests/channels/test_slack_channel.py index d243235..f7eec95 100644 --- a/tests/test_slack_channel.py +++ b/tests/channels/test_slack_channel.py @@ -2,6 +2,12 @@ from __future__ import annotations import pytest +# Check optional Slack dependencies before running tests +try: + import slack_sdk # noqa: F401 +except ImportError: + pytest.skip("Slack dependencies not installed (slack-sdk)", allow_module_level=True) + from nanobot.bus.events import OutboundMessage from nanobot.bus.queue import MessageBus from nanobot.channels.slack import SlackChannel diff --git a/tests/test_telegram_channel.py b/tests/channels/test_telegram_channel.py similarity index 99% rename from tests/test_telegram_channel.py rename to tests/channels/test_telegram_channel.py index 8b6ba97..353d5d0 100644 --- a/tests/test_telegram_channel.py +++ b/tests/channels/test_telegram_channel.py @@ -5,6 +5,12 @@ from unittest.mock import AsyncMock import pytest +# Check optional Telegram dependencies before running tests +try: + import telegram # noqa: F401 +except ImportError: + pytest.skip("Telegram dependencies not installed (python-telegram-bot)", allow_module_level=True) + from nanobot.bus.events import OutboundMessage from nanobot.bus.queue import MessageBus from nanobot.channels.telegram import TELEGRAM_REPLY_CONTEXT_MAX_LEN, TelegramChannel diff --git a/tests/test_weixin_channel.py b/tests/channels/test_weixin_channel.py similarity index 100% rename from tests/test_weixin_channel.py rename to tests/channels/test_weixin_channel.py diff --git a/tests/test_whatsapp_channel.py b/tests/channels/test_whatsapp_channel.py similarity index 100% rename from tests/test_whatsapp_channel.py rename to tests/channels/test_whatsapp_channel.py diff --git a/tests/test_cli_input.py b/tests/cli/test_cli_input.py similarity index 100% rename from tests/test_cli_input.py rename to tests/cli/test_cli_input.py diff --git a/tests/test_commands.py b/tests/cli/test_commands.py similarity index 100% rename from tests/test_commands.py rename to tests/cli/test_commands.py diff --git a/tests/test_restart_command.py b/tests/cli/test_restart_command.py similarity index 100% rename from tests/test_restart_command.py rename to tests/cli/test_restart_command.py diff --git a/tests/test_config_migration.py b/tests/config/test_config_migration.py similarity index 100% rename from tests/test_config_migration.py rename to tests/config/test_config_migration.py diff --git a/tests/test_config_paths.py b/tests/config/test_config_paths.py similarity index 100% rename from tests/test_config_paths.py rename to tests/config/test_config_paths.py diff --git a/tests/test_cron_service.py b/tests/cron/test_cron_service.py similarity index 100% rename from tests/test_cron_service.py rename to tests/cron/test_cron_service.py diff --git a/tests/test_cron_tool_list.py b/tests/cron/test_cron_tool_list.py similarity index 100% rename from tests/test_cron_tool_list.py rename to tests/cron/test_cron_tool_list.py diff --git a/tests/test_azure_openai_provider.py b/tests/providers/test_azure_openai_provider.py similarity index 100% rename from tests/test_azure_openai_provider.py rename to tests/providers/test_azure_openai_provider.py diff --git a/tests/test_custom_provider.py b/tests/providers/test_custom_provider.py similarity index 100% rename from tests/test_custom_provider.py rename to tests/providers/test_custom_provider.py diff --git a/tests/test_litellm_kwargs.py b/tests/providers/test_litellm_kwargs.py similarity index 100% rename from tests/test_litellm_kwargs.py rename to tests/providers/test_litellm_kwargs.py diff --git a/tests/test_mistral_provider.py b/tests/providers/test_mistral_provider.py similarity index 100% rename from tests/test_mistral_provider.py rename to tests/providers/test_mistral_provider.py diff --git a/tests/test_provider_retry.py b/tests/providers/test_provider_retry.py similarity index 100% rename from tests/test_provider_retry.py rename to tests/providers/test_provider_retry.py diff --git a/tests/test_providers_init.py b/tests/providers/test_providers_init.py similarity index 100% rename from tests/test_providers_init.py rename to tests/providers/test_providers_init.py diff --git a/tests/test_security_network.py b/tests/security/test_security_network.py similarity index 100% rename from tests/test_security_network.py rename to tests/security/test_security_network.py diff --git a/tests/test_exec_security.py b/tests/tools/test_exec_security.py similarity index 100% rename from tests/test_exec_security.py rename to tests/tools/test_exec_security.py diff --git a/tests/test_filesystem_tools.py b/tests/tools/test_filesystem_tools.py similarity index 100% rename from tests/test_filesystem_tools.py rename to tests/tools/test_filesystem_tools.py diff --git a/tests/test_mcp_tool.py b/tests/tools/test_mcp_tool.py similarity index 100% rename from tests/test_mcp_tool.py rename to tests/tools/test_mcp_tool.py diff --git a/tests/test_message_tool.py b/tests/tools/test_message_tool.py similarity index 100% rename from tests/test_message_tool.py rename to tests/tools/test_message_tool.py diff --git a/tests/test_message_tool_suppress.py b/tests/tools/test_message_tool_suppress.py similarity index 100% rename from tests/test_message_tool_suppress.py rename to tests/tools/test_message_tool_suppress.py diff --git a/tests/test_tool_validation.py b/tests/tools/test_tool_validation.py similarity index 100% rename from tests/test_tool_validation.py rename to tests/tools/test_tool_validation.py diff --git a/tests/test_web_fetch_security.py b/tests/tools/test_web_fetch_security.py similarity index 100% rename from tests/test_web_fetch_security.py rename to tests/tools/test_web_fetch_security.py diff --git a/tests/test_web_search_tool.py b/tests/tools/test_web_search_tool.py similarity index 100% rename from tests/test_web_search_tool.py rename to tests/tools/test_web_search_tool.py