Files
personal-ai-assistant/backend/tests/test_memory.py
dolgolyov.alexei 8b8fe916f0 Phase 4: Documents & Memory — upload, FTS, AI tools, context injection
Backend:
- Document + MemoryEntry models with Alembic migration (GIN FTS index)
- File upload endpoint with path traversal protection (sanitized filenames)
- Background document text extraction (PyMuPDF)
- Full-text search on extracted_text via PostgreSQL tsvector/tsquery
- Memory CRUD with enum-validated categories/importance, field allow-list
- AI tools: save_memory, search_documents, get_memory (Claude function calling)
- Tool execution loop in stream_ai_response (multi-turn tool use)
- Context assembly: injects critical memory + relevant doc excerpts
- File storage abstraction (local filesystem, S3-swappable)
- Secure file deletion (DB flush before disk delete)

Frontend:
- Document upload dialog (drag-and-drop + file picker)
- Document list with status badges, search, download (via authenticated blob)
- Document viewer with extracted text preview
- Memory list grouped by category with importance color coding
- Memory editor with category/importance dropdowns
- Documents + Memory pages with full CRUD
- Enabled sidebar navigation for both sections

Review fixes applied:
- Sanitized upload filenames (path traversal prevention)
- Download via axios blob (not bare <a href>, preserves auth)
- Route ordering: /search before /{id}/reindex
- Memory update allows is_active=False + field allow-list
- MemoryEditor form resets on mode switch
- Literal enum validation on category/importance schemas
- DB flush before file deletion for data integrity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:46:59 +03:00

110 lines
3.7 KiB
Python

import pytest
from httpx import AsyncClient
@pytest.fixture
async def auth_headers(client: AsyncClient):
resp = await client.post("/api/v1/auth/register", json={
"email": "memuser@example.com",
"username": "memuser",
"password": "testpass123",
})
assert resp.status_code == 201
return {"Authorization": f"Bearer {resp.json()['access_token']}"}
async def test_create_memory(client: AsyncClient, auth_headers: dict):
resp = await client.post("/api/v1/memory/", json={
"category": "condition",
"title": "Diabetes Type 2",
"content": "Diagnosed in 2024, managed with metformin",
"importance": "critical",
}, headers=auth_headers)
assert resp.status_code == 201
data = resp.json()
assert data["category"] == "condition"
assert data["title"] == "Diabetes Type 2"
assert data["importance"] == "critical"
assert data["is_active"] is True
async def test_list_memories(client: AsyncClient, auth_headers: dict):
await client.post("/api/v1/memory/", json={
"category": "allergy",
"title": "Penicillin",
"content": "Severe allergic reaction",
"importance": "critical",
}, headers=auth_headers)
resp = await client.get("/api/v1/memory/", headers=auth_headers)
assert resp.status_code == 200
assert len(resp.json()["entries"]) >= 1
async def test_filter_by_category(client: AsyncClient, auth_headers: dict):
await client.post("/api/v1/memory/", json={
"category": "medication",
"title": "Metformin",
"content": "500mg twice daily",
"importance": "high",
}, headers=auth_headers)
resp = await client.get("/api/v1/memory/", params={"category": "medication"}, headers=auth_headers)
assert resp.status_code == 200
entries = resp.json()["entries"]
assert all(e["category"] == "medication" for e in entries)
async def test_update_memory(client: AsyncClient, auth_headers: dict):
resp = await client.post("/api/v1/memory/", json={
"category": "vital",
"title": "Blood Pressure",
"content": "130/85",
"importance": "medium",
}, headers=auth_headers)
entry_id = resp.json()["id"]
resp = await client.patch(f"/api/v1/memory/{entry_id}", json={
"content": "125/80 (improved)",
"importance": "low",
}, headers=auth_headers)
assert resp.status_code == 200
assert resp.json()["content"] == "125/80 (improved)"
assert resp.json()["importance"] == "low"
async def test_delete_memory(client: AsyncClient, auth_headers: dict):
resp = await client.post("/api/v1/memory/", json={
"category": "other",
"title": "To Delete",
"content": "Test",
"importance": "low",
}, headers=auth_headers)
entry_id = resp.json()["id"]
resp = await client.delete(f"/api/v1/memory/{entry_id}", headers=auth_headers)
assert resp.status_code == 204
resp = await client.get(f"/api/v1/memory/{entry_id}", headers=auth_headers)
assert resp.status_code == 404
async def test_memory_ownership_isolation(client: AsyncClient, auth_headers: dict):
resp = await client.post("/api/v1/memory/", json={
"category": "condition",
"title": "Private Info",
"content": "Sensitive",
"importance": "critical",
}, headers=auth_headers)
entry_id = resp.json()["id"]
resp = await client.post("/api/v1/auth/register", json={
"email": "memother@example.com",
"username": "memother",
"password": "testpass123",
})
other_headers = {"Authorization": f"Bearer {resp.json()['access_token']}"}
resp = await client.get(f"/api/v1/memory/{entry_id}", headers=other_headers)
assert resp.status_code == 404