@@ -178,6 +178,16 @@ function requireValue<T>(value: T | null | undefined, label: string): T {
178178 return value ;
179179}
180180
181+ function createDeferred < T = void > ( ) {
182+ let resolve ! : ( value : T | PromiseLike < T > ) => void ;
183+ let reject ! : ( reason ?: unknown ) => void ;
184+ const promise = new Promise < T > ( ( resolvePromise , rejectPromise ) => {
185+ resolve = resolvePromise ;
186+ reject = rejectPromise ;
187+ } ) ;
188+ return { promise, resolve, reject } ;
189+ }
190+
181191function requireRecord ( value : unknown , label : string ) : Record < string , unknown > {
182192 if ( typeof value !== "object" || value === null || Array . isArray ( value ) ) {
183193 throw new Error ( `expected ${ label } to be an object` ) ;
@@ -1717,6 +1727,52 @@ describe("createTelegramBot", () => {
17171727 expect ( replySpy ) . toHaveBeenCalledTimes ( 1 ) ;
17181728 } ) ;
17191729
1730+ it ( "dedupes a replayed Telegram message after handler recreation while dispatch is pending" , async ( ) => {
1731+ loadConfig . mockReturnValue ( {
1732+ channels : { telegram : { dmPolicy : "open" , allowFrom : [ "*" ] } } ,
1733+ } ) ;
1734+
1735+ const firstDispatchStarted = createDeferred ( ) ;
1736+ const finishFirstDispatch = createDeferred ( ) ;
1737+ replySpy . mockImplementationOnce ( async ( _ctx : MsgContext , opts ?: GetReplyOptions ) => {
1738+ await opts ?. onReplyStart ?.( ) ;
1739+ firstDispatchStarted . resolve ( ) ;
1740+ await finishFirstDispatch . promise ;
1741+ return undefined ;
1742+ } ) ;
1743+
1744+ const replayedCtx = ( ) => ( {
1745+ update : { update_id : 8488602 } ,
1746+ message : {
1747+ chat : { id : 123 , type : "private" } ,
1748+ from : { id : 456 , username : "testuser" } ,
1749+ text : "replay while pending" ,
1750+ date : 1736380800 ,
1751+ message_id : 43 ,
1752+ } ,
1753+ me : { username : "openclaw_bot" } ,
1754+ getFile : async ( ) => ( { download : async ( ) => new Uint8Array ( ) } ) ,
1755+ } ) ;
1756+
1757+ createTelegramBot ( { token : "tok" } ) ;
1758+ const firstRun = ( getOnHandler ( "message" ) as ( ctx : Record < string , unknown > ) => Promise < void > ) (
1759+ replayedCtx ( ) ,
1760+ ) ;
1761+ await firstDispatchStarted . promise ;
1762+ expect ( replySpy ) . toHaveBeenCalledTimes ( 1 ) ;
1763+
1764+ onSpy . mockClear ( ) ;
1765+ createTelegramBot ( { token : "tok" } ) ;
1766+ await ( getOnHandler ( "message" ) as ( ctx : Record < string , unknown > ) => Promise < void > ) (
1767+ replayedCtx ( ) ,
1768+ ) ;
1769+
1770+ expect ( replySpy ) . toHaveBeenCalledTimes ( 1 ) ;
1771+ finishFirstDispatch . resolve ( ) ;
1772+ await firstRun ;
1773+ expect ( replySpy ) . toHaveBeenCalledTimes ( 1 ) ;
1774+ } ) ;
1775+
17201776 it ( "persists update offsets after successful dispatch completion" , async ( ) => {
17211777 // For this test we need sequentialize(...) to behave like a normal middleware and call next().
17221778 sequentializeSpy . mockImplementationOnce (
0 commit comments