diff --git a/frontend/src/components/Initializers/BaselineInitializers.test.tsx b/frontend/src/components/Initializers/BaselineInitializers.test.tsx
index e13b780670..71729a236c 100644
--- a/frontend/src/components/Initializers/BaselineInitializers.test.tsx
+++ b/frontend/src/components/Initializers/BaselineInitializers.test.tsx
@@ -63,6 +63,7 @@ describe('BaselineInitializers', () => {
const row = screen.getByTestId('baseline-initializer-row-ghost')
expect(within(row).getByText('Initializer is no longer registered.')).toBeInTheDocument()
- expect(within(row).getByText(/Required env vars: None/)).toBeInTheDocument()
+ // No synthetic catalog entry is rendered, so no env-vars line is shown for the row.
+ expect(within(row).queryByText(/Required env vars:/)).not.toBeInTheDocument()
})
})
diff --git a/frontend/src/components/Initializers/BaselineInitializers.tsx b/frontend/src/components/Initializers/BaselineInitializers.tsx
index 46910182ff..aa84f2f692 100644
--- a/frontend/src/components/Initializers/BaselineInitializers.tsx
+++ b/frontend/src/components/Initializers/BaselineInitializers.tsx
@@ -3,17 +3,23 @@ import { Text } from '@fluentui/react-components'
import type { BaselineInitializerSetting, RegisteredInitializer } from '@/types'
import { formatInitializerParameters } from './initializerFormatting'
-import { resolveRegisteredInitializer } from './initializerLookup'
+import {
+ findRegisteredInitializer,
+ initializerFallbackDescription,
+ type CatalogStatus,
+} from './initializerLookup'
import { useInitializersStyles } from './Initializers.styles'
interface BaselineInitializersProps {
items: BaselineInitializerSetting[]
registeredInitializers: RegisteredInitializer[]
+ catalogStatus?: CatalogStatus
}
export default function BaselineInitializers({
items,
registeredInitializers,
+ catalogStatus = 'loaded',
}: BaselineInitializersProps) {
const styles = useInitializersStyles()
@@ -32,7 +38,7 @@ export default function BaselineInitializers({
) : (
{items.map((item: BaselineInitializerSetting) => {
- const initializer = resolveRegisteredInitializer(item.initializer_name, registeredInitializers)
+ const initializer = findRegisteredInitializer(item.initializer_name, registeredInitializers)
return (
{item.initializer_name}
- {initializer.description || 'No description available.'}
-
- Required env vars: {initializer.required_env_vars.length > 0
- ? initializer.required_env_vars.join(', ')
- : 'None'}
+
+ {initializer
+ ? initializer.description || 'No description available.'
+ : initializerFallbackDescription(catalogStatus)}
+ {initializer && (
+
+ Required env vars: {initializer.required_env_vars.length > 0
+ ? initializer.required_env_vars.join(', ')
+ : 'None'}
+
+ )}
Order: {item.order_index}
diff --git a/frontend/src/components/Initializers/Initializers.test.tsx b/frontend/src/components/Initializers/Initializers.test.tsx
index 3ddc8b6954..ef13299130 100644
--- a/frontend/src/components/Initializers/Initializers.test.tsx
+++ b/frontend/src/components/Initializers/Initializers.test.tsx
@@ -285,6 +285,28 @@ describe('Initializers', () => {
expect(screen.getByText('Service Unavailable')).toBeInTheDocument()
})
+ it('should drop the loaded catalog and disable Add when a refresh fails after a successful load', async () => {
+ const user = userEvent.setup()
+ renderInitializers()
+
+ const loadedRow = await screen.findByTestId('baseline-initializer-row-target')
+ await waitFor(() => expect(loadedRow).toHaveTextContent('Registers targets.'))
+ expect(loadedRow).toHaveTextContent('Required env vars: AZURE_OPENAI_ENDPOINT')
+ expect(screen.getByRole('button', { name: 'Add initializer' })).toBeEnabled()
+
+ mockedInitializersApi.listRegistered.mockRejectedValueOnce(new Error('Service Unavailable'))
+ await user.click(screen.getByRole('button', { name: 'Refresh' }))
+
+ await waitFor(() => expect(mockedInitializersApi.listRegistered).toHaveBeenCalledTimes(2))
+ const refreshedRow = await screen.findByTestId('baseline-initializer-row-target')
+ await waitFor(() => expect(refreshedRow).toHaveTextContent('Catalog metadata temporarily unavailable.'))
+ expect(refreshedRow).not.toHaveTextContent('Registers targets.')
+ expect(refreshedRow).not.toHaveTextContent('Required env vars')
+ expect(screen.getByRole('button', { name: 'Add initializer' })).toBeDisabled()
+ expect(screen.getByRole('combobox', { name: 'Initializer to add' })).toBeDisabled()
+ expect(screen.getByText('Service Unavailable')).toBeInTheDocument()
+ })
+
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..ddf735166f 100644
--- a/frontend/src/components/Initializers/Initializers.tsx
+++ b/frontend/src/components/Initializers/Initializers.tsx
@@ -10,6 +10,7 @@ import type { InitializerSettingsResponse, RegisteredInitializer, UpdateAddition
import AdditionalInitializers from './AdditionalInitializers'
import AvailableInitializersDialog from './AvailableInitializersDialog'
import BaselineInitializers from './BaselineInitializers'
+import type { CatalogStatus } from './initializerLookup'
import { useInitializersStyles } from './Initializers.styles'
interface StatusMessage {
@@ -26,6 +27,7 @@ export default function Initializers() {
const styles = useInitializersStyles()
const [settings, setSettings] = useState
(EMPTY_SETTINGS)
const [registeredInitializers, setRegisteredInitializers] = useState([])
+ const [catalogStatus, setCatalogStatus] = useState('loading')
const [loading, setLoading] = useState(true)
const [statusMessage, setStatusMessage] = useState(null)
const [refetchCount, setRefetchCount] = useState(0)
@@ -55,7 +57,12 @@ export default function Initializers() {
if (registeredResult.status === 'fulfilled') {
setRegisteredInitializers(registeredResult.value.items)
+ setCatalogStatus('loaded')
} else {
+ // Drop the previous catalog: entries left behind would keep rendering stale
+ // descriptions and env vars, and would keep Add enabled off the first stale name.
+ setRegisteredInitializers([])
+ setCatalogStatus('error')
const catalogError = toApiError(registeredResult.reason).detail
setStatusMessage((current: StatusMessage | null) =>
current
@@ -178,7 +185,8 @@ export default function Initializers() {
{
+describe('findRegisteredInitializer', () => {
it('returns the matching catalog entry by name', () => {
- const result = resolveRegisteredInitializer('scorer', registered)
+ const result = findRegisteredInitializer('scorer', registered)
expect(result).toBe(registered[1])
})
- it('returns an "no longer registered" placeholder when the name is unknown', () => {
- const result = resolveRegisteredInitializer('ghost', registered)
+ it('returns undefined when the name is not in a loaded catalog', () => {
+ const result = findRegisteredInitializer('ghost', registered)
- expect(result).toEqual({
- initializer_name: 'ghost',
- initializer_type: 'UnknownInitializer',
- description: 'Initializer is no longer registered.',
- required_env_vars: [],
- supported_parameters: [],
- })
+ expect(result).toBeUndefined()
})
- it('returns a placeholder when the catalog is empty', () => {
- const result = resolveRegisteredInitializer('target', [])
+ it('returns undefined when the catalog is empty', () => {
+ const result = findRegisteredInitializer('target', [])
- expect(result.initializer_name).toBe('target')
- expect(result.initializer_type).toBe('UnknownInitializer')
+ expect(result).toBeUndefined()
+ })
+})
+
+describe('initializerFallbackDescription', () => {
+ it('reports a definitive unregistration once the catalog has loaded', () => {
+ expect(initializerFallbackDescription('loaded')).toBe('Initializer is no longer registered.')
+ })
+
+ it('reports a temporary outage when the catalog request failed', () => {
+ expect(initializerFallbackDescription('error')).toBe('Catalog metadata temporarily unavailable.')
+ })
+
+ it('does not claim unregistration while the catalog is still loading', () => {
+ expect(initializerFallbackDescription('loading')).toBe('Catalog metadata temporarily unavailable.')
})
})
diff --git a/frontend/src/components/Initializers/initializerLookup.ts b/frontend/src/components/Initializers/initializerLookup.ts
index 73b3d889b1..c59a2935b3 100644
--- a/frontend/src/components/Initializers/initializerLookup.ts
+++ b/frontend/src/components/Initializers/initializerLookup.ts
@@ -1,26 +1,40 @@
import type { RegisteredInitializer } from '@/types'
+/**
+ * Lifecycle of the registered-initializer catalog request.
+ *
+ * `loading` — the initial fetch has not settled yet.
+ * `loaded` — the catalog is the current source of truth; lookups that miss
+ * genuinely mean "no longer registered".
+ * `error` — the catalog fetch (or latest refresh) failed; entries may be
+ * stale, so catalog-derived metadata must not be rendered and controls
+ * that depend on the catalog stay disabled.
+ */
+export type CatalogStatus = 'loading' | 'loaded' | 'error'
+
/**
* 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.
+ * Returns `undefined` when the name is not in the catalog; callers branch on
+ * the catalog status to decide whether that means "no longer registered"
+ * (`loaded`) or "metadata unavailable" (`error`/`loading`) instead of
+ * rendering synthetic placeholder entries.
*/
-export function resolveRegisteredInitializer(
+export function findRegisteredInitializer(
initializerName: string,
registeredInitializers: RegisteredInitializer[],
-): RegisteredInitializer {
- const match = registeredInitializers.find((item) => item.initializer_name === initializerName)
- if (match) {
- return match
- }
+): RegisteredInitializer | undefined {
+ return registeredInitializers.find((item) => item.initializer_name === initializerName)
+}
- return {
- initializer_name: initializerName,
- initializer_type: 'UnknownInitializer',
- description: 'Initializer is no longer registered.',
- required_env_vars: [],
- supported_parameters: [],
- }
+/**
+ * Description shown for a row whose initializer has no catalog entry.
+ *
+ * Only a settled, successful catalog load can claim "no longer registered";
+ * any other status means the metadata is simply not known yet.
+ */
+export function initializerFallbackDescription(catalogStatus: CatalogStatus): string {
+ return catalogStatus === 'loaded'
+ ? 'Initializer is no longer registered.'
+ : 'Catalog metadata temporarily unavailable.'
}