@@ -228,4 +228,59 @@ await store.MessageStore.AppendMessages([
228228
229229 unhandledExceptionHandler . ShouldNotHaveExceptions ( ) ;
230230 }
231+
232+ public abstract Task EmptyMessageIsNotDeliveredWhenFlowIsRestartedViaControlPanel ( ) ;
233+ public async Task EmptyMessageIsNotDeliveredWhenFlowIsRestartedViaControlPanel ( Task < IFunctionStore > functionStore )
234+ {
235+ var store = await functionStore ;
236+
237+ var flowId = TestFlowId . Create ( ) ;
238+ var unhandledExceptionHandler = new UnhandledExceptionCatcher ( ) ;
239+ // Watchdogs are disabled so the pending messages can only reach the flow via the control panel restart's
240+ // hand-over (the RestartExecution route) - not via the MessageWatchdog route.
241+ using var functionsRegistry = new FunctionsRegistry (
242+ store ,
243+ new Settings ( unhandledExceptionHandler . Catch , enableWatchdogs : false )
244+ ) ;
245+
246+ var registration = functionsRegistry . RegisterFunc (
247+ flowId . Type ,
248+ inner : async Task < string > ( string _ , Workflow workflow ) => await workflow . Message < string > ( )
249+ ) ;
250+
251+ await registration . Schedule ( flowId . Instance . Value , "" ) ;
252+
253+ var controlPanel = await registration . ControlPanel ( flowId . Instance ) ;
254+ controlPanel . ShouldNotBeNull ( ) ;
255+ await controlPanel . BusyWaitUntil ( c => c . Status == Status . Suspended ) ;
256+
257+ var storedId = registration . MapToStoredId ( flowId . Instance ) ;
258+ var replicaId = functionsRegistry . ClusterInfo . ReplicaId ;
259+ var serializer = DefaultSerializer . Instance ;
260+ await store . MessageStore . AppendMessages ( [
261+ new StoredIdAndMessage ( storedId , StoredMessage . CreateEmpty ( replicaId ) ) ,
262+ new StoredIdAndMessage (
263+ storedId ,
264+ new StoredMessage (
265+ serializer . Serialize ( "hello world" , typeof ( string ) ) ,
266+ serializer . SerializeType ( typeof ( string ) ) ,
267+ Position : 0 ,
268+ Replica : replicaId
269+ )
270+ )
271+ ] ) ;
272+
273+ await controlPanel . ScheduleRestart ( ) ;
274+
275+ // The restarted flow receives only the non-empty message.
276+ await controlPanel . WaitForCompletion ( allowPostponeAndSuspended : true ) ;
277+ await controlPanel . Refresh ( ) ;
278+ controlPanel . Status . ShouldBe ( Status . Succeeded ) ;
279+ controlPanel . Result . ShouldBe ( "hello world" ) ;
280+
281+ // The empty message is deleted by the restart hand-over; the delivered one is deleted after delivery.
282+ await BusyWait . Until ( async ( ) => ( await store . MessageStore . GetMessages ( storedId ) ) . Count == 0 ) ;
283+
284+ unhandledExceptionHandler . ShouldNotHaveExceptions ( ) ;
285+ }
231286}
0 commit comments