@@ -245,58 +245,54 @@ describe('PostStartInjector', () => {
245245 expect ( kubeConfigApi . injectKubeConfig ) . toHaveBeenCalledTimes ( 1 ) ;
246246 } ) ;
247247
248- // ── parallel polling ─────────────────────────── ───────────────────────────
248+ // ── polling fallback (only after watch failure) ───────────────────────────
249249
250- describe ( 'parallel polling' , ( ) => {
251- test ( 'polls every 2 s alongside the watch ' , ( ) => {
250+ describe ( 'polling fallback ' , ( ) => {
251+ test ( 'does not poll while watch is healthy ' , ( ) => {
252252 invoke ( ) ;
253253
254- // Nothing before the first interval elapses
255- jest . advanceTimersByTime ( 1999 ) ;
256- expect ( devworkspaceApi . getByName ) . toHaveBeenCalledTimes ( 1 ) ; // only initial check
257-
258- jest . advanceTimersByTime ( 1 ) ;
259- // Initial check + first poll
260- expect ( devworkspaceApi . getByName ) . toHaveBeenCalledTimes ( 2 ) ;
261-
262- jest . advanceTimersByTime ( 2000 ) ;
263- expect ( devworkspaceApi . getByName ) . toHaveBeenCalledTimes ( 3 ) ;
254+ jest . advanceTimersByTime ( 10000 ) ;
255+ // Only the initial check, no polling
256+ expect ( devworkspaceApi . getByName ) . toHaveBeenCalledTimes ( 1 ) ;
264257 } ) ;
265258
266- test ( 'injects via poll when watch is silent ' , async ( ) => {
259+ test ( 'starts polling after watch ERROR and injects via poll ' , async ( ) => {
267260 ( devworkspaceApi . getByName as jest . Mock )
268261 . mockResolvedValueOnce ( { status : { phase : 'Starting' } } ) // initial check
269262 . mockResolvedValueOnce ( { status : { phase : 'Starting' } } ) // poll #1
270- . mockResolvedValue ( { status : { phase : 'Running' , devworkspaceId : 'ws-poll-id ' } } ) ;
263+ . mockResolvedValue ( { status : { phase : 'Running' , devworkspaceId : 'ws-after-err ' } } ) ;
271264
272265 invoke ( ) ;
273266 await flushMicrotasks ( ) ;
274267
268+ await capturedListener ( errorMessage ( ) ) ;
269+ expect ( devworkspaceApi . stopWatching ) . toHaveBeenCalled ( ) ;
270+ expect ( logger . info ) . toHaveBeenCalledWith ( expect . stringContaining ( 'falling back to polling' ) ) ;
271+
275272 jest . advanceTimersByTime ( 2000 ) ;
276273 await flushMicrotasks ( ) ;
277274 expect ( kubeConfigApi . injectKubeConfig ) . not . toHaveBeenCalled ( ) ;
278275
279276 jest . advanceTimersByTime ( 2000 ) ;
280277 await flushMicrotasks ( ) ;
281-
282- expect ( kubeConfigApi . injectKubeConfig ) . toHaveBeenCalledWith ( namespace , 'ws-poll-id' ) ;
283- expect ( podmanApi . podmanLogin ) . toHaveBeenCalledWith ( namespace , 'ws-poll-id' ) ;
278+ expect ( kubeConfigApi . injectKubeConfig ) . toHaveBeenCalledWith ( namespace , 'ws-after-err' ) ;
284279 expect ( ( PostStartInjector as any ) . activeWatches . has ( key ) ) . toBe ( false ) ;
285280 } ) ;
286281
287- test ( 'does not double-inject when watch and poll both detect Running' , async ( ) => {
282+ test ( 'starts polling after watchInNamespace rejection' , async ( ) => {
283+ ( devworkspaceApi . watchInNamespace as jest . Mock ) . mockRejectedValue (
284+ new Error ( 'watch rejected' ) ,
285+ ) ;
288286 ( devworkspaceApi . getByName as jest . Mock )
289287 . mockResolvedValueOnce ( { status : { phase : 'Starting' } } ) // initial check
290- . mockResolvedValue ( { status : { phase : 'Running' , devworkspaceId : 'ws-both ' } } ) ;
288+ . mockResolvedValue ( { status : { phase : 'Running' , devworkspaceId : 'ws-reject-id ' } } ) ;
291289
292290 invoke ( ) ;
293291 await flushMicrotasks ( ) ;
294292
295- await capturedListener ( dwMessage ( 'Running' , 'ws-both' ) ) ;
296293 jest . advanceTimersByTime ( 2000 ) ;
297294 await flushMicrotasks ( ) ;
298-
299- expect ( kubeConfigApi . injectKubeConfig ) . toHaveBeenCalledTimes ( 1 ) ;
295+ expect ( kubeConfigApi . injectKubeConfig ) . toHaveBeenCalledWith ( namespace , 'ws-reject-id' ) ;
300296 } ) ;
301297
302298 test . each ( [ 'Failed' , 'Failing' , 'Stopped' , 'Stopping' , 'Terminating' ] ) (
@@ -309,6 +305,9 @@ describe('PostStartInjector', () => {
309305 invoke ( ) ;
310306 await flushMicrotasks ( ) ;
311307
308+ // Trigger watch failure to start polling
309+ await capturedListener ( errorMessage ( ) ) ;
310+
312311 jest . advanceTimersByTime ( 2000 ) ;
313312 await flushMicrotasks ( ) ;
314313
@@ -326,59 +325,26 @@ describe('PostStartInjector', () => {
326325 invoke ( ) ;
327326 await flushMicrotasks ( ) ;
328327
329- jest . advanceTimersByTime ( 2000 ) ;
330- await flushMicrotasks ( ) ;
331- expect ( kubeConfigApi . injectKubeConfig ) . not . toHaveBeenCalled ( ) ;
332-
333- jest . advanceTimersByTime ( 2000 ) ;
334- await flushMicrotasks ( ) ;
335- expect ( kubeConfigApi . injectKubeConfig ) . toHaveBeenCalledWith ( namespace , 'ws-retry-id' ) ;
336- } ) ;
337-
338- test ( 'polling continues after watch ERROR' , async ( ) => {
339- ( devworkspaceApi . getByName as jest . Mock )
340- . mockResolvedValueOnce ( { status : { phase : 'Starting' } } ) // initial check
341- . mockResolvedValueOnce ( { status : { phase : 'Starting' } } ) // poll #1
342- . mockResolvedValue ( { status : { phase : 'Running' , devworkspaceId : 'ws-after-err' } } ) ;
343-
344- invoke ( ) ;
345- await flushMicrotasks ( ) ;
346-
328+ // Trigger watch failure to start polling
347329 await capturedListener ( errorMessage ( ) ) ;
348- expect ( devworkspaceApi . stopWatching ) . toHaveBeenCalled ( ) ;
349330
350331 jest . advanceTimersByTime ( 2000 ) ;
351332 await flushMicrotasks ( ) ;
352333 expect ( kubeConfigApi . injectKubeConfig ) . not . toHaveBeenCalled ( ) ;
353334
354335 jest . advanceTimersByTime ( 2000 ) ;
355336 await flushMicrotasks ( ) ;
356- expect ( kubeConfigApi . injectKubeConfig ) . toHaveBeenCalledWith ( namespace , 'ws-after-err ' ) ;
337+ expect ( kubeConfigApi . injectKubeConfig ) . toHaveBeenCalledWith ( namespace , 'ws-retry-id ' ) ;
357338 } ) ;
358339 } ) ;
359340
360341 // ── elapsed time logging ────────────────────────────────────────────────
361342
362- test ( 'logs elapsed time when injection succeeds quickly ' , async ( ) => {
343+ test ( 'logs elapsed time when injection succeeds' , async ( ) => {
363344 invoke ( ) ;
364345 await capturedListener ( dwMessage ( 'Running' , 'ws-123' ) ) ;
365346
366347 expect ( logger . info ) . toHaveBeenCalledWith ( expect . stringContaining ( '0s after start request' ) ) ;
367- expect ( logger . warn ) . not . toHaveBeenCalledWith (
368- expect . stringContaining ( 'exceeds the ~12s UDI entrypoint.sh window' ) ,
369- ) ;
370- } ) ;
371-
372- test ( 'warns when injection exceeds the 12 s UDI entrypoint window' , async ( ) => {
373- invoke ( ) ;
374-
375- jest . advanceTimersByTime ( 15000 ) ;
376- await capturedListener ( dwMessage ( 'Running' , 'ws-slow' ) ) ;
377-
378- expect ( logger . warn ) . toHaveBeenCalledWith (
379- expect . stringContaining ( 'exceeds the ~12s UDI entrypoint.sh window' ) ,
380- ) ;
381- expect ( logger . warn ) . toHaveBeenCalledWith ( expect . stringContaining ( 'Detection source: watch' ) ) ;
382348 } ) ;
383349
384350 // ── ownership guard (stale callback safety) ───────────────────────────────
0 commit comments