Skip to main content
Glama
usage-stats-sender.test.ts8.62 kB
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals"; import { noOpLogger } from "@mcpx/toolkit-core/logging"; import { UsageStatsSender, UsageStatsSocket } from "./usage-stats-sender.js"; import { WebappBoundPayloadOf } from "@mcpx/webapp-protocol/messages"; describe("UsageStatsSender", () => { let sender: UsageStatsSender; let mockSocket: UsageStatsSocket; let emittedMessages: Array<unknown> = []; let payloadGenerator: () => WebappBoundPayloadOf<"usage-stats">; const waitFor = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); type ToolsConfig = Record< string, { description?: string; isCustom: boolean; usage: { callCount: number; lastCalledAt?: string }; } >; const createPayload = ( agentCount: number, serverNames: string[], toolsPerServer?: ToolsConfig, ): WebappBoundPayloadOf<"usage-stats"> => ({ agents: Array.from({ length: agentCount }, (_, i) => ({ clientInfo: { name: `agent-${i}`, version: "1.0.0", }, })), targetServers: serverNames.map((name) => ({ name, status: "connected" as const, type: "stdio" as const, tools: toolsPerServer || {}, })), }); beforeEach(() => { emittedMessages = []; mockSocket = { emit: (_event, data) => { emittedMessages.push(data); }, }; payloadGenerator = () => createPayload(1, ["server1"]); }); afterEach(() => { sender?.stop(); }); it("should send immediately on start", () => { sender = new UsageStatsSender(noOpLogger, payloadGenerator, 1000); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); }); it("should send periodically at specified interval when payload changes", async () => { let counter = 0; payloadGenerator = () => createPayload(counter++, ["server1"]); sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); // Wait 120ms: interval at 50ms and 100ms should fire await waitFor(120); // Should have sent: initial + 2 intervals = 3 messages (or more if timing varies) expect(emittedMessages.length).toBeGreaterThanOrEqual(3); }); it("should not send if payload is unchanged", async () => { sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); await waitFor(120); // Should still be 1 because payload hasn't changed expect(emittedMessages).toHaveLength(1); }); it("should send when payload changes", async () => { let counter = 0; payloadGenerator = () => createPayload(counter, ["server1"]); sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); counter = 1; await waitFor(60); expect(emittedMessages).toHaveLength(2); counter = 2; await waitFor(60); expect(emittedMessages).toHaveLength(3); }); it("should treat different array orders as same payload", async () => { let serverNames = ["server1", "server2"]; payloadGenerator = () => createPayload(1, serverNames); sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); // Reverse order serverNames = ["server2", "server1"]; await waitFor(60); // Should not send again because content is the same, just different order expect(emittedMessages).toHaveLength(1); }); it("should stop interval on stop()", async () => { let counter = 0; payloadGenerator = () => createPayload(counter++, ["server1"]); sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); await waitFor(60); expect(emittedMessages).toHaveLength(2); sender.stop(); const countAfterStop = emittedMessages.length; await waitFor(100); // Should not have sent more after stop expect(emittedMessages).toHaveLength(countAfterStop); }); it("should reset hash on stop() so first send after restart always goes through", async () => { sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); sender.stop(); // Start again with same payload sender.start(mockSocket); // Should send again even though payload is same, because hash was reset expect(emittedMessages).toHaveLength(2); }); it("should restart cleanly when start() called multiple times", async () => { let counter = 0; payloadGenerator = () => createPayload(counter++, ["server1"]); sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); await waitFor(30); // Start again before interval fires sender.start(mockSocket); // Should have sent immediately on second start expect(emittedMessages).toHaveLength(2); await waitFor(60); // Should continue with new interval expect(emittedMessages.length).toBeGreaterThanOrEqual(3); }); describe("tool usage deduplication", () => { it("should send when tool callCount changes", async () => { const tools: ToolsConfig = { "read-file": { description: "Read a file", isCustom: false, usage: { callCount: 1 }, }, }; payloadGenerator = () => createPayload(1, ["server1"], tools); sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); // Increment callCount tools["read-file"]!.usage.callCount = 5; await waitFor(60); // Should send because callCount changed expect(emittedMessages).toHaveLength(2); }); it("should dedupe when tools are in different key order", async () => { let toolsOrder1: ToolsConfig = { "read-file": { description: "Read a file", isCustom: false, usage: { callCount: 1 }, }, "write-file": { description: "Write a file", isCustom: false, usage: { callCount: 2 }, }, }; payloadGenerator = () => createPayload(1, ["server1"], toolsOrder1); sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); // Create payload with tools in different order but same content toolsOrder1 = { "write-file": { description: "Write a file", isCustom: false, usage: { callCount: 2 }, }, "read-file": { description: "Read a file", isCustom: false, usage: { callCount: 1 }, }, }; await waitFor(60); // Should NOT send again - same tools, just different key order expect(emittedMessages).toHaveLength(1); }); it("should send when a new tool is added", async () => { const tools: ToolsConfig = { "read-file": { description: "Read a file", isCustom: false, usage: { callCount: 1 }, }, }; payloadGenerator = () => createPayload(1, ["server1"], tools); sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); // Add a new tool tools["write-file"] = { description: "Write a file", isCustom: false, usage: { callCount: 0 }, }; await waitFor(60); // Should send because new tool was added expect(emittedMessages).toHaveLength(2); }); it("should send when tool metadata changes", async () => { const tools: ToolsConfig = { "read-file": { description: "Read a file", isCustom: false, usage: { callCount: 1 }, }, }; payloadGenerator = () => createPayload(1, ["server1"], tools); sender = new UsageStatsSender(noOpLogger, payloadGenerator, 50); sender.start(mockSocket); expect(emittedMessages).toHaveLength(1); // Change description tools["read-file"]!.description = "Read a file from disk"; await waitFor(60); // Should send because tool description changed expect(emittedMessages).toHaveLength(2); }); }); });

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/TheLunarCompany/lunar'

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