Skip to main content
Glama
e2e.test.ts19.4 kB
/** * True integration tests that run against real services * * Prerequisites: * - Docker Compose services running (Qdrant, Embeddings) * - Run: docker compose -f docker-compose.test.yml up -d * * These tests verify the complete system works end-to-end */ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { unlinkSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; // Skip if services not available const QDRANT_URL = process.env.DOCLEA_QDRANT_URL || "http://localhost:6333"; const EMBEDDING_URL = process.env.DOCLEA_EMBEDDING_ENDPOINT || "http://localhost:8080"; async function checkService(url: string): Promise<boolean> { try { const response = await fetch(url, { method: "GET" }); return response.ok; } catch { return false; } } async function servicesAvailable(): Promise<boolean> { const [qdrant, embeddings] = await Promise.all([ checkService(`${QDRANT_URL}/readyz`), checkService(`${EMBEDDING_URL}/health`), ]); return qdrant && embeddings; } // Generate UUID for Qdrant point IDs function generateUUID(): string { return crypto.randomUUID(); } describe("E2E Integration Tests", () => { const TEST_DB_PATH = join(tmpdir(), `doclea-test-${Date.now()}.db`); const TEST_COLLECTION = `doclea_test_${Date.now()}`; let skipTests = false; let collectionCreated = false; beforeAll(async () => { skipTests = !(await servicesAvailable()); if (skipTests) { console.warn("⚠️ Skipping E2E tests: Docker services not available"); console.warn(" Run: docker compose -f docker-compose.test.yml up -d"); return; } // Create test collection upfront const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vectors: { size: 384, distance: "Cosine", }, }), }, ); collectionCreated = response.ok; }); afterAll(async () => { // Cleanup test database try { unlinkSync(TEST_DB_PATH); } catch { // Ignore if file doesn't exist } // Cleanup Qdrant collection if (!skipTests && collectionCreated) { try { await fetch(`${QDRANT_URL}/collections/${TEST_COLLECTION}`, { method: "DELETE", }); } catch { // Ignore cleanup errors } } }); describe("service health", () => { test("qdrant is reachable", async () => { if (skipTests) return; const response = await fetch(`${QDRANT_URL}/readyz`); expect(response.ok).toBe(true); }); test("embeddings service is reachable", async () => { if (skipTests) return; const response = await fetch(`${EMBEDDING_URL}/health`); expect(response.ok).toBe(true); }); test("test collection was created", async () => { if (skipTests) return; expect(collectionCreated).toBe(true); }); }); describe("embeddings generation", () => { test("generates embeddings for text", async () => { if (skipTests) return; const response = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: "Test embedding generation", }), }); expect(response.ok).toBe(true); const data = await response.json(); expect(Array.isArray(data)).toBe(true); expect(data[0]).toHaveLength(384); }); test("generates embeddings for batch", async () => { if (skipTests) return; const response = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: ["First text", "Second text", "Third text"], }), }); expect(response.ok).toBe(true); const data = await response.json(); expect(data).toHaveLength(3); expect(data[0]).toHaveLength(384); expect(data[1]).toHaveLength(384); expect(data[2]).toHaveLength(384); }); test("embeddings are normalized", async () => { if (skipTests) return; const response = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: "Normalized vector test", }), }); const data = await response.json(); const vector = data[0]; // Check L2 norm is approximately 1 const norm = Math.sqrt( vector.reduce((sum: number, v: number) => sum + v * v, 0), ); expect(norm).toBeCloseTo(1, 1); }); }); describe("vector store operations", () => { const pointId = generateUUID(); test("can upsert point with vector", async () => { if (skipTests) return; // Generate embedding const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: "Test memory content" }), }); const embeddings = await embedResponse.json(); // Upsert to Qdrant const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [ { id: pointId, vector: embeddings[0], payload: { memoryId: "mem_test123", type: "decision", title: "Test Decision", }, }, ], }), }, ); expect(response.ok).toBe(true); }); test("can search by vector similarity", async () => { if (skipTests) return; // Generate query embedding const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: "memory content" }), }); const embeddings = await embedResponse.json(); // Search const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points/search`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vector: embeddings[0], limit: 10, with_payload: true, }), }, ); expect(response.ok).toBe(true); const results = await response.json(); expect(results.result.length).toBeGreaterThan(0); expect(results.result[0].payload.memoryId).toBe("mem_test123"); }); test("can filter search by payload", async () => { if (skipTests) return; // Generate query embedding const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: "test" }), }); const embeddings = await embedResponse.json(); // Search with filter const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points/search`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vector: embeddings[0], limit: 10, filter: { must: [{ key: "type", match: { value: "decision" } }], }, with_payload: true, }), }, ); expect(response.ok).toBe(true); const results = await response.json(); for (const result of results.result) { expect(result.payload.type).toBe("decision"); } }); test("can delete point", async () => { if (skipTests) return; const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points/delete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [pointId], }), }, ); expect(response.ok).toBe(true); }); }); describe("semantic similarity", () => { test("similar texts have high similarity", async () => { if (skipTests) return; const response = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: [ "We decided to use PostgreSQL for the database", "The team chose PostgreSQL as our database solution", ], }), }); const embeddings = await response.json(); const similarity = cosineSimilarity(embeddings[0], embeddings[1]); expect(similarity).toBeGreaterThan(0.7); }); test("different texts have lower similarity", async () => { if (skipTests) return; const response = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: [ "We decided to use PostgreSQL for the database", "The weather is nice today for a walk in the park", ], }), }); const embeddings = await response.json(); const similarity = cosineSimilarity(embeddings[0], embeddings[1]); // Relaxed threshold - embeddings can find unexpected similarities expect(similarity).toBeLessThan(0.7); }); test("code-related queries find code memories", async () => { if (skipTests) return; const codeMemory = "Implemented retry logic with exponential backoff"; const nonCodeMemory = "Team meeting scheduled for Monday"; const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: [codeMemory, nonCodeMemory, "retry mechanism implementation"], }), }); const embeddings = await embedResponse.json(); const codeSimiliarity = cosineSimilarity(embeddings[2], embeddings[0]); const nonCodeSimilarity = cosineSimilarity(embeddings[2], embeddings[1]); expect(codeSimiliarity).toBeGreaterThan(nonCodeSimilarity); }); }); describe("full memory workflow", () => { const memoryId = `mem_${Date.now()}`; const qdrantId = generateUUID(); test("can store memory with embedding", async () => { if (skipTests) return; const content = "We decided to use Bun for faster builds and tests"; const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: content }), }); const embeddings = await embedResponse.json(); const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [ { id: qdrantId, vector: embeddings[0], payload: { memoryId, type: "decision", title: "Use Bun Runtime", importance: 0.8, tags: ["runtime", "performance"], }, }, ], }), }, ); expect(response.ok).toBe(true); }); test("can search and find stored memory", async () => { if (skipTests) return; const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: "runtime choice for performance" }), }); const embeddings = await embedResponse.json(); const searchResponse = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points/search`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vector: embeddings[0], limit: 5, with_payload: true, }), }, ); expect(searchResponse.ok).toBe(true); const results = await searchResponse.json(); const found = results.result.find( (r: { payload: { memoryId: string } }) => r.payload.memoryId === memoryId, ); expect(found).toBeDefined(); expect(found.payload.title).toBe("Use Bun Runtime"); }); test("can update memory vector", async () => { if (skipTests) return; const newContent = "Updated: We chose Bun for TypeScript-first development"; const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: newContent }), }); const embeddings = await embedResponse.json(); const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [ { id: qdrantId, vector: embeddings[0], payload: { memoryId, type: "decision", title: "Use Bun Runtime (Updated)", importance: 0.9, tags: ["runtime", "performance", "typescript"], }, }, ], }), }, ); expect(response.ok).toBe(true); }); test("can delete memory from vector store", async () => { if (skipTests) return; const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points/delete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [qdrantId], }), }, ); expect(response.ok).toBe(true); }); }); describe("batch operations", () => { const batchIds = Array.from({ length: 5 }, (_, i) => ({ memoryId: `mem_batch_${i}`, qdrantId: generateUUID(), })); test("can batch upsert multiple memories", async () => { if (skipTests) return; const contents = [ "Authentication uses JWT tokens", "Database migrations with Prisma", "API rate limiting at 100 req/min", "Caching with Redis for sessions", "Error handling with Result types", ]; const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: contents }), }); const embeddings = await embedResponse.json(); const points = batchIds.map((ids, i) => ({ id: ids.qdrantId, vector: embeddings[i], payload: { memoryId: ids.memoryId, type: "decision", title: contents[i].slice(0, 30), }, })); const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points }), }, ); expect(response.ok).toBe(true); }); test("can search across batch memories", async () => { if (skipTests) return; const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: "database schema migrations" }), }); const embeddings = await embedResponse.json(); const searchResponse = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points/search`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vector: embeddings[0], limit: 10, with_payload: true, }), }, ); expect(searchResponse.ok).toBe(true); const results = await searchResponse.json(); expect(results.result.length).toBeGreaterThan(0); }); test("can batch delete memories", async () => { if (skipTests) return; const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points/delete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: batchIds.map((ids) => ids.qdrantId), }), }, ); expect(response.ok).toBe(true); }); }); describe("edge cases", () => { test("handles unicode text", async () => { if (skipTests) return; const unicodeText = "决定使用 PostgreSQL 数据库"; const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: unicodeText }), }); expect(embedResponse.ok).toBe(true); const embeddings = await embedResponse.json(); expect(embeddings[0]).toHaveLength(384); }); test("handles special characters in payload", async () => { if (skipTests) return; const specialId = generateUUID(); const embedResponse = await fetch(`${EMBEDDING_URL}/embed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputs: "test content" }), }); const embeddings = await embedResponse.json(); const response = await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [ { id: specialId, vector: embeddings[0], payload: { title: 'Special "chars" & <tags>', path: "/path/to/file.ts", regex: "^[a-z]+$", }, }, ], }), }, ); expect(response.ok).toBe(true); // Cleanup await fetch( `${QDRANT_URL}/collections/${TEST_COLLECTION}/points/delete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [specialId] }), }, ); }); }); }); // Helper function for cosine similarity function cosineSimilarity(a: number[], b: number[]): number { let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docleaai/doclea-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server