@@ -86,12 +86,17 @@ assert.match(markupSource, /class="input-group pi-config-only"/);
8686assert . match ( markupSource , / i d = " o c t o p u s - a c c o u n t - t o g g l e " / ) ;
8787assert . match ( markupSource , / r e l = " a p p l e - t o u c h - i c o n " / ) ;
8888assert . match ( markupSource , / r e l = " m a n i f e s t " / ) ;
89- assert . match ( markupSource , / s t y l e s \. c s s \? v = 2 0 2 6 \. 8 \. 2 4 \. 1 / ) ;
90- assert . match ( markupSource , / a p p \. j s \? v = 2 0 2 6 \. 8 \. 2 4 \. 1 / ) ;
91- assert . match ( markupSource , / B u i l d v 2 0 2 6 \. 0 8 \. 2 4 \. 1 / ) ;
89+ assert . match ( markupSource , / s t y l e s \. c s s \? v = 2 0 2 6 \. 9 \. 1 \. 1 / ) ;
90+ assert . match ( markupSource , / a p p \. j s \? v = 2 0 2 6 \. 9 \. 1 \. 1 / ) ;
91+ assert . match ( markupSource , / B u i l d v 2 0 2 6 \. 0 9 \. 0 1 \. 1 / ) ;
9292assert . match ( stylesSource , / \. l i n u x - r u n t i m e \. p i - c o n f i g - o n l y \{ d i s p l a y : n o n e ! i m p o r t a n t ; \} / ) ;
9393assert . match ( stylesSource , / \. a c c o u n t - n u m b e r - r o w \. a c c o u n t - s e c r e t - c o n t r o l \. v a l \{ m a x - w i d t h : n o n e ; w h i t e - s p a c e : n o w r a p / ) ;
9494assert . match ( appSource , / L I N U X _ O C T O P U S _ E N D P O I N T = ' \/ a p i \/ o c t o p u s ' / ) ;
95+ assert . match ( appSource , / L I N U X _ D I S P A T C H _ S N A P S H O T _ E N D P O I N T = ' \/ a p i \/ d i s p a t c h - s n a p s h o t ' / ) ;
96+ assert . match ( appSource , / s h o u l d F e t c h D i s p a t c h e s = ! w i n d o w \. l i n u x R u n t i m e \| \| w i n d o w \. l i n u x R o l e = = = ' w o r k e r ' / , 'Only the worker may query Octopus plannedDispatches on Raspberry Pi' ) ;
97+ assert . match ( appSource , / a w a i t p u b l i s h L i n u x D i s p a t c h S n a p s h o t \( d i s p a t c h e s , d i s p a t c h E n a b l e d \? ' s y n c i n g ' : ' d i s a b l e d ' \) / , 'A new worker dispatch revision must be published before FoxESS synchronization' ) ;
98+ assert . match ( appSource , / a w a i t p u b l i s h L i n u x D i s p a t c h S n a p s h o t \( [ \s \S ] * ?i s A u t o D i s p a t c h \? ' c o n f i r m e d ' : ' d i s a b l e d ' / , 'The worker must publish only a REST-confirmed scheduler result' ) ;
99+ assert . match ( appSource , / s t a r t L i n u x D i s p a t c h S n a p s h o t P o l l / , 'Dashboards must follow the worker dispatch snapshot promptly' ) ;
95100assert . match ( appSource , / L I N U X _ F O X _ L I V E _ E N D P O I N T = ' \/ a p i \/ f o x e s s \/ l i v e ' / ) ;
96101assert . match ( appSource , / s t a r t L i n u x L i v e T e l e m e t r y P o l l / ) ;
97102assert . match ( appSource , / o c t o p u s F o x e s s D a s h b o a r d A c c e s s C o d e / ) ;
@@ -113,6 +118,8 @@ assert.match(await readFile(path.join(root, 'linux', 'server.mjs'), 'utf8'), /\/
113118assert . match ( await readFile ( path . join ( root , 'linux' , 'server.mjs' ) , 'utf8' ) , / \/ a p i \/ s e r v i c e - s t a t u s / ) ;
114119assert . match ( await readFile ( path . join ( root , 'linux' , 'server.mjs' ) , 'utf8' ) , / \/ a p i \/ n a t i v e - c o n f i g / ) ;
115120assert . match ( await readFile ( path . join ( root , 'linux' , 'server.mjs' ) , 'utf8' ) , / \/ a p i \/ l i v e - d e m a n d / ) ;
121+ assert . match ( await readFile ( path . join ( root , 'linux' , 'server.mjs' ) , 'utf8' ) , / \/ a p i \/ d i s p a t c h - s n a p s h o t / ) ;
122+ assert . match ( await readFile ( path . join ( root , 'linux' , 'server.mjs' ) , 'utf8' ) , / S t a l e S m a r t D i s p a t c h s y n c h r o n i z a t i o n r e s u l t / ) ;
116123assert . match ( await readFile ( path . join ( root , 'linux' , 'server.mjs' ) , 'utf8' ) , / l a n A c c e s s R e q u i r e d / ) ;
117124assert . match ( await readFile ( path . join ( root , 'linux' , 'server.mjs' ) , 'utf8' ) , / ' C a c h e - C o n t r o l ' : ' n o - c a c h e ' / ) ;
118125assert . match ( linuxBuildSource , / o c t o p u s - f o x e s s \. d e s k t o p / ) ;
@@ -276,6 +283,79 @@ try {
276283 } ) ;
277284 assert . equal ( acceptedLiveDemand . status , 200 ) ;
278285
286+ const dispatchA = [
287+ { startDt : '2026-09-01T22:00:00.000Z' , endDt : '2026-09-01T22:30:00.000Z' }
288+ ] ;
289+ const dispatchB = [
290+ { startDt : '2026-09-02T00:00:00.000Z' , endDt : '2026-09-02T02:00:00.000Z' }
291+ ] ;
292+ const rejectedDispatchUpdate = await fetch ( `http://127.0.0.1:${ port } /api/dispatch-snapshot` , {
293+ method : 'POST' ,
294+ headers : { 'Content-Type' : 'application/json' } ,
295+ body : JSON . stringify ( { dispatches : dispatchA , status : 'syncing' } )
296+ } ) ;
297+ assert . equal ( rejectedDispatchUpdate . status , 403 , 'Only the loopback worker may publish Smart Dispatch state' ) ;
298+
299+ const workerHeaders = {
300+ 'Content-Type' : 'application/json' ,
301+ Referer : `http://127.0.0.1:${ port } /?worker=1`
302+ } ;
303+ const syncingDispatchA = await fetch ( `http://127.0.0.1:${ port } /api/dispatch-snapshot` , {
304+ method : 'POST' ,
305+ headers : workerHeaders ,
306+ body : JSON . stringify ( { dispatches : dispatchA , status : 'syncing' , fetchedAt : '2026-09-01T20:00:00.000Z' } )
307+ } ) ;
308+ assert . equal ( syncingDispatchA . status , 200 ) ;
309+ const dispatchAState = await syncingDispatchA . json ( ) ;
310+ assert . equal ( dispatchAState . status , 'syncing' ) ;
311+ assert . deepEqual ( dispatchAState . dispatches , dispatchA ) ;
312+
313+ const unauthorizedDispatchRead = await fetch ( `http://127.0.0.1:${ port } /api/dispatch-snapshot` , {
314+ headers : { Referer : `http://127.0.0.1:${ port } /?client=1` }
315+ } ) ;
316+ assert . equal ( unauthorizedDispatchRead . status , 401 ) ;
317+ const authorizedDispatchA = await fetch ( `http://127.0.0.1:${ port } /api/dispatch-snapshot` , {
318+ headers : {
319+ Referer : `http://127.0.0.1:${ port } /?client=1` ,
320+ 'X-Octopus-Access-Key' : changedAccessKey
321+ }
322+ } ) . then ( response => response . json ( ) ) ;
323+ assert . equal ( authorizedDispatchA . status , 'syncing' ) ;
324+ assert . equal ( authorizedDispatchA . fetchedAt , '2026-09-01T20:00:00.000Z' ) ;
325+
326+ const syncingDispatchB = await fetch ( `http://127.0.0.1:${ port } /api/dispatch-snapshot` , {
327+ method : 'POST' ,
328+ headers : workerHeaders ,
329+ body : JSON . stringify ( { dispatches : dispatchB , status : 'syncing' } )
330+ } ) ;
331+ assert . equal ( syncingDispatchB . status , 200 ) ;
332+ const staleDispatchAConfirmation = await fetch ( `http://127.0.0.1:${ port } /api/dispatch-snapshot` , {
333+ method : 'POST' ,
334+ headers : workerHeaders ,
335+ body : JSON . stringify ( { dispatches : dispatchA , status : 'confirmed' , scheduleFingerprint : 'schedule-a' } )
336+ } ) ;
337+ assert . equal ( staleDispatchAConfirmation . status , 409 , 'A stale FoxESS confirmation must not replace a newer Octopus revision' ) ;
338+ const confirmedDispatchB = await fetch ( `http://127.0.0.1:${ port } /api/dispatch-snapshot` , {
339+ method : 'POST' ,
340+ headers : workerHeaders ,
341+ body : JSON . stringify ( { dispatches : dispatchB , status : 'confirmed' , scheduleFingerprint : 'schedule-b' } )
342+ } ) ;
343+ assert . equal ( confirmedDispatchB . status , 200 ) ;
344+ const confirmedDispatchState = await confirmedDispatchB . json ( ) ;
345+ assert . equal ( confirmedDispatchState . status , 'confirmed' ) ;
346+ assert . equal ( confirmedDispatchState . scheduleFingerprint , 'schedule-b' ) ;
347+ assert . ok ( confirmedDispatchState . confirmedAt ) ;
348+
349+ const invalidDispatchUpdate = await fetch ( `http://127.0.0.1:${ port } /api/dispatch-snapshot` , {
350+ method : 'POST' ,
351+ headers : workerHeaders ,
352+ body : JSON . stringify ( {
353+ dispatches : [ { startDt : 'invalid' , endDt : 'also-invalid' } ] ,
354+ status : 'syncing'
355+ } )
356+ } ) ;
357+ assert . equal ( invalidDispatchUpdate . status , 400 ) ;
358+
279359 const nativeConfig = await fetch ( `http://127.0.0.1:${ port } /api/native-config` ) . then ( response => response . json ( ) ) ;
280360 assert . equal ( nativeConfig . accessRequired , true ) ;
281361 assert . equal ( nativeConfig . accessKey , changedAccessKey ) ;
0 commit comments