From d95f79cd226f6e39251abcb7ef062a3ecbe5dd73 Mon Sep 17 00:00:00 2001 From: Tang Vu <145498528+tang-vu@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:36:36 +0700 Subject: [PATCH] fix(dashboard): complete initial GPU status load --- .../hooks/__tests__/useGPUDetailed.test.js | 45 +++++++++++++++++++ .../dashboard/src/hooks/useGPUDetailed.js | 10 ++++- 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 ods/extensions/services/dashboard/src/hooks/__tests__/useGPUDetailed.test.js diff --git a/ods/extensions/services/dashboard/src/hooks/__tests__/useGPUDetailed.test.js b/ods/extensions/services/dashboard/src/hooks/__tests__/useGPUDetailed.test.js new file mode 100644 index 0000000000..a1a8dbc849 --- /dev/null +++ b/ods/extensions/services/dashboard/src/hooks/__tests__/useGPUDetailed.test.js @@ -0,0 +1,45 @@ +import { renderHook, waitFor } from '@testing-library/react' +import { useGPUDetailed } from '../useGPUDetailed' + +const response = (body, status = 200) => ({ + ok: status >= 200 && status < 300, + status, + json: async () => body, +}) + +describe('useGPUDetailed', () => { + afterEach(() => { + vi.restoreAllMocks() + Object.defineProperty(document, 'hidden', { configurable: true, value: false }) + }) + + test('loads initial GPU state while the document is hidden', async () => { + Object.defineProperty(document, 'hidden', { configurable: true, value: true }) + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(response({ gpus: [{ name: 'GPU' }] })) + .mockResolvedValueOnce(response({ samples: [] })) + .mockResolvedValueOnce(response({ links: [] }))) + + const { result } = renderHook(() => useGPUDetailed()) + + await waitFor(() => expect(result.current.loading).toBe(false)) + expect(fetch).toHaveBeenCalledTimes(3) + expect(result.current.detailed.gpus[0].name).toBe('GPU') + expect(result.current.error).toBeNull() + }) + + test('surfaces non-success responses without discarding successful endpoints', async () => { + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(response({}, 500)) + .mockResolvedValueOnce(response({ samples: ['kept'] })) + .mockResolvedValueOnce(response({}, 502))) + + const { result } = renderHook(() => useGPUDetailed()) + + await waitFor(() => expect(result.current.loading).toBe(false)) + expect(result.current.error).toBe( + 'GPU status unavailable: details HTTP 500, topology HTTP 502', + ) + expect(result.current.history.samples).toEqual(['kept']) + }) +}) diff --git a/ods/extensions/services/dashboard/src/hooks/useGPUDetailed.js b/ods/extensions/services/dashboard/src/hooks/useGPUDetailed.js index 134d69d618..ddd91a10d9 100644 --- a/ods/extensions/services/dashboard/src/hooks/useGPUDetailed.js +++ b/ods/extensions/services/dashboard/src/hooks/useGPUDetailed.js @@ -11,10 +11,11 @@ export function useGPUDetailed() { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const fetchInFlight = useRef(false) + const hasFetched = useRef(false) useEffect(() => { const fetchAll = async () => { - if (document.hidden) return + if (document.hidden && hasFetched.current) return if (fetchInFlight.current) return fetchInFlight.current = true try { @@ -23,13 +24,18 @@ export function useGPUDetailed() { fetch('/api/gpu/history'), fetch('/api/gpu/topology'), ]) + const failures = [] if (detRes.ok) setDetailed(await detRes.json()) + else failures.push(`details HTTP ${detRes.status}`) if (histRes.ok) setHistory(await histRes.json()) + else failures.push(`history HTTP ${histRes.status}`) if (topoRes.ok) setTopology(await topoRes.json()) - setError(null) + else failures.push(`topology HTTP ${topoRes.status}`) + setError(failures.length > 0 ? `GPU status unavailable: ${failures.join(', ')}` : null) } catch (err) { setError(err.message) } finally { + hasFetched.current = true fetchInFlight.current = false setLoading(false) }