Backend: - Skill model + migration (with FK on chats.skill_id) - Personal + general skill CRUD services with access isolation - Admin skill CRUD endpoints (POST/GET/PATCH/DELETE /admin/skills) - User skill CRUD endpoints (POST/GET/PATCH/DELETE /skills/) - Personal context GET/PUT at /users/me/context - Extended context assembly: primary + personal context + skill prompt - Chat creation/update now accepts skill_id with validation Frontend: - Skill selector dropdown in chat header (grouped: general + personal) - Reusable skill editor form component - Admin skills management page (/admin/skills) - Personal skills page (/skills) - Personal context editor page (/profile/context) - Updated sidebar: Skills, My Context nav items + admin skills link - English + Russian translations for all skill/context UI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
@pytest.fixture
|
|
async def user_headers(client: AsyncClient):
|
|
resp = await client.post("/api/v1/auth/register", json={
|
|
"email": "skilluser@example.com",
|
|
"username": "skilluser",
|
|
"password": "testpass123",
|
|
})
|
|
assert resp.status_code == 201
|
|
return {"Authorization": f"Bearer {resp.json()['access_token']}"}
|
|
|
|
|
|
@pytest.fixture
|
|
async def other_user_headers(client: AsyncClient):
|
|
resp = await client.post("/api/v1/auth/register", json={
|
|
"email": "skillother@example.com",
|
|
"username": "skillother",
|
|
"password": "testpass123",
|
|
})
|
|
assert resp.status_code == 201
|
|
return {"Authorization": f"Bearer {resp.json()['access_token']}"}
|
|
|
|
|
|
# --- Personal Skills ---
|
|
|
|
async def test_create_personal_skill(client: AsyncClient, user_headers: dict):
|
|
resp = await client.post("/api/v1/skills/", json={
|
|
"name": "Nutritionist",
|
|
"description": "Diet and nutrition advice",
|
|
"system_prompt": "You are a nutritionist.",
|
|
}, headers=user_headers)
|
|
assert resp.status_code == 201
|
|
data = resp.json()
|
|
assert data["name"] == "Nutritionist"
|
|
assert data["user_id"] is not None
|
|
|
|
|
|
async def test_list_personal_skills(client: AsyncClient, user_headers: dict):
|
|
await client.post("/api/v1/skills/", json={
|
|
"name": "Test Skill",
|
|
"system_prompt": "Test prompt",
|
|
}, headers=user_headers)
|
|
|
|
resp = await client.get("/api/v1/skills/", params={"include_general": False}, headers=user_headers)
|
|
assert resp.status_code == 200
|
|
assert len(resp.json()["skills"]) >= 1
|
|
|
|
|
|
async def test_update_personal_skill(client: AsyncClient, user_headers: dict):
|
|
resp = await client.post("/api/v1/skills/", json={
|
|
"name": "Old Name",
|
|
"system_prompt": "Prompt",
|
|
}, headers=user_headers)
|
|
skill_id = resp.json()["id"]
|
|
|
|
resp = await client.patch(f"/api/v1/skills/{skill_id}", json={
|
|
"name": "New Name",
|
|
}, headers=user_headers)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == "New Name"
|
|
|
|
|
|
async def test_delete_personal_skill(client: AsyncClient, user_headers: dict):
|
|
resp = await client.post("/api/v1/skills/", json={
|
|
"name": "To Delete",
|
|
"system_prompt": "Prompt",
|
|
}, headers=user_headers)
|
|
skill_id = resp.json()["id"]
|
|
|
|
resp = await client.delete(f"/api/v1/skills/{skill_id}", headers=user_headers)
|
|
assert resp.status_code == 204
|
|
|
|
|
|
async def test_cannot_access_other_users_skill(client: AsyncClient, user_headers: dict, other_user_headers: dict):
|
|
resp = await client.post("/api/v1/skills/", json={
|
|
"name": "Private Skill",
|
|
"system_prompt": "Prompt",
|
|
}, headers=user_headers)
|
|
skill_id = resp.json()["id"]
|
|
|
|
# Other user can't see it
|
|
resp = await client.get(f"/api/v1/skills/{skill_id}", headers=other_user_headers)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# --- Admin Skills ---
|
|
|
|
async def test_non_admin_cannot_manage_general_skills(client: AsyncClient, user_headers: dict):
|
|
resp = await client.get("/api/v1/admin/skills", headers=user_headers)
|
|
assert resp.status_code == 403
|
|
|
|
resp = await client.post("/api/v1/admin/skills", json={
|
|
"name": "General",
|
|
"system_prompt": "Prompt",
|
|
}, headers=user_headers)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
# --- Personal Context ---
|
|
|
|
async def test_personal_context_crud(client: AsyncClient, user_headers: dict):
|
|
# Initially null
|
|
resp = await client.get("/api/v1/users/me/context", headers=user_headers)
|
|
assert resp.status_code == 200
|
|
|
|
# Create
|
|
resp = await client.put("/api/v1/users/me/context", json={
|
|
"content": "I have diabetes type 2",
|
|
}, headers=user_headers)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["content"] == "I have diabetes type 2"
|
|
assert data["version"] == 1
|
|
|
|
# Update
|
|
resp = await client.put("/api/v1/users/me/context", json={
|
|
"content": "I have diabetes type 2 and hypertension",
|
|
}, headers=user_headers)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["version"] == 2
|
|
|
|
|
|
# --- Chat with Skill ---
|
|
|
|
async def test_create_chat_with_skill(client: AsyncClient, user_headers: dict):
|
|
# Create a skill first
|
|
resp = await client.post("/api/v1/skills/", json={
|
|
"name": "Cardiologist",
|
|
"system_prompt": "You are a cardiologist.",
|
|
}, headers=user_headers)
|
|
skill_id = resp.json()["id"]
|
|
|
|
# Create chat with skill
|
|
resp = await client.post("/api/v1/chats/", json={
|
|
"title": "Heart Consultation",
|
|
"skill_id": skill_id,
|
|
}, headers=user_headers)
|
|
assert resp.status_code == 201
|
|
assert resp.json()["skill_id"] == skill_id
|