@@ -109,6 +109,9 @@ describe('Preview preflight jsdom integration', () => {
109109 d : unknown ,
110110 ) => d ;
111111 ( win as unknown as Record < string , unknown > ) . __mockConfigData = { } ;
112+ // Mock HEAD probe as 200 so the self-heal preflight proceeds to inject
113+ ( win as unknown as Record < string , unknown > ) . fetch = ( ) =>
114+ Promise . resolve ( { ok : true , status : 200 } ) ;
112115
113116 const script = win . document . createElement ( 'script' ) ;
114117 script . textContent = extractEvaluableScript ( entry ) ;
@@ -156,6 +159,9 @@ describe('Preview preflight jsdom integration', () => {
156159 d : unknown ,
157160 ) => d ;
158161 ( win as unknown as Record < string , unknown > ) . __mockConfigData = { } ;
162+ // Mock HEAD probe as 200 so self-heal proceeds with injection
163+ ( win as unknown as Record < string , unknown > ) . fetch = ( ) =>
164+ Promise . resolve ( { ok : true , status : 200 } ) ;
159165
160166 const script = win . document . createElement ( 'script' ) ;
161167 script . textContent = extractEvaluableScript ( entry ) ;
@@ -232,6 +238,9 @@ describe('Preview preflight jsdom integration', () => {
232238 d : unknown ,
233239 ) => d ;
234240 ( win as unknown as Record < string , unknown > ) . __mockConfigData = { } ;
241+ // Mock HEAD probe as 200 so self-heal proceeds with injection
242+ ( win as unknown as Record < string , unknown > ) . fetch = ( ) =>
243+ Promise . resolve ( { ok : true , status : 200 } ) ;
235244
236245 const script = win . document . createElement ( 'script' ) ;
237246 script . textContent = extractEvaluableScript ( entry ) ;
@@ -256,6 +265,165 @@ describe('Preview preflight jsdom integration', () => {
256265 } ) ;
257266 } ) ;
258267
268+ describe ( '404 self-heal (missing preview bundle)' , ( ) => {
269+ it ( 'clears cookie and falls through to startFlow when HEAD returns 404' , async ( ) => {
270+ const token = 'k9x2m4p7abcd' ;
271+ const entry = generateWrapEntry ( './skeleton.mjs' , {
272+ previewOrigin : 'cdn.walkeros.io' ,
273+ previewScope : 'proj_abc' ,
274+ } ) ;
275+
276+ // Pre-set the cookie so the cookie-valid branch is taken (no query param)
277+ const dom = createDom ( 'https://example.com/page' ) ;
278+ const win = dom . window ;
279+ win . document . cookie = `elbPreview=${ token } ; path=/` ;
280+
281+ let startFlowCalled = false ;
282+ ( win as unknown as Record < string , unknown > ) . __mockStartFlow = ( ) => {
283+ startFlowCalled = true ;
284+ return Promise . resolve ( { collector : { } , elb : ( ) => { } } ) ;
285+ } ;
286+ ( win as unknown as Record < string , unknown > ) . __mockWireConfig = (
287+ d : unknown ,
288+ ) => d ;
289+ ( win as unknown as Record < string , unknown > ) . __mockConfigData = { } ;
290+
291+ const fetchCalls : Array < { url : string ; method ?: string } > = [ ] ;
292+ ( win as unknown as Record < string , unknown > ) . fetch = (
293+ url : string ,
294+ init ?: { method ?: string } ,
295+ ) => {
296+ fetchCalls . push ( { url, method : init ?. method } ) ;
297+ return Promise . resolve ( { ok : false , status : 404 } ) ;
298+ } ;
299+
300+ const script = win . document . createElement ( 'script' ) ;
301+ script . textContent = extractEvaluableScript ( entry ) ;
302+ win . document . body . appendChild ( script ) ;
303+
304+ // Wait for async fetch + IIFE chain to settle
305+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
306+
307+ // HEAD was issued to the preview URL
308+ expect ( fetchCalls . length ) . toBe ( 1 ) ;
309+ expect ( fetchCalls [ 0 ] . method ) . toBe ( 'HEAD' ) ;
310+ expect ( fetchCalls [ 0 ] . url ) . toBe (
311+ `https://cdn.walkeros.io/preview/proj_abc/walker.${ token } .js` ,
312+ ) ;
313+
314+ // No preview <script> injected
315+ const injected = win . document . querySelectorAll ( 'head > script[src]' ) ;
316+ expect ( injected . length ) . toBe ( 0 ) ;
317+
318+ // Cookie cleared (max-age=0 effectively removes it)
319+ expect ( win . document . cookie ) . not . toContain ( `elbPreview=${ token } ` ) ;
320+
321+ // Production path runs (fall-through to startFlow)
322+ expect ( startFlowCalled ) . toBe ( true ) ;
323+
324+ win . close ( ) ;
325+ } ) ;
326+
327+ it ( 'clears cookie and falls through to startFlow when fetch rejects (network error)' , async ( ) => {
328+ const token = 'Ab3Df5Gh7j9L' ;
329+ const entry = generateWrapEntry ( './skeleton.mjs' , {
330+ previewOrigin : 'cdn.walkeros.io' ,
331+ previewScope : 'proj_abc' ,
332+ } ) ;
333+
334+ const dom = createDom ( 'https://example.com/page' ) ;
335+ const win = dom . window ;
336+ win . document . cookie = `elbPreview=${ token } ; path=/` ;
337+
338+ let startFlowCalled = false ;
339+ ( win as unknown as Record < string , unknown > ) . __mockStartFlow = ( ) => {
340+ startFlowCalled = true ;
341+ return Promise . resolve ( { collector : { } , elb : ( ) => { } } ) ;
342+ } ;
343+ ( win as unknown as Record < string , unknown > ) . __mockWireConfig = (
344+ d : unknown ,
345+ ) => d ;
346+ ( win as unknown as Record < string , unknown > ) . __mockConfigData = { } ;
347+
348+ ( win as unknown as Record < string , unknown > ) . fetch = ( ) =>
349+ Promise . reject ( new Error ( 'network failure' ) ) ;
350+
351+ const script = win . document . createElement ( 'script' ) ;
352+ script . textContent = extractEvaluableScript ( entry ) ;
353+ win . document . body . appendChild ( script ) ;
354+
355+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
356+
357+ // No preview <script> injected
358+ const injected = win . document . querySelectorAll ( 'head > script[src]' ) ;
359+ expect ( injected . length ) . toBe ( 0 ) ;
360+
361+ // Cookie cleared
362+ expect ( win . document . cookie ) . not . toContain ( `elbPreview=${ token } ` ) ;
363+
364+ // Production path runs (fall-through to startFlow)
365+ expect ( startFlowCalled ) . toBe ( true ) ;
366+
367+ win . close ( ) ;
368+ } ) ;
369+
370+ it ( 'injects preview script and skips startFlow when HEAD returns 200' , async ( ) => {
371+ const token = 'k9x2m4p7abcd' ;
372+ const entry = generateWrapEntry ( './skeleton.mjs' , {
373+ previewOrigin : 'cdn.walkeros.io' ,
374+ previewScope : 'proj_abc' ,
375+ } ) ;
376+
377+ const dom = createDom ( 'https://example.com/page' ) ;
378+ const win = dom . window ;
379+ win . document . cookie = `elbPreview=${ token } ; path=/` ;
380+
381+ let startFlowCalled = false ;
382+ ( win as unknown as Record < string , unknown > ) . __mockStartFlow = ( ) => {
383+ startFlowCalled = true ;
384+ return Promise . resolve ( { collector : { } , elb : ( ) => { } } ) ;
385+ } ;
386+ ( win as unknown as Record < string , unknown > ) . __mockWireConfig = (
387+ d : unknown ,
388+ ) => d ;
389+ ( win as unknown as Record < string , unknown > ) . __mockConfigData = { } ;
390+
391+ const fetchCalls : Array < { url : string ; method ?: string } > = [ ] ;
392+ ( win as unknown as Record < string , unknown > ) . fetch = (
393+ url : string ,
394+ init ?: { method ?: string } ,
395+ ) => {
396+ fetchCalls . push ( { url, method : init ?. method } ) ;
397+ return Promise . resolve ( { ok : true , status : 200 } ) ;
398+ } ;
399+
400+ const script = win . document . createElement ( 'script' ) ;
401+ script . textContent = extractEvaluableScript ( entry ) ;
402+ win . document . body . appendChild ( script ) ;
403+
404+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
405+
406+ // HEAD probe fired
407+ expect ( fetchCalls . length ) . toBe ( 1 ) ;
408+ expect ( fetchCalls [ 0 ] . method ) . toBe ( 'HEAD' ) ;
409+
410+ // Preview script injected
411+ const injected = win . document . querySelectorAll ( 'head > script[src]' ) ;
412+ expect ( injected . length ) . toBe ( 1 ) ;
413+ expect ( ( injected [ 0 ] as HTMLScriptElement ) . src ) . toBe (
414+ `https://cdn.walkeros.io/preview/proj_abc/walker.${ token } .js` ,
415+ ) ;
416+
417+ // Cookie preserved
418+ expect ( win . document . cookie ) . toContain ( `elbPreview=${ token } ` ) ;
419+
420+ // startFlow NOT called (preview took over)
421+ expect ( startFlowCalled ) . toBe ( false ) ;
422+
423+ win . close ( ) ;
424+ } ) ;
425+ } ) ;
426+
259427 describe ( 'regression: no preflight without preview options' , ( ) => {
260428 it ( 'does NOT inject any preflight code when no preview options' , async ( ) => {
261429 const entry = generateWrapEntry ( './skeleton.mjs' , {
0 commit comments