diff --git a/frontend/src/components/Initializers/AdditionalInitializers.tsx b/frontend/src/components/Initializers/AdditionalInitializers.tsx index 992292ca3c..4d2bcea574 100644 --- a/frontend/src/components/Initializers/AdditionalInitializers.tsx +++ b/frontend/src/components/Initializers/AdditionalInitializers.tsx @@ -25,6 +25,7 @@ import ConfirmDialog from '../ConfirmDialog' interface AdditionalInitializersProps { items: AdditionalInitializerSetting[] registeredInitializers: RegisteredInitializer[] + catalogAvailable?: boolean creating: boolean savingInitializerId?: string | null saveErrors?: Record @@ -157,6 +158,7 @@ function AdditionalInitializerCard({ export default function AdditionalInitializers({ items, registeredInitializers, + catalogAvailable = true, creating, savingInitializerId = null, saveErrors = {}, @@ -237,7 +239,7 @@ export default function AdditionalInitializers({ {items.map((item: BaselineInitializerSetting) => { - const initializer = resolveRegisteredInitializer(item.initializer_name, registeredInitializers) + const initializer = resolveRegisteredInitializer(item.initializer_name, registeredInitializers, catalogAvailable) return (
{ expect(screen.getByText('Service Unavailable')).toBeInTheDocument() }) + it('should describe rows as temporarily unavailable, not unregistered, when the catalog fails', async () => { + mockedInitializersApi.listRegistered.mockRejectedValue(new Error('Service Unavailable')) + + renderInitializers() + + const row = await screen.findByTestId('baseline-initializer-row-target') + // A metadata outage must not be presented as a configuration problem. + expect(within(row).getByText(/temporarily unavailable/i)).toBeInTheDocument() + expect(within(row).queryByText(/no longer registered/i)).not.toBeInTheDocument() + + mockedInitializersApi.listRegistered.mockResolvedValue({ + items: [targetInitializer], + pagination: { limit: 200, has_more: false }, + }) + fireEvent.click(screen.getByRole('button', { name: 'Refresh' })) + + // Refresh re-runs the load effect (and remounts rows via the loading + // state), so assert on a freshly queried node. + await waitFor(async () => { + expect(screen.getByTestId('baseline-initializer-row-target')).toHaveTextContent( + 'Registers targets.', + ) + }) + }) + it('should remove an additional initializer and show success feedback', async () => { const user = userEvent.setup() renderInitializers() diff --git a/frontend/src/components/Initializers/Initializers.tsx b/frontend/src/components/Initializers/Initializers.tsx index 55c7547d7f..39af8feaa6 100644 --- a/frontend/src/components/Initializers/Initializers.tsx +++ b/frontend/src/components/Initializers/Initializers.tsx @@ -26,6 +26,10 @@ export default function Initializers() { const styles = useInitializersStyles() const [settings, setSettings] = useState(EMPTY_SETTINGS) const [registeredInitializers, setRegisteredInitializers] = useState([]) + // A failed catalog request leaves registration state unknown; rows must + // then say "temporarily unavailable" instead of "no longer registered", + // which would present a metadata outage as a configuration problem. + const [catalogAvailable, setCatalogAvailable] = useState(true) const [loading, setLoading] = useState(true) const [statusMessage, setStatusMessage] = useState(null) const [refetchCount, setRefetchCount] = useState(0) @@ -55,7 +59,9 @@ export default function Initializers() { if (registeredResult.status === 'fulfilled') { setRegisteredInitializers(registeredResult.value.items) + setCatalogAvailable(true) } else { + setCatalogAvailable(false) const catalogError = toApiError(registeredResult.reason).detail setStatusMessage((current: StatusMessage | null) => current @@ -207,10 +213,12 @@ export default function Initializers() { { expect(result.initializer_type).toBe('UnknownInitializer') }) }) + +describe('resolveRegisteredInitializer with catalog availability', () => { + it('reports temporary unavailability instead of unregistered when the catalog request failed', () => { + const result = resolveRegisteredInitializer('target', [], false) + + expect(result).toEqual({ + initializer_name: 'target', + initializer_type: 'UnverifiedInitializer', + description: expect.stringContaining('temporarily unavailable'), + required_env_vars: [], + supported_parameters: [], + }) + expect(result.description).not.toContain('no longer registered') + }) + + it('still resolves matches from stale-but-loaded catalog data when available flag is true', () => { + const result = resolveRegisteredInitializer('target', registered, true) + + expect(result).toBe(registered[0]) + }) +}) diff --git a/frontend/src/components/Initializers/initializerLookup.ts b/frontend/src/components/Initializers/initializerLookup.ts index 73b3d889b1..20cb305f5b 100644 --- a/frontend/src/components/Initializers/initializerLookup.ts +++ b/frontend/src/components/Initializers/initializerLookup.ts @@ -1,16 +1,36 @@ import type { RegisteredInitializer } from '@/types' +export const UNREGISTERED_INITIALIZER_DESCRIPTION = 'Initializer is no longer registered.' +export const CATALOG_UNAVAILABLE_DESCRIPTION = + 'Initializer catalog is temporarily unavailable; registration state cannot be confirmed.' + /** * Resolve a settings entry's `initializer_name` to its catalog definition. * * Settings reference an initializer by name; the catalog (from `listRegistered`) * is the single source of truth for display metadata. When a persisted name is no * longer registered, return a placeholder so the row still renders. + * + * `catalogAvailable` distinguishes the two ways a name can fail to resolve: + * a successful catalog response that lacks the name means the entry is truly + * unregistered, while a failed catalog request only means registration state + * is unknown and must not be reported as a configuration problem. */ export function resolveRegisteredInitializer( initializerName: string, registeredInitializers: RegisteredInitializer[], + catalogAvailable: boolean = true, ): RegisteredInitializer { + if (!catalogAvailable) { + return { + initializer_name: initializerName, + initializer_type: 'UnverifiedInitializer', + description: CATALOG_UNAVAILABLE_DESCRIPTION, + required_env_vars: [], + supported_parameters: [], + } + } + const match = registeredInitializers.find((item) => item.initializer_name === initializerName) if (match) { return match @@ -19,7 +39,7 @@ export function resolveRegisteredInitializer( return { initializer_name: initializerName, initializer_type: 'UnknownInitializer', - description: 'Initializer is no longer registered.', + description: UNREGISTERED_INITIALIZER_DESCRIPTION, required_env_vars: [], supported_parameters: [], }