mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
"""The Directory's Upload skill endpoint: a bare SKILL .md needs YAML name+description,
|
|
a .zip/.skill archive needs a SKILL.md (shallowest wins, siblings ride along), and
|
|
anything else is refused with a readable reason."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import io
|
|
import zipfile
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
import backend.apps.skills.skills as skills_mod
|
|
from backend.apps.skills.models import SkillUpload
|
|
|
|
|
|
@pytest.fixture
|
|
def isolated_skills(tmp_path, monkeypatch):
|
|
d = tmp_path / "skills"
|
|
d.mkdir()
|
|
monkeypatch.setattr(skills_mod, "SKILLS_DIR", str(d))
|
|
monkeypatch.setattr(skills_mod, "INDEX_PATH", str(d / ".skills_index.json"))
|
|
return d
|
|
|
|
|
|
def b64(data: bytes) -> str:
|
|
return base64.b64encode(data).decode()
|
|
|
|
|
|
def make_zip(entries: dict[str, str]) -> bytes:
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as zf:
|
|
for name, content in entries.items():
|
|
zf.writestr(name, content)
|
|
return buf.getvalue()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_md_upload_creates_skill(isolated_skills):
|
|
md = "---\nname: Test Upload\ndescription: A test\n---\n\n# Test Upload\n"
|
|
res = await skills_mod.upload_skill(SkillUpload(filename="test.md", content_b64=b64(md.encode())))
|
|
assert res["ok"] is True
|
|
assert res["skill"]["name"] == "Test Upload"
|
|
assert (isolated_skills / "test-upload" / "SKILL.md").is_file()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_md_without_frontmatter_rejected(isolated_skills):
|
|
with pytest.raises(HTTPException) as e:
|
|
await skills_mod.upload_skill(SkillUpload(filename="x.md", content_b64=b64(b"no yaml")))
|
|
assert e.value.status_code == 400
|
|
assert ".md file must contain skill name and description formatted in YAML" in e.value.detail
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_zip_with_nested_skill_md(isolated_skills):
|
|
raw = make_zip({
|
|
"my-skill/SKILL.md": "---\nname: Zipped\ndescription: d\n---\nbody",
|
|
"my-skill/scripts/run.py": "print('hi')",
|
|
"unrelated/readme.txt": "not part of the skill",
|
|
})
|
|
res = await skills_mod.upload_skill(SkillUpload(filename="my-skill.zip", content_b64=b64(raw)))
|
|
assert res["ok"] is True
|
|
base = isolated_skills / "zipped"
|
|
assert (base / "SKILL.md").is_file()
|
|
assert (base / "scripts" / "run.py").is_file()
|
|
assert not (base / "readme.txt").exists()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_zip_without_skill_md_rejected(isolated_skills):
|
|
raw = make_zip({"folder/notes.md": "just notes"})
|
|
with pytest.raises(HTTPException) as e:
|
|
await skills_mod.upload_skill(SkillUpload(filename="x.zip", content_b64=b64(raw)))
|
|
assert e.value.status_code == 400
|
|
assert ".zip or .skill file must include a SKILL.md file" in e.value.detail
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unsupported_extension_rejected(isolated_skills):
|
|
with pytest.raises(HTTPException) as e:
|
|
await skills_mod.upload_skill(SkillUpload(filename="x.tar.gz", content_b64=b64(b"whatever")))
|
|
assert e.value.status_code == 400
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bad_base64_rejected(isolated_skills):
|
|
with pytest.raises(HTTPException) as e:
|
|
await skills_mod.upload_skill(SkillUpload(filename="x.md", content_b64="!!!not-base64!!!"))
|
|
assert e.value.status_code == 400
|