diff --git a/backend/app/api/gateways.py b/backend/app/api/gateways.py index 53548396..87f48dab 100644 --- a/backend/app/api/gateways.py +++ b/backend/app/api/gateways.py @@ -14,6 +14,7 @@ from app.db import crud from app.db.pagination import paginate from app.db.session import get_session from app.models.agents import Agent +from app.models.gateway_installed_skills import GatewayInstalledSkill from app.models.gateways import Gateway from app.schemas.common import OkResponse from app.schemas.gateways import ( @@ -175,6 +176,12 @@ async def delete_gateway( await service.clear_agent_foreign_keys(agent_id=agent.id) await session.delete(agent) + installed_skills = await GatewayInstalledSkill.objects.filter_by( + gateway_id=gateway.id, + ).all(session) + for installed_skill in installed_skills: + await session.delete(installed_skill) + await session.delete(gateway) await session.commit() return OkResponse() diff --git a/backend/migrations/versions/c9d7e9b6a4f2_add_skills_marketplace_tables.py b/backend/migrations/versions/c9d7e9b6a4f2_add_skills_marketplace_tables.py index a96643e8..0dff61d1 100644 --- a/backend/migrations/versions/c9d7e9b6a4f2_add_skills_marketplace_tables.py +++ b/backend/migrations/versions/c9d7e9b6a4f2_add_skills_marketplace_tables.py @@ -73,6 +73,7 @@ def upgrade() -> None: sa.ForeignKeyConstraint( ["gateway_id"], ["gateways.id"], + ondelete="CASCADE", ), sa.ForeignKeyConstraint( ["skill_id"], diff --git a/backend/tests/test_skills_marketplace_api.py b/backend/tests/test_skills_marketplace_api.py index 4ddda932..df6c5d75 100644 --- a/backend/tests/test_skills_marketplace_api.py +++ b/backend/tests/test_skills_marketplace_api.py @@ -15,6 +15,7 @@ from sqlmodel import SQLModel, col, select from sqlmodel.ext.asyncio.session import AsyncSession from app.api.deps import require_org_admin +from app.api.gateways import router as gateways_router from app.api.skills_marketplace import ( PackSkillCandidate, _collect_pack_skills_from_repo, @@ -44,6 +45,7 @@ def _build_test_app( ) -> FastAPI: app = FastAPI() api_v1 = APIRouter(prefix="/api/v1") + api_v1.include_router(gateways_router) api_v1.include_router(skills_marketplace_router) app.include_router(api_v1) @@ -171,6 +173,58 @@ async def test_install_skill_dispatches_instruction_and_persists_installation( await engine.dispose() +@pytest.mark.asyncio +async def test_delete_gateway_removes_installed_skill_rows() -> 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) + skill = MarketplaceSkill( + organization_id=organization.id, + name="Deploy Helper", + source_url="https://example.com/skills/deploy-helper.git", + ) + session.add(skill) + await session.commit() + await session.refresh(skill) + session.add( + GatewayInstalledSkill( + gateway_id=gateway.id, + skill_id=skill.id, + ), + ) + await session.commit() + + app = _build_test_app(session_maker, organization=organization) + async with AsyncClient( + transport=ASGITransport(app=app), + base_url="http://testserver", + ) as client: + response = await client.delete(f"/api/v1/gateways/{gateway.id}") + + assert response.status_code == 200 + assert response.json() == {"ok": True} + + async with session_maker() as session: + deleted_gateway = await session.get(Gateway, gateway.id) + assert deleted_gateway is None + remaining_installs = ( + await session.exec( + select(GatewayInstalledSkill).where( + col(GatewayInstalledSkill.gateway_id) == gateway.id, + ), + ) + ).all() + assert remaining_installs == [] + finally: + await engine.dispose() + + @pytest.mark.asyncio async def test_list_marketplace_skills_marks_installed_cards() -> None: engine = await _make_engine() diff --git a/frontend/src/app/skills/marketplace/[skillId]/edit/page.tsx b/frontend/src/app/skills/marketplace/[skillId]/edit/page.tsx index d5c1720a..f72c638c 100644 --- a/frontend/src/app/skills/marketplace/[skillId]/edit/page.tsx +++ b/frontend/src/app/skills/marketplace/[skillId]/edit/page.tsx @@ -7,6 +7,6 @@ type EditMarketplaceSkillPageProps = { export default async function EditMarketplaceSkillPage({ params, }: EditMarketplaceSkillPageProps) { - const { skillId } = await params; - redirect(`/skills/packs/${skillId}/edit`); + await params; + redirect("/skills/marketplace"); } diff --git a/frontend/src/components/skills/MarketplaceSkillsTable.tsx b/frontend/src/components/skills/MarketplaceSkillsTable.tsx index 65259b8e..c345531d 100644 --- a/frontend/src/components/skills/MarketplaceSkillsTable.tsx +++ b/frontend/src/components/skills/MarketplaceSkillsTable.tsx @@ -80,9 +80,9 @@ const toPackLabel = (packUrl: string): string => { } }; -const toPackDetailHref = (packUrl: string): string => { +const toPacksHref = (packUrl: string): string => { const params = new URLSearchParams({ source_url: packUrl }); - return `/skills/packs/detail?${params.toString()}`; + return `/skills/packs?${params.toString()}`; }; export function MarketplaceSkillsTable({ @@ -143,7 +143,7 @@ export function MarketplaceSkillsTable({ const packUrl = toPackUrl(row.original.source_url); return ( {truncate(toPackLabel(packUrl), 40)}