mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-17 01:49:15 +08:00
# Conflicts: # AGENTS.md # astrbot/__init__.py # astrbot/cli/utils/__init__.py # astrbot/core/astr_agent_tool_exec.py # astrbot/core/computer/booters/local.py # astrbot/core/cron/manager.py # astrbot/core/knowledge_base/kb_mgr.py # astrbot/core/knowledge_base/retrieval/manager.py # astrbot/core/platform/sources/discord/discord_platform_adapter.py # astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py # astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py # astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py # astrbot/core/platform/sources/wecom/wecom_adapter.py # astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py # astrbot/core/provider/func_tool_manager.py # astrbot/core/provider/sources/mimo_api_common.py # astrbot/core/star/star_manager.py # astrbot/core/tools/computer_tools/fs.py # astrbot/core/tools/computer_tools/python.py # astrbot/core/tools/computer_tools/shell.py # astrbot/core/tools/message_tools.py # astrbot/core/tools/web_search_tools.py # astrbot/core/utils/io.py # astrbot/core/utils/tencent_record_helper.py # dashboard/package.json # dashboard/src/assets/mdi-subset/materialdesignicons-subset.css # dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff # dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 # dashboard/src/components/ConfirmDialog.vue # dashboard/src/components/chat/Chat.vue # dashboard/src/components/chat/ChatInput.vue # dashboard/src/components/chat/ChatMessageList.vue # dashboard/src/components/chat/ConfigSelector.vue # dashboard/src/components/chat/MessageList.vue # dashboard/src/components/chat/ProjectDialog.vue # dashboard/src/components/chat/ProjectList.vue # dashboard/src/components/chat/ProjectView.vue # dashboard/src/components/chat/ProviderModelMenu.vue # dashboard/src/components/chat/RegenerateMenu.vue # dashboard/src/components/chat/StandaloneChat.vue # dashboard/src/components/chat/ThreadPanel.vue # dashboard/src/components/chat/ThreadedMarkdownMessagePart.vue # dashboard/src/components/chat/message_list_comps/MarkdownMessagePart.vue # dashboard/src/components/chat/message_list_comps/ReasoningTimeline.vue # dashboard/src/components/chat/message_list_comps/RefsSidebar.vue # dashboard/src/components/config/UnsavedChangesConfirmDialog.vue # dashboard/src/components/extension/MarketPluginCard.vue # dashboard/src/components/extension/McpServersSection.vue # dashboard/src/components/extension/SkillsSection.vue # dashboard/src/components/extension/componentPanel/components/DetailsDialog.vue # dashboard/src/components/extension/componentPanel/components/RenameDialog.vue # dashboard/src/components/folder/BaseCreateFolderDialog.vue # dashboard/src/components/folder/BaseFolderItemSelector.vue # dashboard/src/components/folder/BaseMoveToFolderDialog.vue # dashboard/src/components/platform/AddNewPlatform.vue # dashboard/src/components/provider/AddNewProvider.vue # dashboard/src/components/provider/ProviderModelsPanel.vue # dashboard/src/components/shared/BackupDialog.vue # dashboard/src/components/shared/ChangelogDialog.vue # dashboard/src/components/shared/ExtensionCard.vue # dashboard/src/components/shared/FileConfigItem.vue # dashboard/src/components/shared/KnowledgeBaseSelector.vue # dashboard/src/components/shared/ListConfigItem.vue # dashboard/src/components/shared/ObjectEditor.vue # dashboard/src/components/shared/PersonaForm.vue # dashboard/src/components/shared/PluginSetSelector.vue # dashboard/src/components/shared/ProviderSelector.vue # dashboard/src/components/shared/ReadmeDialog.vue # dashboard/src/components/shared/SidebarCustomizer.vue # dashboard/src/components/shared/T2ITemplateEditor.vue # dashboard/src/components/shared/UninstallConfirmDialog.vue # dashboard/src/components/shared/WaitingForRestart.vue # dashboard/src/composables/useMessages.ts # dashboard/src/composables/useProjects.ts # dashboard/src/composables/useProviderModelConfigDialog.ts # dashboard/src/composables/useProviderSources.ts # dashboard/src/i18n/locales/ru-RU/features/config-metadata.json # dashboard/src/i18n/locales/ru-RU/features/extension.json # dashboard/src/i18n/locales/ru-RU/features/provider.json # dashboard/src/layouts/full/vertical-header/VerticalHeader.vue # dashboard/src/scss/_override.scss # dashboard/src/stores/common.js # dashboard/src/stores/customizer.ts # dashboard/src/views/ConfigPage.vue # dashboard/src/views/ConversationPage.vue # dashboard/src/views/PlatformPage.vue # dashboard/src/views/SessionManagementPage.vue # dashboard/src/views/WelcomePage.vue # dashboard/src/views/alkaid/KnowledgeBase.vue # dashboard/src/views/alkaid/LongTermMemory.vue # dashboard/src/views/authentication/auth/LoginPage.vue # dashboard/src/views/extension/InstalledPluginsTab.vue # dashboard/src/views/extension/useExtensionPage.js # dashboard/src/views/knowledge-base/DocumentDetail.vue # dashboard/src/views/knowledge-base/KBList.vue # dashboard/src/views/knowledge-base/components/DocumentsTab.vue # dashboard/src/views/knowledge-base/components/SettingsTab.vue # dashboard/src/views/knowledge-base/components/TavilyKeyDialog.vue # dashboard/src/views/persona/FolderTree.vue # dashboard/src/views/persona/MoveToFolderDialog.vue # dashboard/src/views/persona/PersonaManager.vue # requirements.txt # tests/test_local_shell_component.py # tests/unit/test_func_tool_manager.py # tests/unit/test_web_search_tools.py
117 lines
3.0 KiB
Python
117 lines
3.0 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import subprocess
|
||
|
||
import pytest
|
||
|
||
from astrbot.core.computer.booters import local as local_booter
|
||
from astrbot.core.computer.booters.local import (
|
||
LocalShellComponent,
|
||
_decode_bytes_with_fallback,
|
||
)
|
||
|
||
|
||
def test_local_shell_component_decodes_utf8_output() -> None:
|
||
result = _decode_bytes_with_fallback("技能内容".encode())
|
||
assert result == "技能内容"
|
||
|
||
|
||
def test_local_shell_component_prefers_utf8_before_windows_locale(
|
||
monkeypatch,
|
||
) -> None:
|
||
monkeypatch.setattr(local_booter.os, "name", "nt", raising=False)
|
||
monkeypatch.setattr(
|
||
local_booter.locale,
|
||
"getpreferredencoding",
|
||
lambda _do_setlocale=False: "cp936",
|
||
)
|
||
|
||
result = _decode_bytes_with_fallback(
|
||
"技能内容".encode(),
|
||
preferred_encoding="utf-8",
|
||
)
|
||
assert result == "技能内容"
|
||
|
||
|
||
def test_local_shell_component_falls_back_to_gbk_on_windows(monkeypatch) -> None:
|
||
monkeypatch.setattr(local_booter.os, "name", "nt", raising=False)
|
||
monkeypatch.setattr(
|
||
local_booter.locale,
|
||
"getpreferredencoding",
|
||
lambda _do_setlocale=False: "cp1252",
|
||
)
|
||
|
||
result = _decode_bytes_with_fallback("微博热搜".encode("gbk"))
|
||
assert result == "微博热搜"
|
||
|
||
|
||
def test_local_shell_component_falls_back_to_utf8_replace(monkeypatch) -> None:
|
||
monkeypatch.setattr(local_booter.os, "name", "posix", raising=False)
|
||
monkeypatch.setattr(
|
||
local_booter.locale,
|
||
"getpreferredencoding",
|
||
lambda _do_setlocale=False: "utf-8",
|
||
)
|
||
|
||
result = _decode_bytes_with_fallback(b"\xffabc")
|
||
assert result == "<EFBFBD>abc"
|
||
|
||
|
||
def test_local_shell_component_falls_back_when_windows_taskkill_fails(
|
||
monkeypatch,
|
||
) -> None:
|
||
class TimeoutProcess:
|
||
pid = 12345
|
||
returncode = None
|
||
|
||
def __init__(self):
|
||
self.killed = False
|
||
|
||
def kill(self) -> None:
|
||
self.killed = True
|
||
|
||
async def wait(self) -> int:
|
||
return 0
|
||
|
||
class TimeoutSession:
|
||
def __init__(self) -> None:
|
||
self._proc = TimeoutProcess()
|
||
|
||
async def exec(self, *args, **kwargs):
|
||
_ = args, kwargs
|
||
await asyncio.Event().wait()
|
||
|
||
class TaskkillResult:
|
||
returncode = 1
|
||
|
||
session = TimeoutSession()
|
||
cleaned_up: list[str] = []
|
||
|
||
monkeypatch.setattr(
|
||
local_booter.PersistentShellSession,
|
||
"get_or_create",
|
||
lambda _key: session,
|
||
)
|
||
monkeypatch.setattr(
|
||
local_booter.PersistentShellSession,
|
||
"cleanup",
|
||
lambda key: _record_cleanup(cleaned_up, key),
|
||
)
|
||
monkeypatch.setattr(
|
||
subprocess,
|
||
"run",
|
||
lambda *_args, **_kwargs: TaskkillResult(),
|
||
)
|
||
monkeypatch.setattr(local_booter.sys, "platform", "win32")
|
||
|
||
with pytest.raises(TimeoutError):
|
||
asyncio.run(LocalShellComponent().exec("dummy", timeout=0.001))
|
||
|
||
assert session._proc.killed
|
||
assert cleaned_up == ["default"]
|
||
|
||
|
||
async def _record_cleanup(cleaned_up: list[str], key: str) -> None:
|
||
cleaned_up.append(key)
|