Skip to main content
Glama
session-manager.test.ts10.5 kB
/** * Tests for SessionManager * Ported from Python Phase 1C session_manager_test.py */ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; import { SessionManager } from "./session-manager.js"; import { createSession } from "../types/session.js"; // Mock the Redis and D1 clients vi.mock("./redis-client.js"); vi.mock("./d1-client.js"); import { RedisClient } from "./redis-client.js"; import { D1Client } from "./d1-client.js"; describe("SessionManager", () => { let redisClient: RedisClient; let d1Client: D1Client; let sessionManager: SessionManager; beforeEach(() => { // Create mock instances redisClient = new RedisClient(); d1Client = new D1Client({} as any); // Mock D1Database // Mock Redis methods vi.spyOn(redisClient, "get").mockResolvedValue(null); vi.spyOn(redisClient, "set").mockResolvedValue(true); vi.spyOn(redisClient, "delete").mockResolvedValue(true); vi.spyOn(redisClient, "exists").mockResolvedValue(false); vi.spyOn(redisClient, "ttl").mockResolvedValue(-1); // Mock D1 methods vi.spyOn(d1Client, "query").mockResolvedValue([]); vi.spyOn(d1Client, "queryOne").mockResolvedValue(null); vi.spyOn(d1Client, "execute").mockResolvedValue(1); sessionManager = new SessionManager(redisClient, d1Client); }); afterEach(() => { vi.clearAllMocks(); }); describe("createSession", () => { it("should create a new session successfully", async () => { const userId = "user123"; const adapterType = "discord"; const result = await sessionManager.create(userId, adapterType); expect(result).toBeDefined(); expect(result.userId).toBe(userId); expect(result.adapterType).toBe(adapterType); expect(result.version).toBe(1); }); it("should store session in Redis cache", async () => { const userId = "user123"; const adapterType = "discord"; await sessionManager.create(userId, adapterType); expect(redisClient.set).toHaveBeenCalled(); }); it("should store session in D1 database", async () => { const userId = "user123"; const adapterType = "discord"; await sessionManager.create(userId, adapterType); expect(d1Client.execute).toHaveBeenCalled(); }); it("should handle Redis failure gracefully", async () => { vi.spyOn(redisClient, "set").mockResolvedValue(false); const result = await sessionManager.create("user123", "discord"); expect(result).toBeDefined(); // Should still succeed with D1 }); it("should handle D1 failure", async () => { vi.spyOn(d1Client, "execute").mockRejectedValue(new Error("DB error")); // D1 write is non-blocking (fire-and-forget), so create should still succeed // The error is logged but doesn't throw const result = await sessionManager.create("user123", "discord"); expect(result).toBeDefined(); expect(result.userId).toBe("user123"); }); }); describe("getSession", () => { it("should return session from Redis cache if available", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockResolvedValue(session); const result = await sessionManager.get(session.sessionId); expect(result).toEqual(session); expect(redisClient.get).toHaveBeenCalledWith(`session:${session.sessionId}`); expect(d1Client.queryOne).not.toHaveBeenCalled(); }); it("should fetch from D1 if not in Redis cache", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockResolvedValue(null); vi.spyOn(d1Client, "queryOne").mockResolvedValue({ session_id: session.sessionId, user_id: session.userId, adapter_type: session.adapterType, created_at: session.createdAt, last_active: session.lastActive, metadata: JSON.stringify(session.metadata), version: session.version, }); const result = await sessionManager.get(session.sessionId); expect(result).toBeDefined(); expect(result?.sessionId).toBe(session.sessionId); expect(d1Client.queryOne).toHaveBeenCalled(); }); it("should cache D1 result in Redis", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockResolvedValue(null); vi.spyOn(d1Client, "queryOne").mockResolvedValue({ session_id: session.sessionId, user_id: session.userId, adapter_type: session.adapterType, created_at: session.createdAt, last_active: session.lastActive, metadata: JSON.stringify(session.metadata), version: session.version, }); await sessionManager.get(session.sessionId); expect(redisClient.set).toHaveBeenCalled(); }); it("should return null for non-existent session", async () => { vi.spyOn(redisClient, "get").mockResolvedValue(null); vi.spyOn(d1Client, "queryOne").mockResolvedValue(null); const result = await sessionManager.get("nonexistent"); expect(result).toBeNull(); }); it("should handle Redis failure and fall back to D1", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockRejectedValue(new Error("Redis down")); vi.spyOn(d1Client, "queryOne").mockResolvedValue({ session_id: session.sessionId, user_id: session.userId, adapter_type: session.adapterType, created_at: session.createdAt, last_active: session.lastActive, metadata: JSON.stringify(session.metadata), version: session.version, }); const result = await sessionManager.get(session.sessionId); expect(result).toBeDefined(); }); }); describe("updateSession", () => { it("should update session successfully", async () => { const session = createSession("user123", "discord"); const updates = { metadata: { ...session.metadata, clientInfo: { key: "value" }, }, }; vi.spyOn(redisClient, "get").mockResolvedValue(session); vi.spyOn(d1Client, "execute").mockResolvedValue(1); const result = await sessionManager.update(session.sessionId, updates, session.version); expect(result).toBe(true); expect(d1Client.execute).toHaveBeenCalled(); }); it("should handle version conflicts", async () => { const session = createSession("user123", "discord"); // Mock get to return a session with different version const sessionWithNewVersion = { ...session, version: session.version + 1 }; vi.spyOn(redisClient, "get").mockResolvedValue(sessionWithNewVersion); const result = await sessionManager.update(session.sessionId, {}, session.version); expect(result).toBe(false); }); it("should update Redis cache after successful D1 update", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockResolvedValue(session); vi.spyOn(d1Client, "execute").mockResolvedValue(1); await sessionManager.update( session.sessionId, { metadata: { ...session.metadata, clientInfo: { updated: true }, }, }, session.version ); expect(redisClient.set).toHaveBeenCalled(); }); it("should handle D1 update failure", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockResolvedValue(session); // D1 write is non-blocking, so update should still succeed vi.spyOn(d1Client, "execute").mockRejectedValue(new Error("Update failed")); const result = await sessionManager.update(session.sessionId, {}, session.version); expect(result).toBe(true); // Redis update succeeds, D1 failure is non-blocking }); }); describe("endSession", () => { it("should end session successfully", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockResolvedValue(session); vi.spyOn(d1Client, "execute").mockResolvedValue(1); const result = await sessionManager.end(session.sessionId); expect(result).toBe(true); expect(d1Client.execute).toHaveBeenCalled(); }); it("should remove session from Redis cache", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockResolvedValue(session); vi.spyOn(d1Client, "execute").mockResolvedValue(1); await sessionManager.end(session.sessionId); // end() updates the session in Redis (sets with shorter TTL), doesn't delete expect(redisClient.set).toHaveBeenCalled(); }); it("should handle non-existent session", async () => { vi.spyOn(redisClient, "get").mockResolvedValue(null); const result = await sessionManager.end("nonexistent"); expect(result).toBe(false); }); it("returns false when Redis set fails", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockResolvedValue(session); vi.spyOn(redisClient, "set").mockResolvedValueOnce(false); const result = await sessionManager.end(session.sessionId); expect(result).toBe(false); }); it("returns false when D1 update fails", async () => { const session = createSession("user123", "discord"); vi.spyOn(redisClient, "get").mockResolvedValue(session); vi.spyOn(d1Client, "execute").mockRejectedValueOnce(new Error("D1 down")); const result = await sessionManager.end(session.sessionId); expect(result).toBe(false); }); }); describe("exists", () => { it("returns false when Redis key missing", async () => { vi.spyOn(redisClient, "exists").mockResolvedValue(false); const exists = await sessionManager.exists("missing"); expect(exists).toBe(false); }); it("returns true when Redis key exists", async () => { vi.spyOn(redisClient, "exists").mockResolvedValue(true); const session = createSession("user123", "discord"); const result = await sessionManager.exists(session.sessionId); expect(result).toBe(true); }); }); // Note: listSessions and cleanupExpiredSessions methods don't exist in current implementation });

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/hummbl-dev/mcp-server'

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