11// Copyright (c) Microsoft Corporation. All rights reserved.
22// Licensed under the MIT License.
33
4- import * as pb from "../src/proto/orchestrator_service_pb" ;
54import { CompletableTask } from "../src/task/completable-task" ;
6- import { TimerTask , TimerBookkeeping } from "../src/task/timer-task" ;
7-
8- /**
9- * Minimal fake bookkeeping that mirrors the fields TimerTask.cancel() touches on
10- * the real RuntimeOrchestrationContext.
11- */
12- function makeBookkeeping ( ) : TimerBookkeeping {
13- return {
14- _pendingActions : { } as Record < number , pb . OrchestratorAction > ,
15- _pendingTasks : { } as Record < number , CompletableTask < any > > ,
16- } ;
17- }
5+ import { TimerTask } from "../src/task/timer-task" ;
6+ import { RuntimeOrchestrationContext } from "../src/worker/runtime-orchestration-context" ;
187
19- describe ( "TimerTask" , ( ) => {
20- const TIMER_ID = 1 ;
8+ // A far-future fire time so timers created in these tests never fire on their own.
9+ const FUTURE_FIRE_AT = new Date ( Date . now ( ) + 24 * 60 * 60 * 1000 ) ;
2110
11+ describe ( "TimerTask" , ( ) => {
2212 it ( "should start incomplete, not failed, and not canceled" , ( ) => {
23- const bookkeeping = makeBookkeeping ( ) ;
24- const timer = new TimerTask ( bookkeeping , TIMER_ID ) ;
13+ const timer = new TimerTask ( ) ;
2514
2615 expect ( timer . isComplete ) . toBe ( false ) ;
2716 expect ( timer . isCompleted ) . toBe ( false ) ;
@@ -30,34 +19,54 @@ describe("TimerTask", () => {
3019 } ) ;
3120
3221 it ( "should be a Task/CompletableTask instance (identity preserving, no wrapper)" , ( ) => {
33- const bookkeeping = makeBookkeeping ( ) ;
34- const timer = new TimerTask ( bookkeeping , TIMER_ID ) ;
22+ const timer = new TimerTask ( ) ;
3523
3624 // TimerTask must extend CompletableTask so whenAny/whenAll return the real
3725 // instance and `winner === timerTask` identity holds for callers.
3826 expect ( timer ) . toBeInstanceOf ( CompletableTask ) ;
3927 } ) ;
4028
29+ it ( "should return the same TimerTask instance that the context stores in _pendingTasks" , ( ) => {
30+ // Identity: createTimer must hand back the exact instance it tracks so that
31+ // `winner === timerTask` holds after whenAny.
32+ const ctx = new RuntimeOrchestrationContext ( "test-instance" ) ;
33+ const timer = ctx . createTimer ( FUTURE_FIRE_AT ) ;
34+ const timerId = ctx . _sequenceNumber ;
35+
36+ expect ( ctx . _pendingTasks [ timerId ] ) . toBe ( timer ) ;
37+ } ) ;
38+
4139 describe ( "cancel()" , ( ) => {
42- it ( "should flip isCanceled and drop the pending action and pending task" , ( ) => {
43- const bookkeeping = makeBookkeeping ( ) ;
44- const action = new pb . OrchestratorAction ( ) ;
45- action . setId ( TIMER_ID ) ;
46- const timer = new TimerTask ( bookkeeping , TIMER_ID ) ;
47- bookkeeping . _pendingActions [ TIMER_ID ] = action ;
48- bookkeeping . _pendingTasks [ TIMER_ID ] = timer ;
40+ it ( "should flip isCanceled and run the injected cancel handler once" , ( ) => {
41+ const timer = new TimerTask ( ) ;
42+ const cancelHandler = jest . fn ( ) ;
43+ timer . setCancelHandler ( cancelHandler ) ;
4944
5045 timer . cancel ( ) ;
5146
5247 expect ( timer . isCanceled ) . toBe ( true ) ;
53- expect ( bookkeeping . _pendingActions [ TIMER_ID ] ) . toBeUndefined ( ) ;
54- expect ( bookkeeping . _pendingTasks [ TIMER_ID ] ) . toBeUndefined ( ) ;
48+ expect ( cancelHandler ) . toHaveBeenCalledTimes ( 1 ) ;
49+ } ) ;
50+
51+ it ( "should drop the pending CreateTimer action and pending task via the context handler" , ( ) => {
52+ // Drive a real context so the injected closure exercises the actual deletion.
53+ const ctx = new RuntimeOrchestrationContext ( "test-instance" ) ;
54+ const timer = ctx . createTimer ( FUTURE_FIRE_AT ) ;
55+ const timerId = ctx . _sequenceNumber ;
56+
57+ expect ( ctx . _pendingActions [ timerId ] ) . toBeDefined ( ) ;
58+ expect ( ctx . _pendingTasks [ timerId ] ) . toBe ( timer ) ;
59+
60+ timer . cancel ( ) ;
61+
62+ expect ( timer . isCanceled ) . toBe ( true ) ;
63+ expect ( ctx . _pendingActions [ timerId ] ) . toBeUndefined ( ) ;
64+ expect ( ctx . _pendingTasks [ timerId ] ) . toBeUndefined ( ) ;
5565 } ) ;
5666
5767 it ( "should not mark the timer complete (isCompleted stays false, isFaulted false)" , ( ) => {
58- const bookkeeping = makeBookkeeping ( ) ;
59- const timer = new TimerTask ( bookkeeping , TIMER_ID ) ;
60- bookkeeping . _pendingTasks [ TIMER_ID ] = timer ;
68+ const timer = new TimerTask ( ) ;
69+ timer . setCancelHandler ( jest . fn ( ) ) ;
6170
6271 timer . cancel ( ) ;
6372
@@ -66,45 +75,49 @@ describe("TimerTask", () => {
6675 expect ( timer . isFaulted ) . toBe ( false ) ;
6776 } ) ;
6877
69- it ( "should be idempotent when called multiple times" , ( ) => {
70- const bookkeeping = makeBookkeeping ( ) ;
71- const timer = new TimerTask ( bookkeeping , TIMER_ID ) ;
72- bookkeeping . _pendingTasks [ TIMER_ID ] = timer ;
78+ it ( "should be idempotent when called multiple times (handler runs only once) " , ( ) => {
79+ const timer = new TimerTask ( ) ;
80+ const cancelHandler = jest . fn ( ) ;
81+ timer . setCancelHandler ( cancelHandler ) ;
7382
7483 timer . cancel ( ) ;
7584 expect ( ( ) => timer . cancel ( ) ) . not . toThrow ( ) ;
85+
7686 expect ( timer . isCanceled ) . toBe ( true ) ;
87+ expect ( cancelHandler ) . toHaveBeenCalledTimes ( 1 ) ;
7788 } ) ;
7889
7990 it ( "should not remove an unrelated pending action/task with a different id" , ( ) => {
80- const bookkeeping = makeBookkeeping ( ) ;
81- const otherAction = new pb . OrchestratorAction ( ) ;
82- otherAction . setId ( 2 ) ;
83- const otherTask = new CompletableTask < number > ( ) ;
84- bookkeeping . _pendingActions [ 2 ] = otherAction ;
85- bookkeeping . _pendingTasks [ 2 ] = otherTask ;
86-
87- const timer = new TimerTask ( bookkeeping , TIMER_ID ) ;
88- bookkeeping . _pendingTasks [ TIMER_ID ] = timer ;
89-
90- timer . cancel ( ) ;
91-
92- // The sibling entries at id 2 must remain untouched .
93- expect ( bookkeeping . _pendingActions [ 2 ] ) . toBe ( otherAction ) ;
94- expect ( bookkeeping . _pendingTasks [ 2 ] ) . toBe ( otherTask ) ;
91+ // Two sibling timers on one real context; canceling one must not touch the other.
92+ const ctx = new RuntimeOrchestrationContext ( "test-instance" ) ;
93+ const firstTimer = ctx . createTimer ( FUTURE_FIRE_AT ) ;
94+ const firstId = ctx . _sequenceNumber ;
95+ const secondTimer = ctx . createTimer ( FUTURE_FIRE_AT ) ;
96+ const secondId = ctx . _sequenceNumber ;
97+
98+ firstTimer . cancel ( ) ;
99+
100+ // The sibling entries at the other id must remain untouched.
101+ expect ( ctx . _pendingActions [ secondId ] ) . toBeDefined ( ) ;
102+ expect ( ctx . _pendingTasks [ secondId ] ) . toBe ( secondTimer ) ;
103+ // The canceled timer's entries are gone .
104+ expect ( ctx . _pendingActions [ firstId ] ) . toBeUndefined ( ) ;
105+ expect ( ctx . _pendingTasks [ firstId ] ) . toBeUndefined ( ) ;
95106 } ) ;
96107
97108 it ( "should be a no-op after the timer has already fired (completed)" , ( ) => {
98- const bookkeeping = makeBookkeeping ( ) ;
99- const timer = new TimerTask ( bookkeeping , TIMER_ID ) ;
109+ const timer = new TimerTask ( ) ;
110+ const cancelHandler = jest . fn ( ) ;
111+ timer . setCancelHandler ( cancelHandler ) ;
100112
101113 // Simulate the timer firing (handleTimerFired calls complete(undefined)).
102114 timer . complete ( undefined ) ;
103115
104116 expect ( ( ) => timer . cancel ( ) ) . not . toThrow ( ) ;
105- // Canceling a fired timer must not flip isCanceled.
117+ // Canceling a fired timer must not flip isCanceled or run the handler .
106118 expect ( timer . isCanceled ) . toBe ( false ) ;
107119 expect ( timer . isCompleted ) . toBe ( true ) ;
120+ expect ( cancelHandler ) . not . toHaveBeenCalled ( ) ;
108121 } ) ;
109122 } ) ;
110123} ) ;
0 commit comments