"""GET /api/plugins/ui-manifest returns the aggregated manifest (SPEC ยง11.3).""" from __future__ import annotations import uuid import httpx from httpx import ASGITransport from sqlalchemy import delete from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool from app.config import get_settings from app.identity.passwords import hash_password from app.identity.sessions import create_session from app.main import create_app from app.models import User from app.plugins import loader from app.plugins.sdk import Plugin, Slot, UiManifest async def test_ui_manifest_endpoint() -> None: engine = create_async_engine(get_settings().database_url, poolclass=NullPool) sm = async_sessionmaker(engine, expire_on_commit=False) session = sm() user = User(handle=f"ui{uuid.uuid4().hex[:8]}@test.local", password_hash=hash_password("pw")) session.add(user) await session.commit() loader.set_plugins_for_test( [ Plugin( name="labels", version="1.0", ui_manifest=UiManifest(slots=[Slot(name="message.actions", bundle="/labels.js")]), ) ] ) token = await create_session(user.id, "pytest") transport = ASGITransport(app=create_app()) client = httpx.AsyncClient( transport=transport, base_url="http://testserver", cookies={"session": token} ) try: # Unauthenticated request is rejected. async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as anon: assert (await anon.get("/api/plugins/ui-manifest")).status_code == 401 resp = await client.get("/api/plugins/ui-manifest") assert resp.status_code == 200 body = resp.json() assert body == { "plugins": [ { "name": "labels", "version": "1.0", "builtin": False, "slots": [{"name": "message.actions", "bundle": "/labels.js"}], "pages": [], "capabilities": {}, } ] } finally: await client.aclose() async with sm() as cleanup: await cleanup.execute(delete(User).where(User.id == user.id)) await cleanup.commit() await session.close() await engine.dispose()