2cab53bc94
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers CLI - Convert documentation to AI skills dockerfile:Dockerfile name:skill-seekers]) (push) Waiting to run
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers MCP Server - 25 tools for AI assistants dockerfile:Dockerfile.mcp name:skill-seekers-mcp]) (push) Waiting to run
Docker Publish / Test Docker Images (push) Blocked by required conditions
Test Vector Database Adaptors / Test chroma Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test faiss Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test qdrant Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test weaviate Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test MCP Vector DB Tools (push) Waiting to run
Tests / Code Quality (Ruff & Mypy) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.12) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.10) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.12) (push) Waiting to run
Tests / Tests (push) Blocked by required conditions
Tests / Serial / Integration / E2E Tests (push) Blocked by required conditions
Tests / MCP Server Tests (push) Blocked by required conditions
198 lines
6.5 KiB
Python
198 lines
6.5 KiB
Python
"""Tests for the FastAPI embedding server (embedding/server.py).
|
|
|
|
Uses starlette TestClient for in-process HTTP testing.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch
|
|
|
|
try:
|
|
from starlette.testclient import TestClient
|
|
from skill_seekers.embedding.server import app as _embedding_app
|
|
|
|
STARLETTE_AVAILABLE = True
|
|
except (ImportError, SystemExit):
|
|
STARLETTE_AVAILABLE = False
|
|
|
|
pytestmark = pytest.mark.skipif(not STARLETTE_AVAILABLE, reason="Starlette not installed")
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_generator():
|
|
with patch("skill_seekers.embedding.server.generator") as mock_gen:
|
|
mock_gen.list_models.return_value = [
|
|
{
|
|
"name": "text-embedding-3-small",
|
|
"provider": "openai",
|
|
"dimensions": 1536,
|
|
"max_tokens": 8191,
|
|
},
|
|
{
|
|
"name": "text-embedding-3-large",
|
|
"provider": "openai",
|
|
"dimensions": 3072,
|
|
"max_tokens": 8191,
|
|
},
|
|
]
|
|
mock_gen.generate.return_value = [0.1, 0.2, 0.3]
|
|
mock_gen.generate_batch.return_value = ([[0.1, 0.2], [0.3, 0.4]], 2)
|
|
mock_gen.compute_hash.return_value = "mock_hash_abc123"
|
|
yield mock_gen
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_cache():
|
|
with patch("skill_seekers.embedding.server.cache") as mock_cache:
|
|
mock_cache.has.return_value = False
|
|
mock_cache.get.return_value = None
|
|
mock_cache.size.return_value = 42
|
|
mock_cache.stats.return_value = {
|
|
"total": 42,
|
|
"by_model": {"text-embedding-3-small": 42},
|
|
"top_accessed": [],
|
|
"expired": 0,
|
|
"ttl_days": 30,
|
|
}
|
|
mock_cache.clear.return_value = 5
|
|
mock_cache.clear_expired.return_value = 3
|
|
yield mock_cache
|
|
|
|
|
|
@pytest.fixture
|
|
def client(mock_generator, mock_cache): # noqa: ARG001
|
|
with TestClient(_embedding_app) as c:
|
|
yield c
|
|
|
|
|
|
class TestRoot:
|
|
def test_root_endpoint(self, client):
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["service"] == "Skill Seekers Embedding API"
|
|
assert data["version"] == "1.0.0"
|
|
assert "/docs" in data["docs"]
|
|
assert "/health" in data["health"]
|
|
|
|
|
|
class TestHealth:
|
|
def test_health_endpoint(self, client):
|
|
response = client.get("/health")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "ok"
|
|
assert data["version"] == "1.0.0"
|
|
assert "models" in data
|
|
assert data["cache_enabled"] is True
|
|
assert data["cache_size"] == 42
|
|
|
|
|
|
class TestModels:
|
|
def test_list_models(self, client):
|
|
response = client.get("/models")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 2
|
|
assert len(data["models"]) == 2
|
|
assert data["models"][0]["name"] == "text-embedding-3-small"
|
|
assert data["models"][0]["provider"] == "openai"
|
|
assert data["models"][0]["dimensions"] == 1536
|
|
|
|
|
|
class TestEmbedText:
|
|
def test_embed_single_text(self, client):
|
|
response = client.post(
|
|
"/embed", json={"text": "Hello world", "model": "text-embedding-3-small"}
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["model"] == "text-embedding-3-small"
|
|
assert len(data["embedding"]) == 3
|
|
assert data["cached"] is False
|
|
|
|
def test_embed_cached(self, client, mock_cache, mock_generator):
|
|
mock_cache.has.return_value = True
|
|
mock_cache.get.return_value = [0.5, 0.6, 0.7]
|
|
|
|
response = client.post(
|
|
"/embed", json={"text": "cached text", "model": "text-embedding-3-small"}
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["cached"] is True
|
|
assert data["embedding"] == [0.5, 0.6, 0.7]
|
|
|
|
def test_embed_with_normalize(self, client):
|
|
response = client.post("/embed", json={"text": "test", "normalize": False})
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestEmbedBatch:
|
|
def test_embed_batch(self, client):
|
|
response = client.post("/embed/batch", json={"texts": ["text1", "text2"]})
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 2
|
|
assert data["dimensions"] == 2
|
|
assert len(data["embeddings"]) == 2
|
|
|
|
def test_embed_batch_empty(self, client, mock_generator):
|
|
mock_generator.generate_batch.return_value = ([[0.1]], 1)
|
|
|
|
response = client.post("/embed/batch", json={"texts": ["one"]})
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestEmbedSkill:
|
|
def test_embed_skill(self, client, tmp_path):
|
|
skill_dir = tmp_path / "test-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"# Test Skill\n\nThis is a test skill with enough content\n" * 5
|
|
)
|
|
|
|
response = client.post("/embed/skill", json={"skill_path": str(skill_dir)})
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["skill_name"] == "test-skill"
|
|
assert data["model"] == "text-embedding-3-small"
|
|
|
|
def test_embed_skill_not_found(self, client):
|
|
response = client.post("/embed/skill", json={"skill_path": "/nonexistent/path"})
|
|
assert response.status_code == 404
|
|
|
|
def test_embed_skill_no_skill_md(self, client, tmp_path):
|
|
skill_dir = tmp_path / "empty-skill"
|
|
skill_dir.mkdir()
|
|
|
|
response = client.post("/embed/skill", json={"skill_path": str(skill_dir)})
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestCacheEndpoints:
|
|
def test_cache_stats(self, client):
|
|
response = client.get("/cache/stats")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 42
|
|
|
|
def test_clear_cache_all(self, client):
|
|
response = client.post("/cache/clear")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "ok"
|
|
assert data["deleted"] == 5
|
|
|
|
def test_clear_cache_by_model(self, client):
|
|
response = client.post("/cache/clear?model=text-embedding-3-small")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["model"] == "text-embedding-3-small"
|
|
|
|
def test_clear_expired(self, client):
|
|
response = client.post("/cache/clear-expired")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "ok"
|
|
assert data["deleted"] == 3
|