@@ -61,6 +61,11 @@ function findReq(ws: MockWebSocket, method: string): WsFrame | undefined {
6161 return ws . sent . find ( ( frame ) => frame ?. type === 'req' && frame ?. method === method )
6262}
6363
64+ function findReqAt ( ws : MockWebSocket , method : string , index : number ) : WsFrame | undefined {
65+ const matches = ws . sent . filter ( ( frame ) => frame ?. type === 'req' && frame ?. method === method )
66+ return matches [ index ]
67+ }
68+
6469async function waitFor < T > (
6570 getValue : ( ) => T | null | undefined ,
6671 timeoutMs = 2_000 ,
@@ -79,6 +84,7 @@ async function bootstrapConnector(params?: {
7984 onMessage ?: ( msg : any ) => Promise < string >
8085 connectorId ?: string
8186 wsUrl ?: string
87+ config ?: Record < string , unknown >
8288} ) {
8389 const connectorId = params ?. connectorId || `test-${ Date . now ( ) } -${ Math . random ( ) . toString ( 16 ) . slice ( 2 , 8 ) } `
8490 const connector = {
@@ -89,6 +95,8 @@ async function bootstrapConnector(params?: {
8995 credentialId : null ,
9096 config : {
9197 wsUrl : params ?. wsUrl || 'ws://localhost:18789' ,
98+ historyPoll : false ,
99+ ...( params ?. config || { } ) ,
92100 } ,
93101 isEnabled : true ,
94102 status : 'running' ,
@@ -190,6 +198,48 @@ test('openclaw connector suppresses outbound send when NO_MESSAGE is returned',
190198 }
191199} )
192200
201+ test ( 'openclaw connector accepts short session filter for full agent session keys' , async ( ) => {
202+ const received : any [ ] = [ ]
203+ const { instance, ws, identityPath } = await bootstrapConnector ( {
204+ onMessage : async ( msg ) => {
205+ received . push ( msg )
206+ return 'alias-ok'
207+ } ,
208+ config : {
209+ sessionKey : 'main' ,
210+ } ,
211+ } )
212+
213+ try {
214+ await performHandshake ( ws )
215+ ws . emit ( {
216+ type : 'event' ,
217+ event : 'chat' ,
218+ payload : {
219+ state : 'final' ,
220+ sessionKey : 'agent:main:main' ,
221+ message : { role : 'user' , text : 'Hello alias' , sender : 'Wayde' } ,
222+ } ,
223+ } )
224+
225+ const chatReq = await waitFor ( ( ) => findReq ( ws , 'chat.send' ) , 2_000 )
226+ assert . equal ( ( chatReq . params as any ) ?. sessionKey , 'agent:main:main' )
227+ assert . equal ( ( chatReq . params as any ) ?. message , 'alias-ok' )
228+ assert . equal ( received . length , 1 )
229+ assert . equal ( received [ 0 ] ?. text , 'Hello alias' )
230+
231+ ws . emit ( {
232+ type : 'res' ,
233+ id : chatReq . id as string ,
234+ ok : true ,
235+ payload : { runId : 'alias-run' } ,
236+ } )
237+ } finally {
238+ await instance . stop ( )
239+ fs . rmSync ( identityPath , { force : true } )
240+ }
241+ } )
242+
193243test ( 'openclaw connector sendMessage attaches local media payloads' , async ( ) => {
194244 const { instance, ws, identityPath } = await bootstrapConnector ( )
195245 const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'openclaw-connector-test-' ) )
@@ -222,6 +272,90 @@ test('openclaw connector sendMessage attaches local media payloads', async () =>
222272 }
223273} )
224274
275+ test ( 'openclaw connector polls chat.history fallback for inbound user messages' , async ( ) => {
276+ const received : any [ ] = [ ]
277+ const { instance, ws, identityPath } = await bootstrapConnector ( {
278+ onMessage : async ( msg ) => {
279+ received . push ( msg )
280+ return 'history-pong'
281+ } ,
282+ config : {
283+ sessionKey : 'agent:main:main' ,
284+ historyPoll : true ,
285+ historyPollMs : 500 ,
286+ } ,
287+ } )
288+
289+ try {
290+ await performHandshake ( ws )
291+
292+ const firstHistoryReq = await waitFor (
293+ ( ) => findReqAt ( ws , 'chat.history' , 0 ) ,
294+ 2_500 ,
295+ )
296+ assert . equal ( ( firstHistoryReq . params as any ) ?. sessionKey , 'agent:main:main' )
297+ ws . emit ( {
298+ type : 'res' ,
299+ id : firstHistoryReq . id as string ,
300+ ok : true ,
301+ payload : {
302+ sessionKey : 'agent:main:main' ,
303+ messages : [
304+ {
305+ role : 'user' ,
306+ timestamp : 1 ,
307+ content : [ { type : 'text' , text : 'old message' } ] ,
308+ } ,
309+ ] ,
310+ } ,
311+ } )
312+
313+ await new Promise ( ( resolve ) => setTimeout ( resolve , 120 ) )
314+ assert . equal ( received . length , 0 )
315+
316+ const secondHistoryReq = await waitFor (
317+ ( ) => findReqAt ( ws , 'chat.history' , 1 ) ,
318+ 3_000 ,
319+ )
320+ ws . emit ( {
321+ type : 'res' ,
322+ id : secondHistoryReq . id as string ,
323+ ok : true ,
324+ payload : {
325+ sessionKey : 'agent:main:main' ,
326+ messages : [
327+ {
328+ role : 'user' ,
329+ timestamp : 1 ,
330+ content : [ { type : 'text' , text : 'old message' } ] ,
331+ } ,
332+ {
333+ role : 'user' ,
334+ timestamp : 2 ,
335+ content : [ { type : 'text' , text : 'new message' } ] ,
336+ } ,
337+ ] ,
338+ } ,
339+ } )
340+
341+ const chatReq = await waitFor ( ( ) => findReq ( ws , 'chat.send' ) , 2_000 )
342+ assert . equal ( ( chatReq . params as any ) ?. sessionKey , 'agent:main:main' )
343+ assert . equal ( ( chatReq . params as any ) ?. message , 'history-pong' )
344+ assert . equal ( received . length , 1 )
345+ assert . equal ( received [ 0 ] ?. text , 'new message' )
346+
347+ ws . emit ( {
348+ type : 'res' ,
349+ id : chatReq . id as string ,
350+ ok : true ,
351+ payload : { runId : 'history-run' } ,
352+ } )
353+ } finally {
354+ await instance . stop ( )
355+ fs . rmSync ( identityPath , { force : true } )
356+ }
357+ } )
358+
225359test ( 'openclaw connector reconnects when tick watchdog detects stale connection' , async ( ) => {
226360 const { instance, ws, identityPath } = await bootstrapConnector ( )
227361
0 commit comments