11import { describe , it , expect , vi } from "vitest" ;
22import { DistributedStateMiddleware } from "../../../src/adapters/state/distributedMiddleware" ;
3- import { IMessagingDriver , MessagePayload } from "../../../src/infrastructure/messaging/interfaces" ;
3+ import { MessagePayload } from "../../../src/infrastructure/messaging/interfaces" ;
44
5- function makeMockDriver ( ) : IMessagingDriver {
6- return {
7- publish : vi . fn ( ) . mockResolvedValue ( undefined ) ,
8- subscribe : vi . fn ( ) . mockResolvedValue ( undefined ) ,
9- unsubscribe : vi . fn ( ) . mockResolvedValue ( undefined ) ,
10- disconnect : vi . fn ( ) . mockResolvedValue ( undefined ) ,
11- } ;
12- }
5+ const mockRedis = {
6+ publish : vi . fn ( ) . mockResolvedValue ( undefined ) ,
7+ subscribe : vi . fn ( ) ,
8+ quit : vi . fn ( ) . mockResolvedValue ( undefined ) ,
9+ on : vi . fn ( ) ,
10+ options : { } ,
11+ } ;
12+
13+ vi . mock ( 'ioredis' , ( ) => ( {
14+ Redis : vi . fn ( ) . mockImplementation ( function ( ) { return mockRedis ; } ) ,
15+ } ) ) ;
1316
1417interface MockStore {
1518 state : Record < string , unknown > ;
@@ -24,65 +27,68 @@ function makeStore(initial: Record<string, unknown> = {}): MockStore {
2427}
2528
2629describe ( "DistributedStateMiddleware" , ( ) => {
30+ beforeEach ( ( ) => {
31+ vi . clearAllMocks ( ) ;
32+ } ) ;
33+
2734 it ( "intercepts setState and publishes to the driver" , async ( ) => {
28- const mockDriver = makeMockDriver ( ) ;
29- const mw = new DistributedStateMiddleware ( mockDriver ) ;
35+ const mw = new DistributedStateMiddleware ( 'redis://localhost' ) ;
3036 const store = makeStore ( { count : 0 } ) ;
3137 mw . attach ( store ) ;
3238 await store . setState ( { count : 1 } ) ;
3339 expect ( store . state [ 'count' ] ) . toBe ( 1 ) ;
34- expect ( mockDriver . publish ) . toHaveBeenCalledWith ( "kaiban-state-events" , expect . objectContaining ( { data : { stateUpdate : { count : 1 } } } ) ) ;
40+ expect ( mockRedis . publish ) . toHaveBeenCalledWith (
41+ "kaiban-state-events" ,
42+ expect . stringContaining ( '"stateUpdate":{"count":1}' )
43+ ) ;
3544 } ) ;
3645
3746 it ( "sanitizeDelta strips PII keys" , async ( ) => {
38- const mockDriver = makeMockDriver ( ) ;
39- const mw = new DistributedStateMiddleware ( mockDriver ) ;
47+ const mw = new DistributedStateMiddleware ( 'redis://localhost' ) ;
4048 const store = makeStore ( ) ;
4149 mw . attach ( store ) ;
4250 await store . setState ( { count : 2 , email : "x@y.com" , token : "abc" , password : "pw" } ) ;
43- const call = ( mockDriver . publish as ReturnType < typeof vi . fn > ) . mock . calls [ 0 ] ;
44- const data = call [ 1 ] . data . stateUpdate as Record < string , unknown > ;
51+ const call = mockRedis . publish . mock . calls [ 0 ] ;
52+ const parsed = JSON . parse ( call [ 1 ] as string ) as MessagePayload ;
53+ const data = parsed . data [ 'stateUpdate' ] as Record < string , unknown > ;
4554 expect ( data [ 'count' ] ) . toBe ( 2 ) ;
4655 expect ( data [ 'email' ] ) . toBeUndefined ( ) ;
4756 expect ( data [ 'token' ] ) . toBeUndefined ( ) ;
4857 expect ( data [ 'password' ] ) . toBeUndefined ( ) ;
4958 } ) ;
5059
5160 it ( "sanitizeDelta handles null partial (covers null branch)" , async ( ) => {
52- const mockDriver = makeMockDriver ( ) ;
53- const mw = new DistributedStateMiddleware ( mockDriver ) ;
61+ const mw = new DistributedStateMiddleware ( 'redis://localhost' ) ;
5462 const store = makeStore ( ) ;
5563 mw . attach ( store ) ;
5664 // Trigger with null via coercion to cover `if (partial === null) return {}`
5765 await ( store . setState as ( p : unknown ) => Promise < void > ) ( null ) ;
58- const call = ( mockDriver . publish as ReturnType < typeof vi . fn > ) . mock . calls [ 0 ] ;
59- expect ( call [ 1 ] . data . stateUpdate ) . toEqual ( { } ) ;
66+ const call = mockRedis . publish . mock . calls [ 0 ] ;
67+ const parsed = JSON . parse ( call [ 1 ] as string ) as MessagePayload ;
68+ expect ( parsed . data [ 'stateUpdate' ] ) . toEqual ( { } ) ;
6069 } ) ;
6170
6271 it ( "listen() subscribes and delivers state deltas via callback" , async ( ) => {
63- let capturedHandler ! : ( payload : MessagePayload ) => Promise < void > ;
64- const mockDriver : IMessagingDriver = {
65- publish : vi . fn ( ) . mockResolvedValue ( undefined ) ,
66- subscribe : vi . fn ( ( _q , handler ) => { capturedHandler = handler ; return Promise . resolve ( ) ; } ) ,
67- unsubscribe : vi . fn ( ) . mockResolvedValue ( undefined ) ,
68- disconnect : vi . fn ( ) . mockResolvedValue ( undefined ) ,
69- } ;
70- const mw = new DistributedStateMiddleware ( mockDriver ) ;
72+ let capturedHandler ! : ( channel : string , message : string ) => void ;
73+ mockRedis . on . mockImplementation ( ( event , handler ) => {
74+ if ( event === 'message' ) capturedHandler = handler ;
75+ } ) ;
76+
77+ const mw = new DistributedStateMiddleware ( 'redis://localhost' ) ;
7178 const onStateChange = vi . fn ( ) ;
7279 await mw . listen ( onStateChange ) ;
73- await capturedHandler ( { taskId : "g" , agentId : "system" , timestamp : 0 , data : { stateUpdate : { x : 1 } } } ) ;
80+
81+ // Simulate incoming Redis pub/sub message
82+ const msg = JSON . stringify ( { taskId : "g" , agentId : "system" , timestamp : 0 , data : { stateUpdate : { x : 1 } } } ) ;
83+ capturedHandler ( "kaiban-state-events" , msg ) ;
84+
7485 expect ( onStateChange ) . toHaveBeenCalledWith ( { x : 1 } ) ;
7586 } ) ;
7687
7788 it ( "publish error is caught and logged without throwing" , async ( ) => {
78- const mockDriver : IMessagingDriver = {
79- publish : vi . fn ( ) . mockRejectedValue ( new Error ( "redis down" ) ) ,
80- subscribe : vi . fn ( ) . mockResolvedValue ( undefined ) ,
81- unsubscribe : vi . fn ( ) . mockResolvedValue ( undefined ) ,
82- disconnect : vi . fn ( ) . mockResolvedValue ( undefined ) ,
83- } ;
89+ mockRedis . publish . mockRejectedValueOnce ( new Error ( "redis down" ) ) ;
8490 const errSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
85- const mw = new DistributedStateMiddleware ( mockDriver ) ;
91+ const mw = new DistributedStateMiddleware ( 'redis://localhost' ) ;
8692 const store = makeStore ( ) ;
8793 mw . attach ( store ) ;
8894 await expect ( store . setState ( { x : 1 } ) ) . resolves . not . toThrow ( ) ;
@@ -91,11 +97,10 @@ describe("DistributedStateMiddleware", () => {
9197 } ) ;
9298
9399 it ( "sanitizeDelta handles empty state update" , async ( ) => {
94- const mockDriver = makeMockDriver ( ) ;
95- const mw = new DistributedStateMiddleware ( mockDriver ) ;
100+ const mw = new DistributedStateMiddleware ( 'redis://localhost' ) ;
96101 const store = makeStore ( ) ;
97102 mw . attach ( store ) ;
98103 await store . setState ( { } ) ;
99- expect ( mockDriver . publish ) . toHaveBeenCalledOnce ( ) ;
104+ expect ( mockRedis . publish ) . toHaveBeenCalledOnce ( ) ;
100105 } ) ;
101106} ) ;
0 commit comments