feat(skills): add metadata and branch fields to skill packs and marketplace skills

This commit is contained in:
Abhimanyu Saharan
2026-02-14 12:26:45 +05:30
parent 5b9e81aa6d
commit 40dcf50f4b
17 changed files with 1049 additions and 51 deletions

View File

@@ -3,11 +3,13 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from uuid import uuid4
import pytest
from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError
from app.models.boards import Board
from app.models.organization_board_access import OrganizationBoardAccess
@@ -15,6 +17,7 @@ from app.models.organization_invite_board_access import OrganizationInviteBoardA
from app.models.organization_invites import OrganizationInvite
from app.models.organization_members import OrganizationMember
from app.models.organizations import Organization
from app.models.skill_packs import SkillPack
from app.models.users import User
from app.schemas.organizations import OrganizationBoardAccessSpec, OrganizationMemberAccessUpdate
from app.services import organizations
@@ -107,6 +110,44 @@ def test_normalize_role(value: str, expected: str) -> None:
assert organizations.normalize_role(value) == expected
def test_normalize_skill_pack_source_url_normalizes_trivial_variants() -> None:
assert (
organizations._normalize_skill_pack_source_url("https://github.com/org/repo")
== "https://github.com/org/repo"
)
assert (
organizations._normalize_skill_pack_source_url("https://github.com/org/repo/")
== "https://github.com/org/repo"
)
assert (
organizations._normalize_skill_pack_source_url(" https://github.com/org/repo.git ")
== "https://github.com/org/repo"
)
def test_get_default_skill_pack_records_deduplicates_normalized_urls(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
organizations,
"DEFAULT_INSTALLER_SKILL_PACKS",
(
("owner/repo", "pack one", "first"),
("owner/repo/", "pack duplicate", "duplicate"),
("owner/repo.git", "pack duplicate again", "duplicate again"),
("owner/other", "other", "other"),
),
)
now = datetime(2025, 1, 1)
records = organizations._get_default_skill_pack_records(org_id=uuid4(), now=now)
assert len(records) == 2
assert {pack.source_url for pack in records} == {
"https://github.com/owner/repo",
"https://github.com/owner/other",
}
def test_role_rank_unknown_role_falls_back_to_member_rank() -> None:
assert organizations._role_rank("madeup") == 0
assert organizations._role_rank(None) == 0
@@ -218,7 +259,119 @@ async def test_ensure_member_for_user_creates_personal_org_and_owner(
assert any(
isinstance(item, Organization) and item.id == out.organization_id for item in session.added
)
assert session.committed == 1
skill_packs = [
item
for item in [*session.added, *[record for batch in session.added_all for record in batch]]
if isinstance(item, SkillPack)
]
assert len(skill_packs) == 2
pack_sources = {pack.source_url: pack.description for pack in skill_packs}
assert (
pack_sources["https://github.com/sickn33/antigravity-awesome-skills"]
== "The Ultimate Collection of 800+ Agentic Skills for Claude Code/Antigravity/Cursor. "
"Battle-tested, high-performance skills for AI agents including official skills from "
"Anthropic and Vercel."
)
assert (
pack_sources["https://github.com/BrianRWagner/ai-marketing-skills"]
== "Marketing frameworks that AI actually executes. Use for Claude Code, OpenClaw, etc."
)
assert session.committed == 3
assert len(session.added_all) == 0
assert {pack.source_url for pack in skill_packs} == {
"https://github.com/sickn33/antigravity-awesome-skills",
"https://github.com/BrianRWagner/ai-marketing-skills",
}
@pytest.mark.asyncio
async def test_ensure_member_for_user_skips_already_existing_default_pack_by_source_url(
monkeypatch: pytest.MonkeyPatch,
) -> None:
user = User(clerk_user_id="u1", email=None)
existing_pack_source = "https://github.com/sickn33/antigravity-awesome-skills/"
async def _fake_get_active(_session: Any, _user: User) -> None:
return None
async def _fake_get_first(_session: Any, _user_id: Any) -> None:
return None
async def _fake_fetch_existing_pack_sources(
_session: Any,
_org_id: Any,
) -> set[str]:
return {existing_pack_source}
monkeypatch.setattr(organizations, "get_active_membership", _fake_get_active)
monkeypatch.setattr(organizations, "get_first_membership", _fake_get_first)
monkeypatch.setattr(
organizations,
"_fetch_existing_default_pack_sources",
_fake_fetch_existing_pack_sources,
)
session = _FakeSession(exec_results=[_FakeExecResult()])
out = await organizations.ensure_member_for_user(session, user)
assert out.user_id == user.id
assert out.role == "owner"
assert out.organization_id == user.active_organization_id
skill_packs = [item for item in session.added if isinstance(item, SkillPack)]
assert len(skill_packs) == 1
assert skill_packs[0].source_url == "https://github.com/BrianRWagner/ai-marketing-skills"
assert session.committed == 2
assert len(session.added_all) == 0
@pytest.mark.asyncio
async def test_ensure_member_for_user_recovers_on_default_install_integrity_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
org_id = uuid4()
user = User(clerk_user_id="u1", email=None, active_organization_id=org_id)
existing_member = OrganizationMember(
organization_id=org_id,
user_id=user.id,
role="owner",
)
call_count = 0
async def _fake_get_active(_session: Any, _user: User) -> None:
return None
async def _fake_get_first(_session: Any, _user_id: Any) -> OrganizationMember | None:
nonlocal call_count
call_count += 1
if call_count == 1:
return None
return existing_member
async def _fake_fetch_existing_pack_sources(
_session: Any,
_org_id: Any,
) -> set[str]:
return set()
monkeypatch.setattr(organizations, "get_active_membership", _fake_get_active)
monkeypatch.setattr(organizations, "get_first_membership", _fake_get_first)
monkeypatch.setattr(
organizations,
"_fetch_existing_default_pack_sources",
_fake_fetch_existing_pack_sources,
)
session = _FakeSession(
exec_results=[_FakeExecResult(), _FakeExecResult()],
commit_side_effects=[IntegrityError("statement", [], None)],
)
out = await organizations.ensure_member_for_user(session, user)
assert out is existing_member
assert out.organization_id == org_id
assert call_count == 2
assert user.active_organization_id == org_id
@pytest.mark.asyncio

View File

@@ -485,6 +485,61 @@ async def test_create_skill_pack_rejects_localhost_source_url() -> None:
await engine.dispose()
@pytest.mark.asyncio
async def test_create_skill_pack_is_unique_by_normalized_source_url() -> None:
engine = await _make_engine()
session_maker = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
try:
async with session_maker() as session:
organization, _gateway = await _seed_base(session)
await session.commit()
app = _build_test_app(session_maker, organization=organization)
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
) as client:
first = await client.post(
"/api/v1/skills/packs",
json={"source_url": "https://github.com/org/repo"},
)
spaced = await client.post(
"/api/v1/skills/packs",
json={"source_url": " https://github.com/org/repo.git "},
)
second = await client.post(
"/api/v1/skills/packs",
json={"source_url": "https://github.com/org/repo/"},
)
third = await client.post(
"/api/v1/skills/packs",
json={"source_url": "https://github.com/org/repo.git"},
)
packs = await client.get("/api/v1/skills/packs")
assert first.status_code == 200
assert spaced.status_code == 200
assert second.status_code == 200
assert third.status_code == 200
assert spaced.json()["id"] == first.json()["id"]
assert spaced.json()["source_url"] == first.json()["source_url"]
assert second.json()["id"] == first.json()["id"]
assert second.json()["source_url"] == first.json()["source_url"]
assert third.json()["id"] == first.json()["id"]
assert third.json()["source_url"] == first.json()["source_url"]
assert packs.status_code == 200
pack_items = packs.json()
assert len(pack_items) == 1
assert pack_items[0]["source_url"] == "https://github.com/org/repo"
finally:
await engine.dispose()
@pytest.mark.asyncio
async def test_list_skill_packs_includes_skill_count() -> None:
engine = await _make_engine()
@@ -548,6 +603,109 @@ async def test_list_skill_packs_includes_skill_count() -> None:
await engine.dispose()
@pytest.mark.asyncio
async def test_update_skill_pack_rejects_duplicate_normalized_source_url() -> None:
engine = await _make_engine()
session_maker = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
try:
async with session_maker() as session:
organization, _gateway = await _seed_base(session)
pack_a = SkillPack(
organization_id=organization.id,
source_url="https://github.com/org/repo",
name="Pack A",
)
pack_b = SkillPack(
organization_id=organization.id,
source_url="https://github.com/org/other-repo",
name="Pack B",
)
session.add(pack_a)
session.add(pack_b)
await session.commit()
await session.refresh(pack_a)
await session.refresh(pack_b)
app = _build_test_app(session_maker, organization=organization)
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
) as client:
response = await client.patch(
f"/api/v1/skills/packs/{pack_b.id}",
json={"source_url": "https://github.com/org/repo/"},
)
assert response.status_code == 409
assert "already exists" in response.json()["detail"].lower()
async with session_maker() as session:
pack_rows = (
await session.exec(
select(SkillPack)
.where(col(SkillPack.organization_id) == organization.id)
.order_by(col(SkillPack.created_at).asc())
)
).all()
assert len(pack_rows) == 2
assert {str(pack.source_url) for pack in pack_rows} == {
"https://github.com/org/repo",
"https://github.com/org/other-repo",
}
finally:
await engine.dispose()
@pytest.mark.asyncio
async def test_update_skill_pack_normalizes_source_url_on_update() -> None:
engine = await _make_engine()
session_maker = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
try:
async with session_maker() as session:
organization, _gateway = await _seed_base(session)
pack = SkillPack(
organization_id=organization.id,
source_url="https://github.com/org/old",
name="Initial",
)
session.add(pack)
await session.commit()
await session.refresh(pack)
app = _build_test_app(session_maker, organization=organization)
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
) as client:
response = await client.patch(
f"/api/v1/skills/packs/{pack.id}",
json={"source_url": " https://github.com/org/new.git/ "},
)
assert response.status_code == 200
assert response.json()["source_url"] == "https://github.com/org/new"
async with session_maker() as session:
updated = (
await session.exec(
select(SkillPack).where(col(SkillPack.id) == pack.id),
)
).one()
assert str(updated.source_url) == "https://github.com/org/new"
finally:
await engine.dispose()
def test_collect_pack_skills_from_repo_uses_root_index_when_present(tmp_path: Path) -> None:
repo_dir = tmp_path / "repo"
repo_dir.mkdir()
@@ -672,3 +830,39 @@ def test_collect_pack_skills_from_repo_supports_top_level_skill_folders(
"https://github.com/BrianRWagner/ai-marketing-skills/tree/main/homepage-audit"
in by_source
)
def test_collect_pack_skills_from_repo_streams_large_index(tmp_path: Path) -> None:
repo_dir = tmp_path / "repo"
repo_dir.mkdir()
(repo_dir / "SKILL.md").write_text("# Fallback Skill\n", encoding="utf-8")
huge_description = "x" * (300 * 1024)
(repo_dir / "skills_index.json").write_text(
json.dumps(
{
"skills": [
{
"id": "oversized",
"name": "Huge Index Skill",
"description": huge_description,
"path": "skills/ignored",
},
],
}
),
encoding="utf-8",
)
skills = _collect_pack_skills_from_repo(
repo_dir=repo_dir,
source_url="https://github.com/example/oversized-pack",
branch="main",
)
assert len(skills) == 1
assert (
skills[0].source_url
== "https://github.com/example/oversized-pack/tree/main/skills/ignored"
)
assert skills[0].name == "Huge Index Skill"