Clawith/backend/tests/test_skills_api.py

209 lines
5.5 KiB
Python

import uuid
from types import SimpleNamespace
import httpx
import pytest
from app.api import skills as skills_api
from app.core.security import get_current_user
from app.main import app
class FakeScalarResult:
def __init__(self, value):
self._value = value
def scalar_one_or_none(self):
return self._value
class TrapList(list):
def __iter__(self):
raise AssertionError("newly created skills should not iterate over lazy files")
class FakeSession:
def __init__(self, *, skill=None):
self.skill = skill
self.added = []
self.deleted = []
self.committed = False
async def execute(self, _query):
return FakeScalarResult(self.skill)
def add(self, value):
self.added.append(value)
async def flush(self):
return None
async def delete(self, value):
self.deleted.append(value)
async def commit(self):
self.committed = True
class FakeAsyncSessionFactory:
def __init__(self, session):
self.session = session
def __call__(self):
return self
async def __aenter__(self):
return self.session
async def __aexit__(self, exc_type, exc, tb):
return False
class FakeQuery:
def where(self, *_args, **_kwargs):
return self
def options(self, *_args, **_kwargs):
return self
def order_by(self, *_args, **_kwargs):
return self
class RaiseOnInstanceAccess:
def __get__(self, instance, owner):
if instance is None:
return self
raise AssertionError("newly created skills should not iterate over lazy files")
class QueryField:
def is_(self, _value):
return self
def __eq__(self, _other):
return self
class FakeSkill:
folder_name = QueryField()
tenant_id = QueryField()
files = RaiseOnInstanceAccess()
def __init__(self, **kwargs):
self.id = uuid.uuid4()
for key, value in kwargs.items():
setattr(self, key, value)
@pytest.fixture
def org_admin_user():
return SimpleNamespace(
id=uuid.uuid4(),
role="org_admin",
tenant_id=uuid.uuid4(),
is_active=True,
department_id=None,
)
@pytest.fixture
def platform_admin_user():
return SimpleNamespace(
id=uuid.uuid4(),
role="platform_admin",
tenant_id=uuid.uuid4(),
is_active=True,
department_id=None,
)
@pytest.fixture
def client():
transport = httpx.ASGITransport(app=app)
async def _build():
return httpx.AsyncClient(transport=transport, base_url="http://test")
return _build
@pytest.mark.asyncio
async def test_org_admin_can_delete_custom_skill_via_browse(monkeypatch, client, org_admin_user):
skill = SimpleNamespace(
id=uuid.uuid4(),
folder_name="tenant-skill",
tenant_id=org_admin_user.tenant_id,
is_builtin=False,
files=[],
)
session = FakeSession(skill=skill)
monkeypatch.setattr(skills_api, "async_session", FakeAsyncSessionFactory(session))
app.dependency_overrides[get_current_user] = lambda: org_admin_user
async with await client() as ac:
response = await ac.delete("/api/skills/browse/delete", params={"path": "tenant-skill"})
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {"ok": True}
assert session.deleted == [skill]
assert session.committed is True
@pytest.mark.asyncio
async def test_org_admin_can_delete_custom_skill_directly(monkeypatch, client, org_admin_user):
skill = SimpleNamespace(
id=uuid.uuid4(),
folder_name="tenant-skill",
tenant_id=org_admin_user.tenant_id,
is_builtin=False,
)
session = FakeSession(skill=skill)
monkeypatch.setattr(skills_api, "async_session", FakeAsyncSessionFactory(session))
app.dependency_overrides[get_current_user] = lambda: org_admin_user
async with await client() as ac:
response = await ac.delete(f"/api/skills/{skill.id}")
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {"ok": True}
assert session.deleted == [skill]
assert session.committed is True
@pytest.mark.asyncio
async def test_browse_write_creates_tenant_skill_without_iterating_lazy_files(
monkeypatch, client, platform_admin_user
):
session = FakeSession(skill=None)
monkeypatch.setattr(skills_api, "async_session", FakeAsyncSessionFactory(session))
monkeypatch.setattr(skills_api, "select", lambda *_args, **_kwargs: FakeQuery())
monkeypatch.setattr(skills_api, "selectinload", lambda *_args, **_kwargs: None)
monkeypatch.setattr(skills_api, "Skill", FakeSkill)
app.dependency_overrides[get_current_user] = lambda: platform_admin_user
async with await client() as ac:
response = await ac.put(
"/api/skills/browse/write",
json={"path": "tenant-skill/SKILL.md", "content": "# test"},
)
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {"ok": True}
created_skill = next(value for value in session.added if isinstance(value, FakeSkill))
created_file = next(value for value in session.added if isinstance(value, skills_api.SkillFile))
assert created_skill.folder_name == "tenant-skill"
assert created_skill.tenant_id == platform_admin_user.tenant_id
assert created_file.path == "SKILL.md"
assert created_file.content == "# test"
assert session.committed is True