import type { GetKnowledgeSpacesByIdSourceConnectionsResponse, GetSourceProvidersResponse, } from '@dify/contracts/knowledge-fs/types.gen' import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { StrictMode } from 'react' import { render } from '@/test/console/render' import { AddSourcePage } from '../add-source-page' import { newKnowledgeSourceDraftStorageKey } from '../routes' const routerMock = vi.hoisted(() => ({ push: vi.fn(), replace: vi.fn(), })) vi.mock('@/next/navigation', () => ({ useRouter: () => routerMock })) const toastInfoMock = vi.hoisted(() => vi.fn()) vi.mock('@langgenius/dify-ui/toast', () => ({ toast: { info: toastInfoMock }, })) type ConnectionsInfiniteData = { pages: GetKnowledgeSpacesByIdSourceConnectionsResponse[] } type ConnectionsInfiniteOptions = { enabled?: boolean getNextPageParam: ( lastPage: GetKnowledgeSpacesByIdSourceConnectionsResponse, ) => string | undefined input: (pageParam: string | null) => unknown initialPageParam: string | null } const queryState = vi.hoisted(() => ({ connections: { data: { pages: [{ items: [] }] } as ConnectionsInfiniteData | undefined, error: null as unknown, fetchNextPage: vi.fn(), hasNextPage: false, isFetchNextPageError: false, isFetchingNextPage: false, isPending: false, refetch: vi.fn(), }, providers: { data: undefined as GetSourceProvidersResponse | undefined, error: null as unknown, isPending: false, refetch: vi.fn(), }, })) const clientMock = vi.hoisted(() => ({ createConnection: vi.fn(), refreshConnection: vi.fn(), })) const queryClientMock = vi.hoisted(() => ({ invalidateQueries: vi.fn(), })) const providerQueryOptionsMock = vi.hoisted(() => vi.fn((options: { enabled?: boolean }) => ({ enabled: options.enabled, queryKey: ['source-providers'], })), ) const connectionInfiniteOptionsMock = vi.hoisted(() => vi.fn((options: ConnectionsInfiniteOptions) => ({ enabled: options.enabled, queryKey: ['source-connections'], })), ) const providerHookOptionsMock = vi.hoisted(() => vi.fn()) const connectionHookOptionsMock = vi.hoisted(() => vi.fn()) vi.mock('@tanstack/react-query', async (importOriginal) => { const original = await importOriginal() return { ...original, useInfiniteQuery: (options: unknown) => { connectionHookOptionsMock(options) return queryState.connections }, useQuery: (options: unknown) => { providerHookOptionsMock(options) return queryState.providers }, useQueryClient: () => queryClientMock, } }) vi.mock('@/service/client', () => ({ consoleClient: { knowledgeFs: { postKnowledgeSpacesByIdSourceConnections: clientMock.createConnection, postKnowledgeSpacesByIdSourceConnectionsByConnectionIdRefresh: clientMock.refreshConnection, }, }, consoleQuery: { knowledgeFs: { getSourceProviders: { queryOptions: providerQueryOptionsMock, }, getKnowledgeSpacesByIdSourceConnections: { infiniteOptions: connectionInfiniteOptionsMock, key: vi.fn(() => ['source-connections']), }, }, }, })) const firecrawlProvider: GetSourceProvidersResponse['items'][number] = { authKinds: ['api-key', 'endpoint'], available: true, capabilities: ['website-crawl'], configuration: [ { description: 'Plugin identifier', name: 'pluginId', required: true, secret: false, type: 'string', }, { description: 'Plugin provider', name: 'provider', required: true, secret: false, type: 'string', }, { description: 'Plugin datasource', name: 'datasource', required: true, secret: false, type: 'string', }, { description: 'Firecrawl API key', format: 'password', name: 'apiKey', required: true, secret: true, type: 'string', }, { description: 'Self-hosted endpoint', format: 'uri', name: 'endpoint', required: false, secret: false, type: 'string', }, ], displayName: 'Plugin daemon website crawl', id: 'plugin-daemon-website', } const connection = ( status: 'provisioning' | 'active' | 'expired' | 'error' | 'revoked', version = 2, ) => ({ authKind: 'api-key' as const, configuration: { datasource: 'crawl', pluginId: 'langgenius/firecrawl_datasource', provider: 'firecrawl', }, createdAt: '2026-07-20T10:00:00Z', id: 'connection-1', knowledgeSpaceId: 'space-1', name: 'Firecrawl', providerId: 'plugin-daemon-website', scopes: [], status, updatedAt: '2026-07-20T10:00:00Z', version, }) function createDeferred() { let resolve!: (value: T) => void const promise = new Promise((resolvePromise) => { resolve = resolvePromise }) return { promise, resolve } } describe('AddSourcePage', () => { beforeEach(() => { vi.clearAllMocks() globalThis.sessionStorage.clear() clientMock.createConnection.mockReset() clientMock.refreshConnection.mockReset() queryState.connections.refetch.mockReset() queryState.providers.refetch.mockReset() queryState.providers.data = { items: [firecrawlProvider] } queryState.providers.error = null queryState.providers.isPending = false queryState.connections.data = { pages: [{ items: [] }] } queryState.connections.error = null queryState.connections.hasNextPage = false queryState.connections.isFetchNextPageError = false queryState.connections.isFetchingNextPage = false queryState.connections.isPending = false }) afterEach(() => { globalThis.sessionStorage.clear() }) it('loads the provider catalog and every scoped connection cursor page', () => { queryState.providers.isPending = true render() expect(providerQueryOptionsMock).toHaveBeenCalledWith({ context: { silent: true }, enabled: true, input: {}, retry: false, }) const options = connectionInfiniteOptionsMock.mock.lastCall?.[0] expect(options).toBeDefined() if (!options) throw new Error('Expected connection infinite query options') expect(options.input(null)).toEqual({ params: { id: 'space-1' }, query: { limit: 200 } }) expect(options.input('next')).toEqual({ params: { id: 'space-1' }, query: { cursor: 'next', limit: 200 }, }) expect(options.getNextPageParam({ items: [], nextCursor: 'next' })).toBe('next') expect(options.initialPageParam).toBeNull() expect(screen.getByRole('status')).toBeInTheDocument() }) it('continues loading connection pages automatically', async () => { queryState.connections.hasNextPage = true render() await waitFor(() => expect(queryState.connections.fetchNextPage).toHaveBeenCalledOnce()) }) it('stops website connection pagination after switching source type', async () => { const user = userEvent.setup() render() queryState.connections.hasNextPage = true await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' })) expect(queryState.connections.fetchNextPage).not.toHaveBeenCalled() }) it.each(['onlineDocuments', 'onlineDrive'])( 'does not request website provider data for the %s state', (initialSourceType) => { queryState.providers.isPending = true queryState.connections.isPending = true render() expect(providerHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: false }) expect(connectionHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: false }) expect(screen.queryByRole('status', { name: 'common.loading' })).not.toBeInTheDocument() expect(screen.getByText('dataset.newKnowledge.providerUnavailable')).toBeInTheDocument() }, ) it('stops automatic connection pagination after a cursor error', () => { queryState.connections.error = new Error('next page failed') queryState.connections.hasNextPage = true queryState.connections.isFetchNextPageError = true render() expect(queryState.connections.fetchNextPage).not.toHaveBeenCalled() expect(screen.getByText('dataset.newKnowledge.providerLoadFailed')).toBeInTheDocument() }) it('finds the provider connection on a later loaded page', () => { queryState.connections.data = { pages: [{ items: [] }, { items: [connection('active')] }] } render() expect(screen.getByText(/dataset\.newKnowledge\.providerConnected/)).toBeInTheDocument() expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked() expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ })).toBeEnabled() expect( screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' }), ).toBeDisabled() }) it('offers replacement setup when the saved provider connection is revoked', () => { queryState.connections.data = { pages: [{ items: [connection('revoked')] }] } render() expect( screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ).toBeEnabled() expect(screen.queryByRole('button', { name: 'common.operation.retry' })).not.toBeInTheDocument() }) it('restores a website draft handed off by the creation flow', async () => { const user = userEvent.setup() queryState.connections.data = { pages: [{ items: [connection('active')] }] } render( , ) expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ })).toHaveValue( 'https://docs.dify.ai', ) expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ })).toHaveValue( 'Dify docs', ) await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.crawlOptions/ })) expect( screen.getByRole('checkbox', { name: 'dataset.newKnowledge.includeSubpages' }), ).not.toBeChecked() expect(screen.getByRole('spinbutton', { name: 'dataset.newKnowledge.maxPages' })).toHaveValue( 25, ) }) it('keeps the exact website provider selected and lets the user switch to Firecrawl', async () => { const user = userEvent.setup() render( , ) expect(screen.getByRole('radio', { name: 'Jina Reader' })).toBeChecked() expect(providerHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: false }) expect(connectionHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: false }) expect(screen.getByText('dataset.newKnowledge.providerUnavailable')).toBeInTheDocument() await user.click(screen.getByRole('radio', { name: 'Firecrawl' })) expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked() expect(providerHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: true }) expect(connectionHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: true }) }) it('clears a website dependency alert when the provider changes', async () => { const user = userEvent.setup() render( , ) await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) expect(screen.getByRole('alert')).toBeInTheDocument() await user.click(screen.getByRole('radio', { name: 'Jina Reader' })) expect(screen.queryByRole('alert')).not.toBeInTheDocument() }) it('restores online document configuration and reaches an honest backend boundary', async () => { const user = userEvent.setup() render( , ) expect(screen.getByRole('radio', { name: 'Google Docs' })).toBeChecked() expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toHaveValue( 'Shared product docs', ) expect(screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' })).toHaveValue( 'daily', ) const addSource = screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' }) expect(addSource).toBeEnabled() await user.click(addSource) expect(screen.getByRole('alert')).toHaveTextContent( 'dataset.newKnowledge.sourceSetupBackendDependency', ) }) it('restores a website draft from session storage', async () => { queryState.connections.data = { pages: [{ items: [connection('active')] }] } const storageKey = newKnowledgeSourceDraftStorageKey('draft-1') globalThis.sessionStorage.setItem( storageKey, JSON.stringify({ includeSubpages: false, maxPages: 25, provider: 'Firecrawl', rootUrl: 'https://docs.dify.ai', sourceName: 'Dify docs', sourceType: 'websiteCrawl', syncPolicy: 'provider', }), ) render() expect( await screen.findByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), ).toHaveValue('https://docs.dify.ai') expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ })).toHaveValue( 'Dify docs', ) expect(globalThis.sessionStorage.getItem(storageKey)).not.toBeNull() }) it('retains the draft through Strict Mode, delayed loading, and a real remount', async () => { queryState.connections.data = { pages: [{ items: [connection('active')] }] } queryState.connections.isPending = true const storageKey = newKnowledgeSourceDraftStorageKey('strict-draft') globalThis.sessionStorage.setItem( storageKey, JSON.stringify({ includeSubpages: true, maxPages: 100, provider: 'Firecrawl', rootUrl: 'https://docs.dify.ai/strict', sourceName: 'Strict docs', sourceType: 'websiteCrawl', syncPolicy: 'provider', }), ) const view = render( , ) await waitFor(() => expect(globalThis.sessionStorage.getItem(storageKey)).not.toBeNull()) queryState.connections.isPending = false view.rerender( , ) expect( await screen.findByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), ).toHaveValue('https://docs.dify.ai/strict') expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ })).toHaveValue( 'Strict docs', ) view.unmount() queryState.connections.isPending = false render( , ) expect( await screen.findByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), ).toHaveValue('https://docs.dify.ai/strict') }) it('clears the stored draft when source setup is canceled', async () => { const user = userEvent.setup() const historyBack = vi.spyOn(window.history, 'back').mockImplementation(() => undefined) const storageKey = newKnowledgeSourceDraftStorageKey('cancel-draft') globalThis.sessionStorage.setItem( storageKey, JSON.stringify({ includeSubpages: true, maxPages: 100, provider: 'Firecrawl', rootUrl: 'https://docs.dify.ai', sourceName: 'Dify docs', sourceType: 'websiteCrawl', syncPolicy: 'provider', }), ) render() await user.click( await screen.findByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' }), ) const confirmation = await screen.findByRole('alertdialog', { name: 'dataset.newKnowledge.discardSourceDraftTitle', }) expect(globalThis.sessionStorage.getItem(storageKey)).not.toBeNull() await user.click( screen.getByRole('button', { name: 'dataset.newKnowledge.discardDraftConfirm' }), ) expect(globalThis.sessionStorage.getItem(storageKey)).toBeNull() expect(historyBack).toHaveBeenCalledOnce() act(() => window.dispatchEvent(new PopStateEvent('popstate'))) expect(routerMock.replace).toHaveBeenCalledWith('/datasets/new/space-1/sources') expect(confirmation).not.toBeInTheDocument() }) it('creates the exact Firecrawl provider connection without leaking credentials', async () => { const user = userEvent.setup() clientMock.createConnection.mockResolvedValue(connection('active')) render() await user.click( screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'secret-value') await user.type(screen.getByLabelText('Endpoint'), 'https://crawl.example.com') await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) await waitFor(() => expect(clientMock.createConnection).toHaveBeenCalledWith({ body: { authKind: 'api-key', configuration: { datasource: 'crawl', endpoint: 'https://crawl.example.com', pluginId: 'langgenius/firecrawl_datasource', provider: 'firecrawl', }, credentials: { apiKey: 'secret-value' }, name: 'Firecrawl', providerId: 'plugin-daemon-website', }, params: { id: 'space-1' }, }), ) await screen.findByRole('status', { name: 'appApi.loading' }) act(() => window.dispatchEvent(new PopStateEvent('popstate'))) expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({ queryKey: ['source-connections'], }) expect(screen.getByText(/dataset\.newKnowledge\.providerConnected/)).toBeInTheDocument() expect(screen.queryByDisplayValue('secret-value')).not.toBeInTheDocument() }) it('keeps the connection action focused and explicitly named while pending', async () => { const user = userEvent.setup() const createConnectionDeferred = createDeferred>() clientMock.createConnection.mockReturnValue(createConnectionDeferred.promise) render() await user.click( screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'secret-value') const connectButton = screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider', }) await user.click(connectButton) const pendingButton = screen.getByRole('button', { name: 'dataset.newKnowledge.connectingProvider', }) expect(pendingButton).toBe(connectButton) expect(pendingButton).toHaveAttribute('aria-disabled', 'true') expect(pendingButton).toHaveFocus() await user.click(pendingButton) expect(clientMock.createConnection).toHaveBeenCalledOnce() await act(async () => createConnectionDeferred.resolve(connection('active'))) await screen.findByRole('status', { name: 'appApi.loading' }) }) it('releases the parent history guard before the crawl preview owns navigation', async () => { const user = userEvent.setup() const historyBack = vi.spyOn(window.history, 'back').mockImplementation(() => undefined) clientMock.createConnection.mockResolvedValue(connection('active')) render() await user.click( screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'secret-value') await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) await waitFor(() => expect(historyBack).toHaveBeenCalledOnce()) expect(screen.queryByText(/dataset\.newKnowledge\.providerConnected/)).not.toBeInTheDocument() act(() => window.dispatchEvent(new PopStateEvent('popstate'))) await screen.findByText(/dataset\.newKnowledge\.providerConnected/) await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) expect(historyBack).toHaveBeenCalledOnce() expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources') }) it('keeps the website setup interactive until it reaches the backend boundary', async () => { const user = userEvent.setup() render( , ) expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.rootUrl' })).toHaveValue( 'https://docs.dify.ai', ) expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toHaveValue( 'Dify docs', ) expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.rootUrl' })).toBeEnabled() expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled() const crawlAndPreview = screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview', }) expect(crawlAndPreview).toBeEnabled() await user.click(crawlAndPreview) expect(screen.getByRole('alert')).toHaveTextContent( 'dataset.newKnowledge.sourceSetupBackendDependency', ) }) it('keeps pending website edits when the provider connection becomes active', async () => { const user = userEvent.setup() const initialSourceDraft = { includeSubpages: true, maxPages: 100, provider: 'Firecrawl' as const, rootUrl: 'https://docs.dify.ai', sourceName: 'Dify docs', sourceType: 'websiteCrawl' as const, syncPolicy: 'provider' as const, } const view = render( , ) const rootUrl = screen.getByRole('textbox', { name: 'dataset.newKnowledge.rootUrl' }) const sourceName = screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }) await user.clear(rootUrl) await user.type(rootUrl, 'https://docs.dify.ai/edited') await user.clear(sourceName) await user.type(sourceName, 'Edited docs') queryState.connections.data = { pages: [{ items: [connection('active')] }] } view.rerender( , ) await screen.findByRole('status', { name: 'appApi.loading' }) act(() => window.dispatchEvent(new PopStateEvent('popstate'))) expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ })).toHaveValue( 'https://docs.dify.ai/edited', ) expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ })).toHaveValue( 'Edited docs', ) }) it('does not select a lookalike provider by fuzzy display name', () => { queryState.providers.data = { items: [{ ...firecrawlProvider, displayName: 'Firecrawl impostor', id: 'impostor' }], } render() expect(screen.getByText('dataset.newKnowledge.firecrawlUnavailable')).toBeInTheDocument() }) it('clears sensitive input but retains non-sensitive input after a connection error', async () => { const user = userEvent.setup() clientMock.createConnection.mockRejectedValue(new Error('provider unavailable')) queryState.connections.refetch.mockResolvedValue({ data: queryState.connections.data }) render() await user.click( screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'do-not-retain') await user.type(screen.getByLabelText('Endpoint'), 'https://crawl.example.com') await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) expect(await screen.findByText('dataset.newKnowledge.connectionFailed')).toBeInTheDocument() expect(screen.getByLabelText(/Api Key/)).toHaveValue('') expect(screen.getByLabelText('Endpoint')).toHaveValue('https://crawl.example.com') }) it('reconciles a response-lost create before showing an error', async () => { const user = userEvent.setup() clientMock.createConnection.mockRejectedValue(new Error('response lost')) queryState.connections.refetch.mockResolvedValue({ data: { pages: [{ items: [connection('active')] }] }, }) render() await user.click( screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'secret-value') await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) await waitFor(() => expect(clientMock.createConnection).toHaveBeenCalledOnce()) await screen.findByRole('status', { name: 'appApi.loading' }) act(() => window.dispatchEvent(new PopStateEvent('popstate'))) expect(await screen.findByText(/dataset\.newKnowledge\.providerConnected/)).toBeInTheDocument() expect(screen.queryByText('dataset.newKnowledge.connectionFailed')).not.toBeInTheDocument() }) it('clears an API key when authentication modes are changed and changed back', async () => { const user = userEvent.setup() clientMock.createConnection.mockResolvedValue(connection('active')) render() await user.click( screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'must-not-return') await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.authKind.endpoint' })) await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.authKind.api-key' })) expect(screen.getByLabelText(/Api Key/)).toHaveValue('') }) it('supports an endpoint descriptor without sending a hidden secret field', async () => { const user = userEvent.setup() clientMock.createConnection.mockResolvedValue({ ...connection('active'), authKind: 'endpoint', }) render() await user.click( screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'must-not-be-sent') await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.authKind.endpoint' })) await user.type(screen.getByLabelText('Endpoint'), 'https://crawl.example.com') await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) await waitFor(() => expect(clientMock.createConnection).toHaveBeenCalledWith({ body: { authKind: 'endpoint', configuration: { datasource: 'crawl', endpoint: 'https://crawl.example.com', pluginId: 'langgenius/firecrawl_datasource', provider: 'firecrawl', }, credentials: {}, name: 'Firecrawl', providerId: 'plugin-daemon-website', }, params: { id: 'space-1' }, }), ) }) it('associates provider field descriptions with their controls', async () => { const user = userEvent.setup() const booleanProvider = { ...firecrawlProvider, configuration: [ ...firecrawlProvider.configuration, { description: 'Use stealth mode', name: 'stealth', required: false, secret: false, type: 'boolean' as const, }, ], } queryState.providers.data = { items: [booleanProvider] } render() await user.click( screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) expect(screen.getByLabelText(/Api Key/)).toHaveAccessibleDescription('Firecrawl API key') expect(screen.getByLabelText('Stealth')).toHaveAccessibleDescription('Use stealth mode') }) it('refreshes an errored connection using its current version', async () => { const user = userEvent.setup() queryState.connections.data = { pages: [{ items: [connection('error')] }] } clientMock.refreshConnection.mockResolvedValue(connection('active')) render() await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) await waitFor(() => expect(clientMock.refreshConnection).toHaveBeenCalledWith({ body: { expectedVersion: 2 }, params: { connectionId: 'connection-1', id: 'space-1' }, }), ) expect(queryClientMock.invalidateQueries).toHaveBeenCalled() expect(screen.getByText(/dataset\.newKnowledge\.providerConnected/)).toBeInTheDocument() }) it('keeps the refresh action focused and explicitly named while pending', async () => { const user = userEvent.setup() const refreshConnectionDeferred = createDeferred>() queryState.connections.data = { pages: [{ items: [connection('error')] }] } clientMock.refreshConnection.mockReturnValue(refreshConnectionDeferred.promise) render() const refreshButton = screen.getByRole('button', { name: 'common.operation.retry' }) await user.click(refreshButton) const pendingButton = screen.getByRole('button', { name: 'dataset.newKnowledge.refreshingConnection', }) expect(pendingButton).toBe(refreshButton) expect(pendingButton).toHaveAttribute('aria-disabled', 'true') expect(pendingButton).toHaveFocus() await user.click(pendingButton) expect(clientMock.refreshConnection).toHaveBeenCalledOnce() await act(async () => refreshConnectionDeferred.resolve(connection('active'))) await screen.findByText(/dataset\.newKnowledge\.providerConnected/) }) it('reconciles a refresh version race and retries with the server version', async () => { const user = userEvent.setup() queryState.connections.data = { pages: [{ items: [connection('error')] }] } clientMock.refreshConnection .mockRejectedValueOnce(new Error('version conflict')) .mockResolvedValueOnce(connection('active', 4)) queryState.connections.refetch.mockResolvedValue({ data: { pages: [ { items: [ connection('error', 3), { ...connection('error', 8), id: 'connection-2', updatedAt: '2026-07-20T11:00:00Z', }, ], }, ], }, }) render() await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) await waitFor(() => expect(queryState.connections.refetch).toHaveBeenCalledOnce()) await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) await waitFor(() => expect(clientMock.refreshConnection).toHaveBeenLastCalledWith({ body: { expectedVersion: 3 }, params: { connectionId: 'connection-1', id: 'space-1' }, }), ) }) it('does not regress the current version when reconciliation reads a stale replica', async () => { const user = userEvent.setup() queryState.connections.data = { pages: [{ items: [connection('error', 3)] }] } clientMock.refreshConnection .mockRejectedValueOnce(new Error('response lost')) .mockResolvedValueOnce(connection('active', 4)) queryState.connections.refetch.mockResolvedValue({ data: { pages: [{ items: [connection('error', 2)] }] }, }) render() await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) await waitFor(() => expect(queryState.connections.refetch).toHaveBeenCalledOnce()) await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) await waitFor(() => expect(clientMock.refreshConnection).toHaveBeenLastCalledWith({ body: { expectedVersion: 3 }, params: { connectionId: 'connection-1', id: 'space-1' }, }), ) }) it('lets a newer remote connection version replace a local mutation response', async () => { const user = userEvent.setup() queryState.connections.data = { pages: [{ items: [connection('error')] }] } clientMock.refreshConnection.mockResolvedValue(connection('provisioning', 3)) const view = render() await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) expect( await screen.findByText('dataset.newKnowledge.connectionProvisioning'), ).toBeInTheDocument() queryState.connections.data = { pages: [ { items: [ connection('active', 3), { ...connection('active', 10), id: 'connection-2', updatedAt: '2026-07-20T11:00:00Z', }, ], }, ], } view.rerender() expect(screen.getByText(/dataset\.newKnowledge\.providerConnected/)).toBeInTheDocument() }) it('reconciles a provisioning connection with the refreshed server state', async () => { const user = userEvent.setup() queryState.connections.data = { pages: [{ items: [connection('provisioning')] }] } queryState.connections.refetch.mockResolvedValue({ data: { pages: [{ items: [connection('active')] }] }, }) render() await user.click( screen.getByRole('button', { name: 'dataset.newKnowledge.refreshConnectionStatus' }), ) expect(await screen.findByText(/dataset\.newKnowledge\.providerConnected/)).toBeInTheDocument() }) it('shows a retryable error when provisioning reconciliation fails', async () => { const user = userEvent.setup() queryState.connections.data = { pages: [{ items: [connection('provisioning')] }] } queryState.connections.refetch.mockResolvedValue({ data: queryState.connections.data, error: new Error('temporary failure'), }) render() await user.click( screen.getByRole('button', { name: 'dataset.newKnowledge.refreshConnectionStatus' }), ) expect(await screen.findByRole('alert')).toHaveTextContent( 'dataset.newKnowledge.connectionRefreshFailed', ) }) it('shows a retryable error when provisioning reconciliation finds no connection', async () => { const user = userEvent.setup() queryState.connections.data = { pages: [{ items: [connection('provisioning')] }] } queryState.connections.refetch.mockImplementation(async () => { queryState.connections.data = { pages: [{ items: [] }] } return { data: queryState.connections.data } }) const view = render() await user.click( screen.getByRole('button', { name: 'dataset.newKnowledge.refreshConnectionStatus' }), ) view.rerender() expect(await screen.findByRole('alert')).toHaveTextContent( 'dataset.newKnowledge.connectionRefreshFailed', ) expect( screen.queryByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ).not.toBeInTheDocument() }) it('keeps source types selectable and shows an honest dependency state', async () => { const user = userEvent.setup() render() expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.websiteCrawl' })).toBeChecked() const onlineDocuments = screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments', }) expect(onlineDocuments).toBeEnabled() expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDrive' })).toBeEnabled() expect(screen.getByRole('radio', { name: 'Jina Reader' })).toBeEnabled() expect( screen.getByRole('group', { name: 'datasetCreation.stepOne.website.chooseProvider' }), ).toBeInTheDocument() expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked() await user.click(onlineDocuments) expect(onlineDocuments).toBeChecked() expect( screen.getByRole('group', { name: 'datasetCreation.stepOne.website.chooseProvider' }), ).toBeVisible() expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled() expect(screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' })).toBeEnabled() expect(screen.getByRole('status')).toHaveTextContent('dataset.newKnowledge.providerUnavailable') await user.type( screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }), 'Product docs', ) await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDrive' })) expect(screen.getByRole('radio', { name: 'Google Drive' })).toBeChecked() await user.click(onlineDocuments) expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toHaveValue( 'Product docs', ) }) it('restores the selected source type from the create flow', () => { render() expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDrive' })).toBeChecked() expect( screen.getByRole('region', { name: 'dataset.newKnowledge.selectFilesAndFolders' }), ).toBeVisible() expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled() expect(screen.getByRole('status')).toHaveTextContent('dataset.newKnowledge.providerUnavailable') }) it('disables the final Add source action while its backend dependency is missing', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' })) const addSource = screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' }) expect(addSource).toBeDisabled() expect(toastInfoMock).not.toHaveBeenCalled() }) it('shows catalog unavailability instead of offering a fake connection', () => { queryState.providers.data = { items: [{ ...firecrawlProvider, available: false, unavailableReason: 'Disabled by admin' }], } render() expect(screen.getByText('Disabled by admin')).toBeInTheDocument() expect( screen.queryByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ).not.toBeInTheDocument() }) it('does not offer unsupported OAuth or undocumented direct connection paths', () => { queryState.providers.data = { items: [ { ...firecrawlProvider, authKinds: ['oauth2'], configuration: firecrawlProvider.configuration.slice(0, 3), }, ], } render() expect(screen.getByText('dataset.newKnowledge.providerUnavailable')).toBeInTheDocument() expect( screen.queryByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ).not.toBeInTheDocument() }) })