@@ -17,6 +17,100 @@ import {
1717} from "./session-mcp-types.ts" ;
1818import { RedisClient } from "./redis-client.ts" ;
1919
20+ type RedisEvent = "close" | "end" | "error" | "ready" ;
21+
22+ class DoctorRedisRuntime {
23+ private readonly listeners = new Map <
24+ RedisEvent ,
25+ Set < ( ...args : unknown [ ] ) => void >
26+ > ( ) ;
27+
28+ connect ( ) : Promise < void > {
29+ this . emit ( "ready" ) ;
30+ return Promise . resolve ( ) ;
31+ }
32+
33+ ping ( ) : Promise < "PONG" > {
34+ return Promise . resolve ( "PONG" ) ;
35+ }
36+
37+ quit ( ) : Promise < "OK" > {
38+ return Promise . resolve ( "OK" ) ;
39+ }
40+
41+ lpush ( ) : Promise < number > {
42+ return Promise . resolve ( 0 ) ;
43+ }
44+
45+ rpush ( ) : Promise < number > {
46+ return Promise . resolve ( 0 ) ;
47+ }
48+
49+ lmove ( ) : Promise < string | null > {
50+ return Promise . resolve ( null ) ;
51+ }
52+
53+ lrange ( ) : Promise < string [ ] > {
54+ return Promise . resolve ( [ ] ) ;
55+ }
56+
57+ llen ( ) : Promise < number > {
58+ return Promise . resolve ( 0 ) ;
59+ }
60+
61+ ltrim ( ) : Promise < void > {
62+ return Promise . resolve ( ) ;
63+ }
64+
65+ lindex ( ) : Promise < string | null > {
66+ return Promise . resolve ( null ) ;
67+ }
68+
69+ lset ( ) : Promise < void > {
70+ return Promise . resolve ( ) ;
71+ }
72+
73+ get ( ) : Promise < string | null > {
74+ return Promise . resolve ( null ) ;
75+ }
76+
77+ set ( ) : Promise < "OK" > {
78+ return Promise . resolve ( "OK" ) ;
79+ }
80+
81+ expire ( ) : Promise < number > {
82+ return Promise . resolve ( 1 ) ;
83+ }
84+
85+ del ( ) : Promise < number > {
86+ return Promise . resolve ( 0 ) ;
87+ }
88+
89+ hset ( ) : Promise < number > {
90+ return Promise . resolve ( 0 ) ;
91+ }
92+
93+ hgetall ( ) : Promise < Record < string , string > > {
94+ return Promise . resolve ( { } ) ;
95+ }
96+
97+ on ( event : RedisEvent , listener : ( ...args : unknown [ ] ) => void ) : void {
98+ const listeners = this . listeners . get ( event ) ?? new Set ( ) ;
99+ listeners . add ( listener ) ;
100+ this . listeners . set ( event , listeners ) ;
101+ }
102+
103+ off ( event : RedisEvent , listener : ( ...args : unknown [ ] ) => void ) : void {
104+ this . listeners . get ( event ) ?. delete ( listener ) ;
105+ }
106+
107+ private emit ( event : RedisEvent , ...args : unknown [ ] ) : void {
108+ for ( const listener of this . listeners . get ( event ) ?? [ ] ) {
109+ listener ( ...args ) ;
110+ }
111+ }
112+ }
113+
20114const textEncoder = new TextEncoder ( ) ;
21115
22116const toolContext = {
@@ -106,6 +200,106 @@ describe("session-mcp-runtime", () => {
106200 }
107201 } ) ;
108202
203+ it ( "reports live redis health in session_doctor when a redis client is provided" , async ( ) => {
204+ const degradedRedis = new RedisClient ( { endpoint : "redis://unused" } ) ;
205+ const degradedRuntime = createSessionMcpRuntime ( {
206+ redisClient : degradedRedis ,
207+ sessionTtlSeconds : 60 ,
208+ } ) ;
209+ const connectedRedis = new RedisClient ( {
210+ endpoint : "redis://unused" ,
211+ runtimeFactory : ( ) => new DoctorRedisRuntime ( ) ,
212+ } ) ;
213+ const connectedRuntime = createSessionMcpRuntime ( {
214+ redisClient : connectedRedis ,
215+ sessionTtlSeconds : 60 ,
216+ } ) ;
217+
218+ try {
219+ const degradedSerialized = await degradedRuntime . tools . session_doctor
220+ . execute (
221+ validRequests . session_doctor ,
222+ toolContext ,
223+ ) ;
224+ const degraded = JSON . parse ( degradedSerialized ) ;
225+
226+ assertEquals ( degraded . runtime . status , "ok" ) ;
227+ assertEquals ( degraded . redis . status , "degraded" ) ;
228+
229+ await connectedRedis . connect ( ) ;
230+
231+ const connectedSerialized = await connectedRuntime . tools . session_doctor
232+ . execute (
233+ validRequests . session_doctor ,
234+ toolContext ,
235+ ) ;
236+ const connected = JSON . parse ( connectedSerialized ) ;
237+
238+ assertEquals ( connected . runtime . status , "ok" ) ;
239+ assertEquals ( connected . redis . status , "ok" ) ;
240+ assertEquals ( connected . graphiti_cache . status , "not_checked" ) ;
241+ } finally {
242+ await degradedRuntime . dispose ( ) ;
243+ await degradedRedis . close ( ) ;
244+ await connectedRuntime . dispose ( ) ;
245+ await connectedRedis . close ( ) ;
246+ }
247+ } ) ;
248+
249+ it ( "reports local graphiti cache health in session_doctor" , async ( ) => {
250+ const disconnectedRedis = new RedisClient ( { endpoint : "redis://unused" } ) ;
251+ const connectedRedis = new RedisClient ( {
252+ endpoint : "redis://unused" ,
253+ runtimeFactory : ( ) => new DoctorRedisRuntime ( ) ,
254+ } ) ;
255+
256+ const noCacheRuntime = createSessionMcpRuntime ( ) ;
257+ const degradedCacheRuntime = createSessionMcpRuntime ( {
258+ redisClient : disconnectedRedis ,
259+ sessionTtlSeconds : 60 ,
260+ graphitiCache : { } ,
261+ } ) ;
262+ const connectedCacheRuntime = createSessionMcpRuntime ( {
263+ redisClient : connectedRedis ,
264+ sessionTtlSeconds : 60 ,
265+ graphitiCache : { } ,
266+ } ) ;
267+
268+ try {
269+ const noCache = JSON . parse (
270+ await noCacheRuntime . tools . session_doctor . execute (
271+ validRequests . session_doctor ,
272+ toolContext ,
273+ ) ,
274+ ) ;
275+ assertEquals ( noCache . graphiti_cache . status , "not_checked" ) ;
276+
277+ const degradedCache = JSON . parse (
278+ await degradedCacheRuntime . tools . session_doctor . execute (
279+ validRequests . session_doctor ,
280+ toolContext ,
281+ ) ,
282+ ) ;
283+ assertEquals ( degradedCache . graphiti_cache . status , "degraded" ) ;
284+
285+ await connectedRedis . connect ( ) ;
286+
287+ const connectedCache = JSON . parse (
288+ await connectedCacheRuntime . tools . session_doctor . execute (
289+ validRequests . session_doctor ,
290+ toolContext ,
291+ ) ,
292+ ) ;
293+ assertEquals ( connectedCache . graphiti_cache . status , "ok" ) ;
294+ } finally {
295+ await noCacheRuntime . dispose ( ) ;
296+ await degradedCacheRuntime . dispose ( ) ;
297+ await connectedCacheRuntime . dispose ( ) ;
298+ await disconnectedRedis . close ( ) ;
299+ await connectedRedis . close ( ) ;
300+ }
301+ } ) ;
302+
109303 it ( "caps serialized responses to the exact 8 KB budget" , async ( ) => {
110304 const runtime = createSessionMcpRuntime ( ) ;
111305
0 commit comments