@@ -16,10 +16,18 @@ const mocks = vi.hoisted(() => ({
1616 formatRestartSentinelMessage : vi . fn ( ( ) => "restart message" ) ,
1717 summarizeRestartSentinel : vi . fn ( ( ) => "restart summary" ) ,
1818 resolveMainSessionKeyFromConfig : vi . fn ( ( ) => "agent:main:main" ) ,
19- parseSessionThreadInfo : vi . fn ( ( ) => ( { baseSessionKey : null , threadId : undefined } ) ) ,
19+ parseSessionThreadInfo : vi . fn (
20+ ( ) : { baseSessionKey : string | null | undefined ; threadId : string | undefined } => ( {
21+ baseSessionKey : null ,
22+ threadId : undefined ,
23+ } ) ,
24+ ) ,
2025 loadSessionEntry : vi . fn ( ( ) => ( { cfg : { } , entry : { } } ) ) ,
21- resolveAnnounceTargetFromKey : vi . fn ( ( ) => null ) ,
22- deliveryContextFromSession : vi . fn ( ( ) => undefined ) ,
26+ deliveryContextFromSession : vi . fn (
27+ ( ) :
28+ | { channel ?: string ; to ?: string ; accountId ?: string ; threadId ?: string | number }
29+ | undefined => undefined ,
30+ ) ,
2331 mergeDeliveryContext : vi . fn ( ( a ?: Record < string , unknown > , b ?: Record < string , unknown > ) => ( {
2432 ...b ,
2533 ...a ,
@@ -50,18 +58,14 @@ vi.mock("../config/sessions.js", () => ({
5058 resolveMainSessionKeyFromConfig : mocks . resolveMainSessionKeyFromConfig ,
5159} ) ) ;
5260
53- vi . mock ( "../config/sessions/delivery -info.js" , ( ) => ( {
61+ vi . mock ( "../config/sessions/thread -info.js" , ( ) => ( {
5462 parseSessionThreadInfo : mocks . parseSessionThreadInfo ,
5563} ) ) ;
5664
5765vi . mock ( "./session-utils.js" , ( ) => ( {
5866 loadSessionEntry : mocks . loadSessionEntry ,
5967} ) ) ;
6068
61- vi . mock ( "../agents/tools/sessions-send-helpers.js" , ( ) => ( {
62- resolveAnnounceTargetFromKey : mocks . resolveAnnounceTargetFromKey ,
63- } ) ) ;
64-
6569vi . mock ( "../utils/delivery-context.js" , ( ) => ( {
6670 deliveryContextFromSession : mocks . deliveryContextFromSession ,
6771 mergeDeliveryContext : mocks . mergeDeliveryContext ,
@@ -126,6 +130,14 @@ describe("scheduleRestartSentinelWake", () => {
126130 } ,
127131 } ,
128132 } ) ;
133+ mocks . parseSessionThreadInfo . mockReset ( ) ;
134+ mocks . parseSessionThreadInfo . mockReturnValue ( { baseSessionKey : null , threadId : undefined } ) ;
135+ mocks . loadSessionEntry . mockReset ( ) ;
136+ mocks . loadSessionEntry . mockReturnValue ( { cfg : { } , entry : { } } ) ;
137+ mocks . deliveryContextFromSession . mockReset ( ) ;
138+ mocks . deliveryContextFromSession . mockReturnValue ( undefined ) ;
139+ mocks . resolveOutboundTarget . mockReset ( ) ;
140+ mocks . resolveOutboundTarget . mockReturnValue ( { ok : true as const , to : "+15550002" } ) ;
129141 mocks . deliverOutboundPayloads . mockReset ( ) ;
130142 mocks . deliverOutboundPayloads . mockResolvedValue ( [ { channel : "whatsapp" , messageId : "msg-1" } ] ) ;
131143 mocks . enqueueDelivery . mockReset ( ) ;
@@ -278,4 +290,73 @@ describe("scheduleRestartSentinelWake", () => {
278290 expect ( mocks . requestHeartbeatNow ) . not . toHaveBeenCalled ( ) ;
279291 expect ( mocks . deliverOutboundPayloads ) . not . toHaveBeenCalled ( ) ;
280292 } ) ;
293+
294+ it ( "skips outbound restart notice when no canonical delivery context survives restart" , async ( ) => {
295+ mocks . consumeRestartSentinel . mockResolvedValue ( {
296+ payload : {
297+ sessionKey : "agent:main:matrix:channel:!lowercased:example.org" ,
298+ } ,
299+ } as Awaited < ReturnType < typeof mocks . consumeRestartSentinel > > ) ;
300+ mocks . parseSessionThreadInfo . mockReturnValue ( {
301+ baseSessionKey : "agent:main:matrix:channel:!lowercased:example.org" ,
302+ threadId : undefined ,
303+ } ) ;
304+ mocks . deliveryContextFromSession . mockReturnValue ( undefined ) ;
305+
306+ await scheduleRestartSentinelWake ( { deps : { } as never } ) ;
307+
308+ expect ( mocks . enqueueSystemEvent ) . toHaveBeenCalledWith (
309+ "restart message" ,
310+ expect . objectContaining ( {
311+ sessionKey : "agent:main:matrix:channel:!lowercased:example.org" ,
312+ } ) ,
313+ ) ;
314+ expect ( mocks . deliverOutboundPayloads ) . not . toHaveBeenCalled ( ) ;
315+ expect ( mocks . enqueueDelivery ) . not . toHaveBeenCalled ( ) ;
316+ expect ( mocks . resolveOutboundTarget ) . not . toHaveBeenCalled ( ) ;
317+ } ) ;
318+
319+ it ( "falls back to the base session when the thread entry only has partial route metadata" , async ( ) => {
320+ mocks . consumeRestartSentinel . mockResolvedValue ( {
321+ payload : {
322+ sessionKey : "agent:main:matrix:channel:!lowercased:example.org:thread:$thread-event" ,
323+ } ,
324+ } as Awaited < ReturnType < typeof mocks . consumeRestartSentinel > > ) ;
325+ mocks . parseSessionThreadInfo . mockReturnValue ( {
326+ baseSessionKey : "agent:main:matrix:channel:!lowercased:example.org" ,
327+ threadId : "$thread-event" ,
328+ } ) ;
329+ mocks . loadSessionEntry
330+ . mockReturnValueOnce ( {
331+ cfg : { } ,
332+ entry : { origin : { provider : "matrix" , threadId : "$thread-event" } } ,
333+ } )
334+ . mockReturnValueOnce ( {
335+ cfg : { } ,
336+ entry : { lastChannel : "matrix" , lastTo : "room:!MixedCase:example.org" } ,
337+ } ) ;
338+ mocks . deliveryContextFromSession
339+ . mockReturnValueOnce ( { channel : "matrix" , threadId : "$thread-event" } )
340+ . mockReturnValueOnce ( { channel : "matrix" , to : "room:!MixedCase:example.org" } ) ;
341+ mocks . resolveOutboundTarget . mockReturnValue ( {
342+ ok : true as const ,
343+ to : "room:!MixedCase:example.org" ,
344+ } ) ;
345+
346+ await scheduleRestartSentinelWake ( { deps : { } as never } ) ;
347+
348+ expect ( mocks . resolveOutboundTarget ) . toHaveBeenCalledWith (
349+ expect . objectContaining ( {
350+ channel : "matrix" ,
351+ to : "room:!MixedCase:example.org" ,
352+ } ) ,
353+ ) ;
354+ expect ( mocks . deliverOutboundPayloads ) . toHaveBeenCalledWith (
355+ expect . objectContaining ( {
356+ channel : "matrix" ,
357+ to : "room:!MixedCase:example.org" ,
358+ threadId : "$thread-event" ,
359+ } ) ,
360+ ) ;
361+ } ) ;
281362} ) ;
0 commit comments