From f9325772bdedeb0edc92cf62c67d730111d216ea Mon Sep 17 00:00:00 2001 From: privilegedescalation-engineer Date: Wed, 25 Mar 2026 01:55:02 +0000 Subject: [PATCH 1/9] fix(e2e): use specific regex for nodes page heading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /node/i regex was too broad and matched both the page heading 'Intel GPU — Nodes' and the empty state 'No GPU Nodes Found', causing a strict mode violation in Playwright. Use /intel gpu.*nodes/i to match only the actual page heading, which contains 'Intel GPU' before 'Nodes'. --- e2e/intel-gpu.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/intel-gpu.spec.ts b/e2e/intel-gpu.spec.ts index 7b58492..8c3ef5c 100644 --- a/e2e/intel-gpu.spec.ts +++ b/e2e/intel-gpu.spec.ts @@ -66,7 +66,7 @@ test.describe('Intel GPU plugin smoke tests', () => { }); await page.goto('/c/main/intel-gpu/nodes'); - await expect(page.getByRole('heading', { name: /node/i })).toBeVisible({ timeout: 15_000 }); + await expect(page.getByRole('heading', { name: /intel gpu.*nodes/i })).toBeVisible({ timeout: 15_000 }); await page.goto('/c/main/intel-gpu/pods'); await expect(page.getByRole('heading', { name: /pod/i })).toBeVisible({ timeout: 15_000 }); -- 2.52.0 From 5670c008e12d83f50da14e806a53048aeedf06d3 Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Wed, 25 Mar 2026 05:57:15 +0000 Subject: [PATCH 2/9] fix: add request timeout wrapper to prevent E2E test hang Add withTimeout() helper that wraps ApiProxy.request calls with a 2s timeout. This prevents the plugin from hanging indefinitely when CRD requests fail or network issues occur in the E2E environment. Root cause: ApiProxy.request to non-existent CRDs would hang forever, causing the Loading Intel GPU data... progressbar to never resolve. Co-Authored-By: Paperclip --- src/api/IntelGpuDataContext.tsx | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/api/IntelGpuDataContext.tsx b/src/api/IntelGpuDataContext.tsx index bf724a9..6ae9a4c 100644 --- a/src/api/IntelGpuDataContext.tsx +++ b/src/api/IntelGpuDataContext.tsx @@ -69,6 +69,18 @@ export function useIntelGpuContext(): IntelGpuContextValue { // Helpers // --------------------------------------------------------------------------- +const DEFAULT_REQUEST_TIMEOUT_MS = 2_000; + +/** Wraps a promise with a timeout, rejecting if it doesn't settle within ms. */ +function withTimeout(promise: Promise, ms: number): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`Request timed out after ${ms}ms`)), ms) + ), + ]); +} + /** Extract raw Kubernetes JSON from Headlamp KubeObject wrappers. */ const extractJsonData = (items: unknown[]): unknown[] => items.map(item => @@ -108,8 +120,11 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode } try { // GpuDevicePlugin CRDs — graceful degradation if CRD not installed try { - const pluginList = await ApiProxy.request( - `/apis/${INTEL_DEVICE_PLUGIN_API_GROUP}/${INTEL_DEVICE_PLUGIN_API_VERSION}/gpudeviceplugins` + const pluginList = await withTimeout( + ApiProxy.request( + `/apis/${INTEL_DEVICE_PLUGIN_API_GROUP}/${INTEL_DEVICE_PLUGIN_API_VERSION}/gpudeviceplugins` + ), + DEFAULT_REQUEST_TIMEOUT_MS ); if (!cancelled && isKubeList(pluginList)) { setCrdAvailable(true); @@ -139,7 +154,10 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode } for (const url of pluginPodSelectors) { try { - const list = await ApiProxy.request(url); + const list = await withTimeout( + ApiProxy.request(url), + DEFAULT_REQUEST_TIMEOUT_MS + ); if (!cancelled && isKubeList(list)) { const gpuPluginPods = filterIntelGpuPluginPods(list.items); foundPluginPods.push(...gpuPluginPods); -- 2.52.0 From 0d5f65176b7066a178c618d8f3d57fec00114d18 Mon Sep 17 00:00:00 2001 From: "privilegedescalation-ceo[bot]" Date: Wed, 25 Mar 2026 07:06:07 +0000 Subject: [PATCH 3/9] ci: re-trigger workflows after Actions approval setting change -- 2.52.0 From 66932958b16f46df4ace37e3a04998043bf5b9cb Mon Sep 17 00:00:00 2001 From: privilegedescalation-engineer Date: Wed, 25 Mar 2026 07:18:19 +0000 Subject: [PATCH 4/9] fix: reformat withTimeout call and add unit test for timeout behavior - Reformat withTimeout call to single line (prettier) - Add unit test for CRD timeout behavior (crdAvailable=false when API fails) Co-Authored-By: Paperclip --- src/api/IntelGpuDataContext.test.tsx | 25 +++++++++++++++++++++++++ src/api/IntelGpuDataContext.tsx | 5 +---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/api/IntelGpuDataContext.test.tsx b/src/api/IntelGpuDataContext.test.tsx index 631a395..4eb9750 100644 --- a/src/api/IntelGpuDataContext.test.tsx +++ b/src/api/IntelGpuDataContext.test.tsx @@ -151,4 +151,29 @@ describe('IntelGpuDataProvider', () => { expect(callCountAfter).toBeGreaterThan(callCountBefore); }); }); + + it('treats a hanging CRD request as unavailable after 2s timeout', async () => { + vi.useFakeTimers(); + const nodeWrapper = { jsonData: {} }; + vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([ + [nodeWrapper], + null, + ] as any); + vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([ + [nodeWrapper], + null, + ] as any); + vi.mocked(ApiProxy.request).mockRejectedValue( + new Error('Request timed out after 2000ms') + ); + + const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); + + await act(async () => { + await vi.advanceTimersByTimeAsync(100); + }); + expect(result.current.loading).toBe(false); + expect(result.current.crdAvailable).toBe(false); + vi.useRealTimers(); + }); }); diff --git a/src/api/IntelGpuDataContext.tsx b/src/api/IntelGpuDataContext.tsx index 6ae9a4c..bd4beb8 100644 --- a/src/api/IntelGpuDataContext.tsx +++ b/src/api/IntelGpuDataContext.tsx @@ -154,10 +154,7 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode } for (const url of pluginPodSelectors) { try { - const list = await withTimeout( - ApiProxy.request(url), - DEFAULT_REQUEST_TIMEOUT_MS - ); + const list = await withTimeout(ApiProxy.request(url), DEFAULT_REQUEST_TIMEOUT_MS); if (!cancelled && isKubeList(list)) { const gpuPluginPods = filterIntelGpuPluginPods(list.items); foundPluginPods.push(...gpuPluginPods); -- 2.52.0 From 66575982afb9c08962e48bd768ff83f0c13c003b Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Wed, 25 Mar 2026 05:57:15 +0000 Subject: [PATCH 5/9] fix: add request timeout wrapper to prevent E2E test hang Add withTimeout() helper that wraps ApiProxy.request calls with a 2s timeout. This prevents the plugin from hanging indefinitely when CRD requests fail or network issues occur in the E2E environment. Root cause: ApiProxy.request to non-existent CRDs would hang forever, causing the Loading Intel GPU data... progressbar to never resolve. Co-Authored-By: Paperclip --- src/api/IntelGpuDataContext.tsx | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/api/IntelGpuDataContext.tsx b/src/api/IntelGpuDataContext.tsx index bf724a9..6ae9a4c 100644 --- a/src/api/IntelGpuDataContext.tsx +++ b/src/api/IntelGpuDataContext.tsx @@ -69,6 +69,18 @@ export function useIntelGpuContext(): IntelGpuContextValue { // Helpers // --------------------------------------------------------------------------- +const DEFAULT_REQUEST_TIMEOUT_MS = 2_000; + +/** Wraps a promise with a timeout, rejecting if it doesn't settle within ms. */ +function withTimeout(promise: Promise, ms: number): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`Request timed out after ${ms}ms`)), ms) + ), + ]); +} + /** Extract raw Kubernetes JSON from Headlamp KubeObject wrappers. */ const extractJsonData = (items: unknown[]): unknown[] => items.map(item => @@ -108,8 +120,11 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode } try { // GpuDevicePlugin CRDs — graceful degradation if CRD not installed try { - const pluginList = await ApiProxy.request( - `/apis/${INTEL_DEVICE_PLUGIN_API_GROUP}/${INTEL_DEVICE_PLUGIN_API_VERSION}/gpudeviceplugins` + const pluginList = await withTimeout( + ApiProxy.request( + `/apis/${INTEL_DEVICE_PLUGIN_API_GROUP}/${INTEL_DEVICE_PLUGIN_API_VERSION}/gpudeviceplugins` + ), + DEFAULT_REQUEST_TIMEOUT_MS ); if (!cancelled && isKubeList(pluginList)) { setCrdAvailable(true); @@ -139,7 +154,10 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode } for (const url of pluginPodSelectors) { try { - const list = await ApiProxy.request(url); + const list = await withTimeout( + ApiProxy.request(url), + DEFAULT_REQUEST_TIMEOUT_MS + ); if (!cancelled && isKubeList(list)) { const gpuPluginPods = filterIntelGpuPluginPods(list.items); foundPluginPods.push(...gpuPluginPods); -- 2.52.0 From 52b1429ba0ef77c7624a7ee622b1c4a008d761ea Mon Sep 17 00:00:00 2001 From: privilegedescalation-engineer Date: Wed, 25 Mar 2026 07:18:19 +0000 Subject: [PATCH 6/9] fix: reformat withTimeout call and add unit test for timeout behavior - Reformat withTimeout call to single line (prettier) - Add unit test for CRD timeout behavior (crdAvailable=false when API fails) Co-Authored-By: Paperclip --- src/api/IntelGpuDataContext.test.tsx | 25 +++++++++++++++++++++++++ src/api/IntelGpuDataContext.tsx | 5 +---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/api/IntelGpuDataContext.test.tsx b/src/api/IntelGpuDataContext.test.tsx index 631a395..4eb9750 100644 --- a/src/api/IntelGpuDataContext.test.tsx +++ b/src/api/IntelGpuDataContext.test.tsx @@ -151,4 +151,29 @@ describe('IntelGpuDataProvider', () => { expect(callCountAfter).toBeGreaterThan(callCountBefore); }); }); + + it('treats a hanging CRD request as unavailable after 2s timeout', async () => { + vi.useFakeTimers(); + const nodeWrapper = { jsonData: {} }; + vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([ + [nodeWrapper], + null, + ] as any); + vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([ + [nodeWrapper], + null, + ] as any); + vi.mocked(ApiProxy.request).mockRejectedValue( + new Error('Request timed out after 2000ms') + ); + + const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); + + await act(async () => { + await vi.advanceTimersByTimeAsync(100); + }); + expect(result.current.loading).toBe(false); + expect(result.current.crdAvailable).toBe(false); + vi.useRealTimers(); + }); }); diff --git a/src/api/IntelGpuDataContext.tsx b/src/api/IntelGpuDataContext.tsx index 6ae9a4c..bd4beb8 100644 --- a/src/api/IntelGpuDataContext.tsx +++ b/src/api/IntelGpuDataContext.tsx @@ -154,10 +154,7 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode } for (const url of pluginPodSelectors) { try { - const list = await withTimeout( - ApiProxy.request(url), - DEFAULT_REQUEST_TIMEOUT_MS - ); + const list = await withTimeout(ApiProxy.request(url), DEFAULT_REQUEST_TIMEOUT_MS); if (!cancelled && isKubeList(list)) { const gpuPluginPods = filterIntelGpuPluginPods(list.items); foundPluginPods.push(...gpuPluginPods); -- 2.52.0 From 957cf144a746224cf4704fd8c17d29dda075fdcc Mon Sep 17 00:00:00 2001 From: privilegedescalation-engineer Date: Wed, 25 Mar 2026 07:21:22 +0000 Subject: [PATCH 7/9] fix: reapply formatting after rebase Co-Authored-By: Paperclip --- src/api/IntelGpuDataContext.test.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/api/IntelGpuDataContext.test.tsx b/src/api/IntelGpuDataContext.test.tsx index 4eb9750..dc61f85 100644 --- a/src/api/IntelGpuDataContext.test.tsx +++ b/src/api/IntelGpuDataContext.test.tsx @@ -155,17 +155,9 @@ describe('IntelGpuDataProvider', () => { it('treats a hanging CRD request as unavailable after 2s timeout', async () => { vi.useFakeTimers(); const nodeWrapper = { jsonData: {} }; - vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([ - [nodeWrapper], - null, - ] as any); - vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([ - [nodeWrapper], - null, - ] as any); - vi.mocked(ApiProxy.request).mockRejectedValue( - new Error('Request timed out after 2000ms') - ); + vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[nodeWrapper], null] as any); + vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[nodeWrapper], null] as any); + vi.mocked(ApiProxy.request).mockRejectedValue(new Error('Request timed out after 2000ms')); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); -- 2.52.0 From 3aa9c15e800fb66ce5a4f3c34b6ab2b2fc474128 Mon Sep 17 00:00:00 2001 From: privilegedescalation-engineer Date: Wed, 25 Mar 2026 07:41:47 +0000 Subject: [PATCH 8/9] fix test: use never-resolving promise and fake timers for withTimeout The previous mock used mockRejectedValue which immediately rejects, so Promise.race resolved before withTimeout's setTimeout fired. Now we use new Promise(() => {}) to simulate a hanging request and advance timers to properly exercise the 2s timeout logic. Co-Authored-By: Paperclip --- src/api/IntelGpuDataContext.test.tsx | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/api/IntelGpuDataContext.test.tsx b/src/api/IntelGpuDataContext.test.tsx index 4eb9750..a17431f 100644 --- a/src/api/IntelGpuDataContext.test.tsx +++ b/src/api/IntelGpuDataContext.test.tsx @@ -155,25 +155,23 @@ describe('IntelGpuDataProvider', () => { it('treats a hanging CRD request as unavailable after 2s timeout', async () => { vi.useFakeTimers(); const nodeWrapper = { jsonData: {} }; - vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([ - [nodeWrapper], - null, - ] as any); - vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([ - [nodeWrapper], - null, - ] as any); - vi.mocked(ApiProxy.request).mockRejectedValue( - new Error('Request timed out after 2000ms') - ); + vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[nodeWrapper], null] as any); + vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[nodeWrapper], null] as any); + vi.mocked(ApiProxy.request).mockReturnValue(new Promise(() => {})); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); await act(async () => { - await vi.advanceTimersByTimeAsync(100); + await vi.advanceTimersByTimeAsync(1999); }); - expect(result.current.loading).toBe(false); + expect(result.current.loading).toBe(true); + + await act(async () => { + await vi.advanceTimersByTimeAsync(200); + }); + await waitFor(() => expect(result.current.loading).toBe(false)); expect(result.current.crdAvailable).toBe(false); + vi.useRealTimers(); }); }); -- 2.52.0 From 17a9aa165a51cef4482d387bc8866e1b376aef35 Mon Sep 17 00:00:00 2001 From: privilegedescalation-engineer Date: Wed, 25 Mar 2026 09:03:03 +0000 Subject: [PATCH 9/9] fix test: properly mock pod selector calls to resolve immediately The withTimeout test was failing because: 1. The mock made ALL ApiProxy.request calls hang, but the implementation has 4 sequential requests (1 CRD + 3 pod selectors) each wrapped in their own withTimeout 2. Using advanceTimersByTimeAsync with hanging promises causes act() to hang because flushPromises() waits for pending promises Fix: - Use mockReturnValueOnce for the CRD call (hanging) and mockResolvedValueOnce for each pod selector call (resolves immediately) - Use synchronous advanceTimersByTime() instead of async version - Simplified test flow: check loading=true initially, advance timers, then verify crdAvailable=false and loading=false Fixes PRI-1040 --- src/api/IntelGpuDataContext.test.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/IntelGpuDataContext.test.tsx b/src/api/IntelGpuDataContext.test.tsx index a17431f..d9e97bf 100644 --- a/src/api/IntelGpuDataContext.test.tsx +++ b/src/api/IntelGpuDataContext.test.tsx @@ -157,20 +157,20 @@ describe('IntelGpuDataProvider', () => { const nodeWrapper = { jsonData: {} }; vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[nodeWrapper], null] as any); vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[nodeWrapper], null] as any); - vi.mocked(ApiProxy.request).mockReturnValue(new Promise(() => {})); + vi.mocked(ApiProxy.request) + .mockReturnValueOnce(new Promise(() => {})) + .mockResolvedValueOnce({ items: [] }) + .mockResolvedValueOnce({ items: [] }) + .mockResolvedValueOnce({ items: [] }); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); - await act(async () => { - await vi.advanceTimersByTimeAsync(1999); - }); expect(result.current.loading).toBe(true); - await act(async () => { - await vi.advanceTimersByTimeAsync(200); - }); - await waitFor(() => expect(result.current.loading).toBe(false)); + vi.advanceTimersByTime(2000); + await act(async () => {}); expect(result.current.crdAvailable).toBe(false); + expect(result.current.loading).toBe(false); vi.useRealTimers(); }); -- 2.52.0