mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
237 lines
7.6 KiB
Python
237 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import cast
|
|
|
|
from astrbot.core.computer import computer_client
|
|
from astrbot.core.computer.booters.base import ComputerBooter
|
|
|
|
|
|
def _extract_embedded_python(command: str) -> str:
|
|
start_marker = "$PYBIN - <<'PY'\n"
|
|
end_marker = "\nPY"
|
|
start = command.find(start_marker)
|
|
assert start != -1
|
|
start += len(start_marker)
|
|
end = command.rfind(end_marker)
|
|
assert end != -1
|
|
return command[start:end]
|
|
|
|
|
|
class _FakeShell:
|
|
def __init__(self, sync_payload_json: str):
|
|
self.sync_payload_json = sync_payload_json
|
|
self.commands: list[str] = []
|
|
|
|
async def exec(self, command: str, **kwargs):
|
|
_ = kwargs
|
|
self.commands.append(command)
|
|
if "PYBIN" in command and "managed_skills" in command:
|
|
return {
|
|
"success": True,
|
|
"stdout": self.sync_payload_json,
|
|
"stderr": "",
|
|
"exit_code": 0,
|
|
}
|
|
return {"success": True, "stdout": "", "stderr": "", "exit_code": 0}
|
|
|
|
|
|
class _FakeBooter:
|
|
def __init__(self, sync_payload_json: str):
|
|
self.shell = _FakeShell(sync_payload_json)
|
|
self.uploads: list[tuple[str, str]] = []
|
|
self.uploaded_entries: list[str] = []
|
|
|
|
async def upload_file(self, path: str, file_name: str) -> dict:
|
|
self.uploads.append((path, file_name))
|
|
with zipfile.ZipFile(path) as zf:
|
|
self.uploaded_entries = sorted(
|
|
name.replace("\\", "/") for name in zf.namelist()
|
|
)
|
|
return {"success": True}
|
|
|
|
|
|
def _write_sdk_registered_skill(
|
|
root: Path,
|
|
skill_name: str,
|
|
) -> None:
|
|
skill_dir = root / skill_name
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
skill_dir.joinpath("SKILL.md").write_text("# demo", encoding="utf-8")
|
|
|
|
|
|
def test_sync_skills_keeps_builtin_skills_when_local_is_empty(monkeypatch, tmp_path: Path):
|
|
data_dir = tmp_path / "data"
|
|
skills_root = tmp_path / "skills"
|
|
temp_root = tmp_path / "temp"
|
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
skills_root.mkdir(parents=True, exist_ok=True)
|
|
temp_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
captured = {"skills": None}
|
|
|
|
def _fake_set_cache(self, skills):
|
|
captured["skills"] = skills
|
|
|
|
monkeypatch.setattr(
|
|
"astrbot.core.computer.computer_client.get_astrbot_skills_path",
|
|
lambda: str(skills_root),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.computer.computer_client.get_astrbot_temp_path",
|
|
lambda: str(temp_root),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.skills.skill_manager.get_astrbot_data_path",
|
|
lambda: str(data_dir),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.skills.skill_manager.get_astrbot_temp_path",
|
|
lambda: str(temp_root),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.computer.computer_client.SkillManager.set_sandbox_skills_cache",
|
|
_fake_set_cache,
|
|
)
|
|
|
|
booter = _FakeBooter(
|
|
'{"skills":[{"name":"python-sandbox","description":"ship","path":"skills/python-sandbox/SKILL.md"}]}'
|
|
)
|
|
asyncio.run(computer_client._sync_skills_to_sandbox(cast(ComputerBooter, booter)))
|
|
|
|
assert booter.uploads == []
|
|
assert any(cmd == "rm -f skills/skills.zip" for cmd in booter.shell.commands)
|
|
assert captured["skills"] == [
|
|
{
|
|
"name": "python-sandbox",
|
|
"description": "ship",
|
|
"path": "skills/python-sandbox/SKILL.md",
|
|
}
|
|
]
|
|
|
|
|
|
def test_sync_skills_uses_managed_strategy_instead_of_wiping_all(
|
|
monkeypatch,
|
|
tmp_path: Path,
|
|
):
|
|
data_dir = tmp_path / "data"
|
|
skills_root = tmp_path / "skills"
|
|
temp_root = tmp_path / "temp"
|
|
skill_dir = skills_root / "custom-agent-skill"
|
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
skill_dir.joinpath("SKILL.md").write_text("# demo", encoding="utf-8")
|
|
temp_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
captured = {"skills": None}
|
|
|
|
def _fake_set_cache(self, skills):
|
|
captured["skills"] = skills
|
|
|
|
monkeypatch.setattr(
|
|
"astrbot.core.computer.computer_client.get_astrbot_skills_path",
|
|
lambda: str(skills_root),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.computer.computer_client.get_astrbot_temp_path",
|
|
lambda: str(temp_root),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.skills.skill_manager.get_astrbot_data_path",
|
|
lambda: str(data_dir),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.skills.skill_manager.get_astrbot_temp_path",
|
|
lambda: str(temp_root),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.computer.computer_client.SkillManager.set_sandbox_skills_cache",
|
|
_fake_set_cache,
|
|
)
|
|
|
|
booter = _FakeBooter(
|
|
'{"skills":[{"name":"custom-agent-skill","description":"","path":"skills/custom-agent-skill/SKILL.md"}]}'
|
|
)
|
|
asyncio.run(computer_client._sync_skills_to_sandbox(cast(ComputerBooter, booter)))
|
|
|
|
assert len(booter.uploads) == 1
|
|
assert booter.uploads[0][1].replace("\\", "/") == "skills/skills.zip"
|
|
assert not any(
|
|
"find skills -mindepth 1 -delete" in cmd for cmd in booter.shell.commands
|
|
)
|
|
assert captured["skills"] == [
|
|
{
|
|
"name": "custom-agent-skill",
|
|
"description": "",
|
|
"path": "skills/custom-agent-skill/SKILL.md",
|
|
}
|
|
]
|
|
|
|
|
|
def test_sync_skills_includes_sdk_registered_skills(monkeypatch, tmp_path: Path):
|
|
data_dir = tmp_path / "data"
|
|
skills_root = tmp_path / "skills"
|
|
temp_root = tmp_path / "temp"
|
|
registered_root = tmp_path / "sdk_registered"
|
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
skills_root.mkdir(parents=True, exist_ok=True)
|
|
temp_root.mkdir(parents=True, exist_ok=True)
|
|
registered_root.mkdir(parents=True, exist_ok=True)
|
|
_write_sdk_registered_skill(registered_root, "browser-helper")
|
|
|
|
captured = {"skills": None}
|
|
|
|
def _fake_set_cache(self, skills):
|
|
captured["skills"] = skills
|
|
|
|
monkeypatch.setattr(
|
|
"astrbot.core.computer.computer_client.get_astrbot_skills_path",
|
|
lambda: str(skills_root),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.computer.computer_client.get_astrbot_temp_path",
|
|
lambda: str(temp_root),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.skills.skill_manager.get_astrbot_data_path",
|
|
lambda: str(data_dir),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.skills.skill_manager.get_astrbot_temp_path",
|
|
lambda: str(temp_root),
|
|
)
|
|
monkeypatch.setattr(
|
|
"astrbot.core.computer.computer_client.SkillManager.set_sandbox_skills_cache",
|
|
_fake_set_cache,
|
|
)
|
|
SkillManager(skills_root=str(skills_root)).replace_sdk_plugin_skills(
|
|
"sdk-demo",
|
|
[
|
|
{
|
|
"name": "sdk-demo.browser-helper",
|
|
"description": "",
|
|
"path": str(registered_root / "browser-helper" / "SKILL.md"),
|
|
"skill_dir": str(registered_root / "browser-helper"),
|
|
}
|
|
],
|
|
)
|
|
|
|
booter = _FakeBooter(
|
|
'{"skills":[{"name":"sdk-demo.browser-helper","description":"","path":"skills/sdk-demo.browser-helper/SKILL.md"}]}'
|
|
)
|
|
asyncio.run(computer_client._sync_skills_to_sandbox(booter))
|
|
|
|
assert len(booter.uploads) == 1
|
|
assert "sdk-demo.browser-helper/" in booter.uploaded_entries
|
|
assert "sdk-demo.browser-helper/SKILL.md" in booter.uploaded_entries
|
|
assert captured["skills"] == [
|
|
{
|
|
"name": "sdk-demo.browser-helper",
|
|
"description": "",
|
|
"path": "skills/sdk-demo.browser-helper/SKILL.md",
|
|
}
|
|
]
|
|
|