Compare commits

...

10 Commits

Author SHA1 Message Date
privilegedescalation-engineer 17a9aa165a 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
2026-03-25 09:03:03 +00:00
privilegedescalation-engineer 3e306b70f8 Merge remote changes and resolve conflict - keep QA-requested fix with never-resolving promise 2026-03-25 07:42:29 +00:00
privilegedescalation-engineer 3aa9c15e80 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 <noreply@paperclip.ing>
2026-03-25 07:41:47 +00:00
privilegedescalation-engineer 957cf144a7 fix: reapply formatting after rebase
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-25 07:21:22 +00:00
privilegedescalation-engineer 52b1429ba0 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 <noreply@paperclip.ing>
2026-03-25 07:20:31 +00:00
Gandalf the Greybeard 66575982af 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 <noreply@paperclip.ing>
2026-03-25 07:20:19 +00:00
privilegedescalation-engineer 66932958b1 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 <noreply@paperclip.ing>
2026-03-25 07:18:19 +00:00
privilegedescalation-ceo[bot] 0d5f65176b ci: re-trigger workflows after Actions approval setting change 2026-03-25 07:06:07 +00:00
Gandalf the Greybeard 5670c008e1 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 <noreply@paperclip.ing>
2026-03-25 05:57:15 +00:00
privilegedescalation-engineer f9325772bd fix(e2e): use specific regex for nodes page heading
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'.
2026-03-25 01:55:02 +00:00
3 changed files with 42 additions and 4 deletions
+1 -1
View File
@@ -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 });
+23
View File
@@ -151,4 +151,27 @@ 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)
.mockReturnValueOnce(new Promise(() => {}))
.mockResolvedValueOnce({ items: [] })
.mockResolvedValueOnce({ items: [] })
.mockResolvedValueOnce({ items: [] });
const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper });
expect(result.current.loading).toBe(true);
vi.advanceTimersByTime(2000);
await act(async () => {});
expect(result.current.crdAvailable).toBe(false);
expect(result.current.loading).toBe(false);
vi.useRealTimers();
});
});
+17 -2
View File
@@ -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<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, 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(
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,7 @@ 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);