Skip to content

Commit 1cf976e

Browse files
committed
test: add tests for InMemoryEventStore
1 parent d3ef39b commit 1cf976e

File tree

2 files changed

+161
-1
lines changed

2 files changed

+161
-1
lines changed

src/InMemoryEventStore.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
2+
3+
import { describe, expect, it, vi } from "vitest";
4+
5+
import { InMemoryEventStore } from "./InMemoryEventStore.js";
6+
7+
describe("InMemoryEventStore", () => {
8+
it("stores events and replays them after a specific event ID", async () => {
9+
const store = new InMemoryEventStore();
10+
const streamId = "test-stream-123";
11+
12+
// Create test messages
13+
const messages: JSONRPCMessage[] = [
14+
{ id: 1, jsonrpc: "2.0", method: "initialize" },
15+
{ id: 2, jsonrpc: "2.0", method: "tools/list" },
16+
{ id: 3, jsonrpc: "2.0", method: "tools/call", params: { name: "test" } },
17+
{ id: 3, jsonrpc: "2.0", result: { success: true } },
18+
{ id: 4, jsonrpc: "2.0", method: "shutdown" },
19+
];
20+
21+
// Store all events and keep track of event IDs
22+
// Add small delays to ensure different timestamps for proper ordering
23+
const eventIds: string[] = [];
24+
for (const message of messages) {
25+
const eventId = await store.storeEvent(streamId, message);
26+
expect(eventId).toContain(streamId);
27+
eventIds.push(eventId);
28+
// Small delay to ensure different timestamps
29+
await new Promise(resolve => setTimeout(resolve, 1));
30+
}
31+
32+
// Test replaying events after the second event
33+
const replayedEvents: Array<{ eventId: string; message: JSONRPCMessage }> = [];
34+
const sendMock = vi.fn(async (eventId: string, message: JSONRPCMessage) => {
35+
replayedEvents.push({ eventId, message });
36+
});
37+
38+
const returnedStreamId = await store.replayEventsAfter(
39+
eventIds[1], // Replay after the second event
40+
{ send: sendMock }
41+
);
42+
43+
// Verify the correct stream ID was returned
44+
expect(returnedStreamId).toBe(streamId);
45+
46+
// Verify that events 3, 4, and 5 were replayed (after event 2)
47+
expect(replayedEvents).toHaveLength(3);
48+
expect(sendMock).toHaveBeenCalledTimes(3);
49+
50+
// Verify the replayed messages are correct and in order
51+
expect(replayedEvents[0].message).toEqual(messages[2]);
52+
expect(replayedEvents[1].message).toEqual(messages[3]);
53+
expect(replayedEvents[2].message).toEqual(messages[4]);
54+
55+
// Verify event IDs are preserved
56+
expect(replayedEvents[0].eventId).toBe(eventIds[2]);
57+
expect(replayedEvents[1].eventId).toBe(eventIds[3]);
58+
expect(replayedEvents[2].eventId).toBe(eventIds[4]);
59+
});
60+
61+
it("isolates events by stream ID and only replays events from the same stream", async () => {
62+
const store = new InMemoryEventStore();
63+
const streamId1 = "stream-alpha";
64+
const streamId2 = "stream-beta";
65+
66+
// Create messages for two different streams
67+
const stream1Messages: JSONRPCMessage[] = [
68+
{ id: 1, jsonrpc: "2.0", method: "stream1.init" },
69+
{ id: 2, jsonrpc: "2.0", method: "stream1.process" },
70+
{ id: 3, jsonrpc: "2.0", method: "stream1.complete" },
71+
];
72+
73+
const stream2Messages: JSONRPCMessage[] = [
74+
{ id: 10, jsonrpc: "2.0", method: "stream2.init" },
75+
{ id: 20, jsonrpc: "2.0", method: "stream2.process" },
76+
{ id: 30, jsonrpc: "2.0", method: "stream2.complete" },
77+
];
78+
79+
// Interleave storing events from both streams with small delays
80+
const stream1EventIds: string[] = [];
81+
const stream2EventIds: string[] = [];
82+
83+
// Store first event from each stream
84+
stream1EventIds.push(await store.storeEvent(streamId1, stream1Messages[0]));
85+
await new Promise(resolve => setTimeout(resolve, 1));
86+
stream2EventIds.push(await store.storeEvent(streamId2, stream2Messages[0]));
87+
await new Promise(resolve => setTimeout(resolve, 1));
88+
89+
// Store second event from each stream
90+
stream1EventIds.push(await store.storeEvent(streamId1, stream1Messages[1]));
91+
await new Promise(resolve => setTimeout(resolve, 1));
92+
stream2EventIds.push(await store.storeEvent(streamId2, stream2Messages[1]));
93+
await new Promise(resolve => setTimeout(resolve, 1));
94+
95+
// Store third event from each stream
96+
stream1EventIds.push(await store.storeEvent(streamId1, stream1Messages[2]));
97+
await new Promise(resolve => setTimeout(resolve, 1));
98+
stream2EventIds.push(await store.storeEvent(streamId2, stream2Messages[2]));
99+
100+
// Replay events from stream 1 after its first event
101+
const stream1ReplayedEvents: Array<{ eventId: string; message: JSONRPCMessage }> = [];
102+
const stream1SendMock = vi.fn(async (eventId: string, message: JSONRPCMessage) => {
103+
stream1ReplayedEvents.push({ eventId, message });
104+
});
105+
106+
const returnedStreamId1 = await store.replayEventsAfter(
107+
stream1EventIds[0],
108+
{ send: stream1SendMock }
109+
);
110+
111+
// Verify only stream 1 events were replayed
112+
expect(returnedStreamId1).toBe(streamId1);
113+
expect(stream1ReplayedEvents).toHaveLength(2);
114+
expect(stream1ReplayedEvents[0].message).toEqual(stream1Messages[1]);
115+
expect(stream1ReplayedEvents[1].message).toEqual(stream1Messages[2]);
116+
117+
// Verify no stream 2 events were included
118+
for (const event of stream1ReplayedEvents) {
119+
expect(event.eventId).toContain(streamId1);
120+
expect(event.eventId).not.toContain(streamId2);
121+
}
122+
123+
// Now replay events from stream 2 after its first event
124+
const stream2ReplayedEvents: Array<{ eventId: string; message: JSONRPCMessage }> = [];
125+
const stream2SendMock = vi.fn(async (eventId: string, message: JSONRPCMessage) => {
126+
stream2ReplayedEvents.push({ eventId, message });
127+
});
128+
129+
const returnedStreamId2 = await store.replayEventsAfter(
130+
stream2EventIds[0],
131+
{ send: stream2SendMock }
132+
);
133+
134+
// Verify only stream 2 events were replayed
135+
expect(returnedStreamId2).toBe(streamId2);
136+
expect(stream2ReplayedEvents).toHaveLength(2);
137+
expect(stream2ReplayedEvents[0].message).toEqual(stream2Messages[1]);
138+
expect(stream2ReplayedEvents[1].message).toEqual(stream2Messages[2]);
139+
140+
// Verify no stream 1 events were included
141+
for (const event of stream2ReplayedEvents) {
142+
expect(event.eventId).toContain(streamId2);
143+
expect(event.eventId).not.toContain(streamId1);
144+
}
145+
146+
// Test edge case: replay with non-existent event ID returns empty string
147+
const invalidResult = await store.replayEventsAfter(
148+
"non-existent-event-id",
149+
{ send: vi.fn() }
150+
);
151+
expect(invalidResult).toBe("");
152+
153+
// Test edge case: replay with empty event ID returns empty string
154+
const emptyResult = await store.replayEventsAfter(
155+
"",
156+
{ send: vi.fn() }
157+
);
158+
expect(emptyResult).toBe("");
159+
});
160+
});

src/InMemoryEventStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* This is a copy of the InMemoryEventStore from the typescript-sdk
3-
* https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/inMemoryEventStore.ts
3+
* https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/examples/shared/inMemoryEventStore.ts
44
*/
55

66
import type { EventStore } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

0 commit comments

Comments
 (0)