Files
personal-ai-assistant/backend/tests/test_skills.py
dolgolyov.alexei 03afb7a075 Phase 3: Skills & Context — skill system, personal context, context layering
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>
2026-03-19 12:55:02 +03:00

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