@@ -2,11 +2,14 @@ namespace Temporalio.Tests.Testing;
22
33using System . Diagnostics ;
44using System . Runtime . InteropServices ;
5+ using NexusRpc ;
6+ using NexusRpc . Handlers ;
57using Temporalio . Activities ;
68using Temporalio . Api . Enums . V1 ;
79using Temporalio . Client ;
810using Temporalio . Common ;
911using Temporalio . Exceptions ;
12+ using Temporalio . Nexus ;
1013using Temporalio . Testing ;
1114using Temporalio . Worker ;
1215using Temporalio . Workflows ;
@@ -241,6 +244,178 @@ await env.WithAutoTimeSkippingDisabledAsync(async () =>
241244 } ) ;
242245 }
243246
247+ [ NexusService ]
248+ public interface INexusLongRunningService
249+ {
250+ [ NexusOperation ]
251+ string RunLongOperation ( string input ) ;
252+ }
253+
254+ [ Workflow ]
255+ public class NexusLongRunningWorkflow
256+ {
257+ [ WorkflowRun ]
258+ public async Task < string > RunAsync ( string input )
259+ {
260+ await Workflow . DelayAsync ( TimeSpan . FromDays ( 2 ) ) ;
261+ return $ "done: { input } ";
262+ }
263+ }
264+
265+ [ NexusServiceHandler ( typeof ( INexusLongRunningService ) ) ]
266+ public class NexusLongRunningServiceHandler
267+ {
268+ [ NexusOperationHandler ]
269+ public IOperationHandler < string , string > RunLongOperation ( ) =>
270+ WorkflowRunOperationHandler . FromHandleFactory (
271+ ( WorkflowRunOperationContext context , string input ) =>
272+ context . StartWorkflowAsync (
273+ ( NexusLongRunningWorkflow wf ) => wf . RunAsync ( input ) ,
274+ new ( ) { Id = $ "nexus-wf-{ Guid . NewGuid ( ) } " } ) ) ;
275+ }
276+
277+ [ Workflow ]
278+ public class NexusCallerWorkflow
279+ {
280+ private readonly string endpoint ;
281+
282+ public NexusCallerWorkflow ( string endpoint ) => this . endpoint = endpoint ;
283+
284+ [ WorkflowRun ]
285+ public async Task < string > RunAsync ( string input )
286+ {
287+ return await Workflow . CreateNexusClient < INexusLongRunningService > ( endpoint ) .
288+ ExecuteNexusOperationAsync ( svc => svc . RunLongOperation ( input ) ) ;
289+ }
290+ }
291+
292+ [ OnlyIntelFact ]
293+ public async Task StartTimeSkippingAsync_NexusLongRunningWorkflow_ProperlySkips ( )
294+ {
295+ await using var env = await WorkflowEnvironment . StartTimeSkippingAsync ( ) ;
296+ var taskQueue = $ "tq-{ Guid . NewGuid ( ) } ";
297+ var endpointName = $ "nexus-endpoint-{ taskQueue } ";
298+ await env . CreateNexusEndpointAsync ( endpointName , taskQueue ) ;
299+ using var worker = new TemporalWorker (
300+ env . Client ,
301+ new TemporalWorkerOptions ( taskQueue ) .
302+ AddNexusService ( new NexusLongRunningServiceHandler ( ) ) .
303+ AddWorkflow < NexusLongRunningWorkflow > ( ) .
304+ AddWorkflow ( WorkflowDefinition . Create (
305+ typeof ( NexusCallerWorkflow ) ,
306+ null ,
307+ _args => new NexusCallerWorkflow ( endpointName ) ) ) ) ;
308+ await worker . ExecuteAsync ( async ( ) =>
309+ {
310+ // Check that timestamp is around now
311+ AssertMore . DateTimeFromUtcNow ( await env . GetCurrentTimeAsync ( ) , TimeSpan . Zero ) ;
312+
313+ // Run the caller workflow which invokes the Nexus operation that starts a
314+ // long-running workflow with a 2-day delay
315+ var watch = Stopwatch . StartNew ( ) ;
316+ var result = await env . Client . ExecuteWorkflowAsync (
317+ ( NexusCallerWorkflow wf ) => wf . RunAsync ( "test-input" ) ,
318+ new ( id : $ "workflow-{ Guid . NewGuid ( ) } ", taskQueue : taskQueue ) ) ;
319+ Assert . Equal ( "done: test-input" , result ) ;
320+
321+ // Verify time was skipped (should complete much faster than 2 days)
322+ Assert . True ( watch . Elapsed < TimeSpan . FromSeconds ( 30 ) ) ;
323+
324+ // Check that the server time advanced ~2 days
325+ AssertMore . DateTimeFromUtcNow ( await env . GetCurrentTimeAsync ( ) , TimeSpan . FromDays ( 2 ) ) ;
326+ } ) ;
327+ }
328+
329+ [ OnlyIntelFact ]
330+ public async Task StartTimeSkippingAsync_NexusLongRunningWorkflow_ManualSkipAndCancel ( )
331+ {
332+ await using var env = await WorkflowEnvironment . StartTimeSkippingAsync ( ) ;
333+ var taskQueue = $ "tq-{ Guid . NewGuid ( ) } ";
334+ var endpointName = $ "nexus-endpoint-{ taskQueue } ";
335+ await env . CreateNexusEndpointAsync ( endpointName , taskQueue ) ;
336+ using var worker = new TemporalWorker (
337+ env . Client ,
338+ new TemporalWorkerOptions ( taskQueue ) .
339+ AddNexusService ( new NexusLongRunningServiceHandler ( ) ) .
340+ AddWorkflow < NexusLongRunningWorkflow > ( ) .
341+ AddWorkflow ( WorkflowDefinition . Create (
342+ typeof ( NexusCallerWorkflow ) ,
343+ null ,
344+ _args => new NexusCallerWorkflow ( endpointName ) ) ) ) ;
345+ await worker . ExecuteAsync ( async ( ) =>
346+ {
347+ // Start the caller workflow
348+ var handle = await env . Client . StartWorkflowAsync (
349+ ( NexusCallerWorkflow wf ) => wf . RunAsync ( "test-input" ) ,
350+ new ( id : $ "workflow-{ Guid . NewGuid ( ) } ", taskQueue : taskQueue ) ) ;
351+
352+ // Manually skip time by 1 day (less than the 2-day delay) and confirm
353+ // the workflow is still running
354+ await env . DelayAsync ( TimeSpan . FromDays ( 1 ) ) ;
355+ var desc = await handle . DescribeAsync ( ) ;
356+ Assert . Equal ( WorkflowExecutionStatus . Running , desc . Status ) ;
357+
358+ // Cancel the caller workflow
359+ await handle . CancelAsync ( ) ;
360+ var exc = await Assert . ThrowsAsync < WorkflowFailedException > (
361+ ( ) => handle . GetResultAsync ( ) ) ;
362+ Assert . IsType < CanceledFailureException > ( exc . InnerException ) ;
363+ } ) ;
364+ }
365+
366+ [ Workflow ]
367+ public class NexusCallerWithTimeoutWorkflow
368+ {
369+ private readonly string endpoint ;
370+
371+ public NexusCallerWithTimeoutWorkflow ( string endpoint ) => this . endpoint = endpoint ;
372+
373+ [ WorkflowRun ]
374+ public async Task < string > RunAsync ( string input )
375+ {
376+ return await Workflow . CreateNexusClient < INexusLongRunningService > ( endpoint ) .
377+ ExecuteNexusOperationAsync (
378+ svc => svc . RunLongOperation ( input ) ,
379+ new ( ) { ScheduleToCloseTimeout = TimeSpan . FromDays ( 1 ) } ) ;
380+ }
381+ }
382+
383+ [ OnlyIntelFact ]
384+ public async Task StartTimeSkippingAsync_NexusScheduleToCloseTimeout_TimesOut ( )
385+ {
386+ await using var env = await WorkflowEnvironment . StartTimeSkippingAsync ( ) ;
387+ var taskQueue = $ "tq-{ Guid . NewGuid ( ) } ";
388+ var endpointName = $ "nexus-endpoint-{ taskQueue } ";
389+ await env . CreateNexusEndpointAsync ( endpointName , taskQueue ) ;
390+ using var worker = new TemporalWorker (
391+ env . Client ,
392+ new TemporalWorkerOptions ( taskQueue ) .
393+ AddNexusService ( new NexusLongRunningServiceHandler ( ) ) .
394+ AddWorkflow < NexusLongRunningWorkflow > ( ) .
395+ AddWorkflow ( WorkflowDefinition . Create (
396+ typeof ( NexusCallerWithTimeoutWorkflow ) ,
397+ null ,
398+ _args => new NexusCallerWithTimeoutWorkflow ( endpointName ) ) ) ) ;
399+ await worker . ExecuteAsync ( async ( ) =>
400+ {
401+ // The backing workflow has a 2-day delay but the Nexus operation has a 1-day
402+ // schedule-to-close timeout, so the timeout should fire via time skipping
403+ var watch = Stopwatch . StartNew ( ) ;
404+ var exc = await Assert . ThrowsAsync < WorkflowFailedException > ( ( ) =>
405+ env . Client . ExecuteWorkflowAsync (
406+ ( NexusCallerWithTimeoutWorkflow wf ) => wf . RunAsync ( "test-input" ) ,
407+ new ( id : $ "workflow-{ Guid . NewGuid ( ) } ", taskQueue : taskQueue ) ) ) ;
408+
409+ // Verify time was skipped (should complete much faster than 1 day)
410+ Assert . True ( watch . Elapsed < TimeSpan . FromSeconds ( 30 ) ) ;
411+
412+ // Verify the failure chain
413+ var nexusExc = Assert . IsType < NexusOperationFailureException > ( exc . InnerException ) ;
414+ var timeoutExc = Assert . IsType < TimeoutFailureException > ( nexusExc . InnerException ) ;
415+ Assert . Equal ( TimeoutType . ScheduleToClose , timeoutExc . TimeoutType ) ;
416+ } ) ;
417+ }
418+
244419 [ Fact ]
245420 public async Task StartLocal_SearchAttributes_ProperlyRegistered ( )
246421 {
0 commit comments