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
+ } ) ;
0 commit comments