@@ -77,9 +77,51 @@ function nextMinute(schedule, afterMs) {
7777 if ( schedule . expression !== '* * * * *' || schedule . timezone !== 'UTC' ) {
7878 throw new Error ( 'unsupported test schedule' ) ;
7979 }
80- return Math . floor ( afterMs / 60_000 + 1 ) * 60_000 ;
80+ // This fixture tests replica claims, not calendar cron evaluation. A relative
81+ // minute keeps initialization in the future even when setup crosses :00;
82+ // the admission phase explicitly makes next_fire_at_ms due in PostgreSQL.
83+ return afterMs + 60_000 ;
8184}
8285
86+ test ( 'replica fixture initializes independently of wall-clock minute boundaries' , ( ) => {
87+ const {
88+ resolveLocalScheduleDecision,
89+ } = require ( '@qinglong/runtime-core/local-scheduler' ) ;
90+ for ( const offset of [ 0 , 1 , 999 , 1_000 , 59_999 ] ) {
91+ const observedAtMs = 120_000 + offset ;
92+ const candidate = {
93+ projectId : 'default' ,
94+ triggerId : 'trigger-multi-replica' ,
95+ triggerRevision : 1 ,
96+ triggerContentDigest : 'a' . repeat ( 64 ) ,
97+ triggerUpdatedAtMs : observedAtMs - 999 ,
98+ taskId : 'task-multi-replica' ,
99+ taskRevision : 1 ,
100+ taskContentDigest : 'b' . repeat ( 64 ) ,
101+ expression : '* * * * *' ,
102+ timezone : 'UTC' ,
103+ misfirePolicy : 'skip' ,
104+ stateVersion : 0 ,
105+ nextFireAtMs : null ,
106+ } ;
107+ assert . equal (
108+ resolveLocalScheduleDecision ( candidate , observedAtMs , 5_000 , nextMinute )
109+ . disposition ,
110+ 'initialize' ,
111+ `minute offset ${ offset } ` ,
112+ ) ;
113+ assert . equal (
114+ resolveLocalScheduleDecision (
115+ { ...candidate , nextFireAtMs : observedAtMs } ,
116+ observedAtMs ,
117+ 5_000 ,
118+ nextMinute ,
119+ ) . disposition ,
120+ 'admit' ,
121+ ) ;
122+ }
123+ } ) ;
124+
83125function workerRequest ( address , requestPath , token , body ) {
84126 const payload = Buffer . from ( JSON . stringify ( body ) ) ;
85127 return new Promise ( ( resolve , reject ) => {
0 commit comments