diff --git a/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx b/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx index 750f0284ad6..0b211b9e2f7 100644 --- a/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx +++ b/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx @@ -352,7 +352,7 @@ describe('CrawlSelectionForm', () => { const offDomainPage = screen.getByRole('checkbox', { name: 'Edit this page' }) expect(offDomainPage).toHaveAttribute('aria-disabled', 'true') expect(offDomainPage).toHaveAccessibleDescription( - 'https://github.com/langgenius/dify/edit/main/docs.md dataset.newKnowledge.skippedOffDomain', + 'github.com/langgenius/dify/edit/main/docs.md dataset.newKnowledge.skippedOffDomain', ) expect(screen.getByText('dataset.newKnowledge.skippedOffDomain')).toBeInTheDocument() expect(screen.getByText(/dataset\.newKnowledge\.skippedFailed/)).toBeInTheDocument() @@ -429,7 +429,7 @@ describe('CrawlSelectionForm', () => { const overflowPage = screen.getByRole('checkbox', { name: 'Page 201' }) expect(overflowPage).toHaveAttribute('aria-disabled', 'true') expect(overflowPage).toHaveAccessibleDescription( - 'https://docs.dify.ai/page-201 dataset.newKnowledge.maxPages: 200', + 'docs.dify.ai/page-201 dataset.newKnowledge.maxPages: 200', ) await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) diff --git a/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx b/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx index c07b11baee6..1f764d5c199 100644 --- a/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx +++ b/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx @@ -562,7 +562,7 @@ describe('CreateKnowledgePage', () => { expect(connectSource).toBeChecked() expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.websiteCrawl' })).toBeChecked() expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked() - for (const unavailableProvider of ['Jina Reader', 'WaterCrawl']) { + for (const unavailableProvider of ['Jina Reader', 'WaterCrawl', 'FakeCrawler']) { await user.click(screen.getByRole('radio', { name: unavailableProvider })) expect(screen.getByRole('radio', { name: unavailableProvider })).toBeChecked() } @@ -598,6 +598,12 @@ describe('CreateKnowledgePage', () => { expect(screen.getByText('Notion')).toBeInTheDocument() expect(screen.getByText('dataset.newKnowledge.notionNotConnected')).toBeInTheDocument() expect(screen.getByRole('button', { name: 'dataset.newKnowledge.connectNotion' })).toBeEnabled() + expect( + screen.queryByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }), + ).not.toBeInTheDocument() + expect( + screen.queryByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + ).not.toBeInTheDocument() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectNotion' })) expect(screen.getByRole('alert')).toHaveTextContent( 'dataset.newKnowledge.sourceSetupBackendDependency', @@ -784,6 +790,7 @@ describe('CreateKnowledgePage', () => { ) await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' })) + await user.click(screen.getByRole('radio', { name: 'Google Docs' })) expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled() await user.type( screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }), @@ -836,7 +843,7 @@ describe('CreateKnowledgePage', () => { expect(screen.getByText('oversized.pdf')).toBeInTheDocument() expect( - screen.getByText('dataset.newKnowledge.documentUploadExclusion.fileSize'), + screen.getByText(/dataset\.newKnowledge\.documentUploadExclusion\.fileSize/), ).toBeInTheDocument() expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeDisabled() expect(screen.queryByRole('button', { name: 'dataset.newKnowledge.preview' })).toBeNull() @@ -1002,6 +1009,7 @@ describe('CreateKnowledgePage', () => { renderPage() await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' })) + await user.click(screen.getByRole('radio', { name: 'Google Docs' })) await user.type( screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder'), 'Release notes', diff --git a/web/features/new-rag/__tests__/document-detail-model.spec.ts b/web/features/new-rag/__tests__/document-detail-model.spec.ts index 28a1c8dfd74..f4c259885ca 100644 --- a/web/features/new-rag/__tests__/document-detail-model.spec.ts +++ b/web/features/new-rag/__tests__/document-detail-model.spec.ts @@ -169,6 +169,7 @@ describe('document detail model', () => { it('bounds tree labels to a single readable summary', () => { expect(chunkTreeLabel('First line\nfull body', 3)).toBe('First line') + expect(chunkTreeLabel('## Markdown heading\nfull body', 3)).toBe('Markdown heading') expect(chunkTreeLabel(' ', 3)).toBe('#3') expect(chunkTreeLabel('x'.repeat(121), 3)).toBe(`${'x'.repeat(119)}…`) expect(chunkTreeLabel(`${'x'.repeat(118)}👨‍👩‍👧‍👦yz`, 3)).toBe(`${'x'.repeat(118)}👨‍👩‍👧‍👦…`) @@ -183,5 +184,9 @@ describe('document detail model', () => { body: '', heading: 'Standalone content', }) + expect(chunkContentParts('### Setup requirements\nWorkspace contract details')).toEqual({ + body: 'Workspace contract details', + heading: 'Setup requirements', + }) }) }) diff --git a/web/features/new-rag/__tests__/document-detail-page.spec.tsx b/web/features/new-rag/__tests__/document-detail-page.spec.tsx index a4aca700737..dfdc1821401 100644 --- a/web/features/new-rag/__tests__/document-detail-page.spec.tsx +++ b/web/features/new-rag/__tests__/document-detail-page.spec.tsx @@ -1304,7 +1304,12 @@ describe('DocumentDetailPage', () => { expect(screen.getByRole('alert')).toHaveTextContent( 'dataset.newKnowledge.documentReindexFailed', ) - expect(screen.getByText('dataset.newKnowledge.lastReadyRevisionHint')).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.viewTask' }), + ).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.retryReindexDocument' }), + ).toBeInTheDocument() expect(screen.getByRole('heading', { name: 'Parent content' })).toBeInTheDocument() }) @@ -1373,7 +1378,9 @@ describe('DocumentDetailPage', () => { }), ) render() - const button = screen.getByRole('button', { name: 'dataset.newKnowledge.reindexDocument' }) + const button = screen.getByRole('button', { + name: 'dataset.newKnowledge.reindexDocument', + }) await user.dblClick(button) expect(reindexMutation.mutateAsync).toHaveBeenCalledOnce() @@ -1490,7 +1497,9 @@ describe('DocumentDetailPage', () => { pages: [{ items: [task({ documentRevision: 4, id: 'old-failed', state: 'failed' })] }], } render() - const button = screen.getByRole('button', { name: 'dataset.newKnowledge.reindexDocument' }) + const button = screen.getByRole('button', { + name: 'dataset.newKnowledge.retryReindexDocument', + }) await user.click(button) await waitFor(() => expect(toastState.success).toHaveBeenCalled()) @@ -1551,7 +1560,7 @@ describe('DocumentDetailPage', () => { try { render() const reindexButton = screen.getByRole('button', { - name: 'dataset.newKnowledge.reindexDocument', + name: 'dataset.newKnowledge.retryReindexDocument', }) await act(async () => { fireEvent.click(reindexButton) diff --git a/web/features/new-rag/__tests__/document-model.spec.ts b/web/features/new-rag/__tests__/document-model.spec.ts index 49e6178770a..75a48b4f21a 100644 --- a/web/features/new-rag/__tests__/document-model.spec.ts +++ b/web/features/new-rag/__tests__/document-model.spec.ts @@ -2,6 +2,7 @@ import type { DocumentProcessingTask, LogicalDocument } from '../document-models import { documentDisplayStatus, newestTaskByDocument, + taskCanCancel, taskCanRetry, taskIsActive, taskNeedsAttention, @@ -108,8 +109,11 @@ describe('new Knowledge document model', () => { it('keeps task badge and actions aligned with the contract states', () => { expect(taskIsActive(task({ state: 'running' }))).toBe(true) + expect(taskCanCancel(task({ canCancel: false, state: 'running' }))).toBe(false) + expect(taskCanCancel(task({ canCancel: true, state: 'queued' }))).toBe(true) expect(taskNeedsAttention(task({ state: 'failed' }))).toBe(true) expect(taskCanRetry(task({ state: 'canceled' }))).toBe(false) + expect(taskCanRetry(task({ canRetry: true, state: 'canceled' }))).toBe(true) expect(taskNeedsAttention(task({ state: 'canceled' }))).toBe(false) expect(taskNeedsAttention(task({ state: 'succeeded' }))).toBe(false) expect(taskCanRetry(task({ state: 'superseded' }))).toBe(false) diff --git a/web/features/new-rag/__tests__/knowledge-overview-page.spec.tsx b/web/features/new-rag/__tests__/knowledge-overview-page.spec.tsx index 69ca3d19b0c..2cffe03a7ed 100644 --- a/web/features/new-rag/__tests__/knowledge-overview-page.spec.tsx +++ b/web/features/new-rag/__tests__/knowledge-overview-page.spec.tsx @@ -555,7 +555,7 @@ describe('KnowledgeOverviewPage', () => { ).toBeInTheDocument() expect( screen.getByRole('combobox', { name: 'dataset.newKnowledge.overview.timeRange' }), - ).toBeInTheDocument() + ).toHaveTextContent('dataset.newKnowledge.overview.today') expect( screen.getByRole('combobox', { name: 'dataset.newKnowledge.overview.operator' }), ).toBeInTheDocument() @@ -846,7 +846,7 @@ describe('KnowledgeOverviewPage', () => { it('shows first-indexing progress while the initial document task is running', () => { queryData.stats.source_count = 0 - queryData.stats.documents = 1 + queryData.stats.documents = 0 queryData.inventory.index_coverage.indexed = 0 queryData.tasks[0]!.operation = 'document_processing' queryData.tasks[0]!.progress_completed = 1 @@ -865,6 +865,31 @@ describe('KnowledgeOverviewPage', () => { expect( screen.getByText('dataset.newKnowledge.overview.indexedDocuments:{"indexed":1,"total":2}'), ).toBeInTheDocument() + expect( + screen.getByRole('progressbar', { name: 'dataset.newKnowledge.overview.indexing' }), + ).toHaveAttribute('aria-valuenow', '1') + expect( + screen.queryByRole('heading', { name: 'dataset.newKnowledge.overview.noSources' }), + ).not.toBeInTheDocument() + }) + + it('exposes first-indexing progress as indeterminate until the total is known', () => { + queryData.stats.source_count = 0 + queryData.stats.documents = 0 + queryData.inventory.index_coverage.indexed = 0 + queryData.tasks[0]!.operation = 'document_processing' + queryData.tasks[0]!.progress_completed = 0 + queryData.tasks[0]!.progress_percent = 0 + queryData.tasks[0]!.progress_total = 0 + queryData.tasks[0]!.state = 'queued' + + renderWithNuqs() + + const progressbar = screen.getByRole('progressbar', { + name: 'dataset.newKnowledge.overview.indexing', + }) + expect(progressbar).not.toHaveAttribute('aria-valuemax') + expect(progressbar).not.toHaveAttribute('aria-valuenow') }) it('keeps the generic first-indexing title when the task has no source', () => { diff --git a/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx b/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx index 388b556a0e9..3a14a85abe3 100644 --- a/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx +++ b/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx @@ -201,11 +201,17 @@ const externalAccess = { function renderForm({ externalAccess: externalAccessOverride = externalAccess, members = [], + onDraftFinish, + onDraftStart, + serverConflict, settings: settingsOverride = settings, space: spaceOverride = space, }: { externalAccess?: typeof externalAccess members?: Member[] + onDraftFinish?: () => void + onDraftStart?: () => void + serverConflict?: boolean settings?: KnowledgeFsSettingsResponse space?: KnowledgeFsSpaceDetailResponse } = {}) { @@ -223,8 +229,11 @@ function renderForm({ externalAccess={externalAccessOverride} members={members} permissions={[]} + serverConflict={serverConflict} settings={settingsOverride} space={spaceOverride} + onDraftFinish={onDraftFinish} + onDraftStart={onDraftStart} />, { wrapper: Wrapper }, ) @@ -548,6 +557,52 @@ describe('KnowledgeSettingsForm', () => { ).toBeDisabled() }) + it('blocks a stale draft after the server baseline changes and restores the latest value', async () => { + const user = userEvent.setup() + const onDraftFinish = vi.fn() + const onDraftStart = vi.fn() + const renderWithName = (name: string, serverConflict = false) => ( + + ) + const view = renderForm({ onDraftFinish, onDraftStart }) + const nameInput = screen.getByRole('textbox', { name: 'datasetSettings.form.name' }) + await user.clear(nameInput) + await user.type(nameInput, 'Version B') + + expect(onDraftStart).toHaveBeenCalled() + view.rerender(renderWithName('Version C', true)) + expect(nameInput).toHaveValue('Version B') + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.settings.serverConflict', + ) + expect( + screen.getByRole('button', { + name: 'dataset.newKnowledge.settings.saveChanges', + }), + ).toBeDisabled() + + await user.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + + expect(nameInput).toHaveValue('Version C') + expect(onDraftFinish).toHaveBeenCalledOnce() + expect(serviceMock.patchSpace).not.toHaveBeenCalled() + }) + it('allows embedding and retrieval to be configured together during initial setup', async () => { const user = userEvent.setup() renderForm({ diff --git a/web/features/new-rag/__tests__/knowledge-settings-page.spec.tsx b/web/features/new-rag/__tests__/knowledge-settings-page.spec.tsx index e0be05ae82d..28d4cc25116 100644 --- a/web/features/new-rag/__tests__/knowledge-settings-page.spec.tsx +++ b/web/features/new-rag/__tests__/knowledge-settings-page.spec.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { screen, waitFor } from '@testing-library/react' +import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { render } from '@/test/console/render' import { KnowledgeSettingsPage } from '../knowledge-settings-page' @@ -30,7 +30,26 @@ vi.mock('@/service/use-common', () => ({ })) vi.mock('../knowledge-settings-form', () => ({ - KnowledgeSettingsForm: () =>
settings-form
, + KnowledgeSettingsForm: ({ + onDraftFinish, + onDraftStart, + serverConflict, + }: { + onDraftFinish: () => void + onDraftStart: () => void + serverConflict: boolean + }) => ( +
+ settings-form + {serverConflict ? 'server-conflict' : 'no-conflict'} + + +
+ ), })) const queryData = vi.hoisted(() => ({ @@ -96,7 +115,10 @@ function renderPage() { const Wrapper = ({ children }: { children: ReactNode }) => ( {children} ) - return render(, { wrapper: Wrapper }) + return { + queryClient, + ...render(, { wrapper: Wrapper }), + } } describe('KnowledgeSettingsPage', () => { @@ -113,7 +135,15 @@ describe('KnowledgeSettingsPage', () => { await waitFor(() => { expect(screen.queryByText('settings-form')).not.toBeInTheDocument() - expect(screen.getByRole('status')).toBeInTheDocument() + expect(screen.getByRole('status')).toHaveTextContent('common.loading') + expect(screen.getByRole('status')).not.toHaveTextContent( + 'dataset.newKnowledge.settings.basicInfo', + ) + expect(screen.getByRole('status')).not.toHaveTextContent( + 'dataset.newKnowledge.settings.dangerZone', + ) + expect(screen.getByText('dataset.newKnowledge.settings.basicInfo')).toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.settings.dangerZone')).toBeInTheDocument() }) }) @@ -156,4 +186,24 @@ describe('KnowledgeSettingsPage', () => { }), ).toBe(false) }) + + it('keeps an active draft mounted and reports a conflict when the server version changes', async () => { + const user = userEvent.setup() + membersQueryMock.data = { accounts: [] } + membersQueryMock.isPending = false + const { queryClient } = renderPage() + + expect(await screen.findByText('no-conflict')).toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'start-draft' })) + act(() => { + queryClient.setQueryData(['knowledge-fs', 'space'], { + ...queryData.space, + resource_version: 2, + }) + }) + + expect(await screen.findByText('server-conflict')).toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'finish-draft' })) + expect(await screen.findByText('no-conflict')).toBeInTheDocument() + }) }) diff --git a/web/features/new-rag/__tests__/quality-page.spec.tsx b/web/features/new-rag/__tests__/quality-page.spec.tsx index 86729a7083d..5360028769a 100644 --- a/web/features/new-rag/__tests__/quality-page.spec.tsx +++ b/web/features/new-rag/__tests__/quality-page.spec.tsx @@ -349,6 +349,52 @@ describe('QualityPage', () => { expect(serviceMock.createGolden).not.toHaveBeenCalled() }) + it('reveals the full annotation and submits edits through the update contract', async () => { + const user = userEvent.setup() + serviceMock.updateGolden.mockResolvedValue({}) + renderPage() + + await screen.findByText('What is the refund policy?') + const annotation = screen.getByRole('button', { name: 'Must cite the refund clause.' }) + await user.hover(annotation) + await waitFor(() => expect(screen.getAllByText('Must cite the refund clause.')).toHaveLength(2)) + + await user.click( + screen.getByRole('button', { + name: /dataset\.newKnowledge\.qualityPage\.questionActions/, + }), + ) + await user.click( + await screen.findByRole('menuitem', { name: 'dataset.newKnowledge.qualityPage.edit' }), + ) + const annotationInput = screen.getByPlaceholderText( + 'dataset.newKnowledge.qualityPage.annotationPlaceholder', + ) + await user.clear(annotationInput) + await user.type(annotationInput, 'Updated expected answer') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.qualityPage.save' })) + + await waitFor(() => + expect(serviceMock.updateGolden).toHaveBeenCalledWith( + { + body: { + annotation: 'Updated expected answer', + evidence_text: '', + expected_evidence_ids: [], + match_policy: 'all', + question: 'What is the refund policy?', + tags: ['billing'], + }, + params: { control_space_id: 'space-1', question_id: 'golden-1' }, + }, + expect.anything(), + ), + ) + expect( + screen.queryByRole('dialog', { name: 'dataset.newKnowledge.qualityPage.editTitle' }), + ).not.toBeInTheDocument() + }) + it('resolves the protected trace reference before navigating', async () => { const user = userEvent.setup() navigationMock.tab = 'bad-cases' diff --git a/web/features/new-rag/__tests__/retrieval-test-page.spec.tsx b/web/features/new-rag/__tests__/retrieval-test-page.spec.tsx index a1258848291..27a7c90c33b 100644 --- a/web/features/new-rag/__tests__/retrieval-test-page.spec.tsx +++ b/web/features/new-rag/__tests__/retrieval-test-page.spec.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { screen, waitFor } from '@testing-library/react' +import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { render } from '@/test/console/render' import { RetrievalTestPage } from '../retrieval-test-page' @@ -294,7 +294,21 @@ describe('RetrievalTestPage', () => { onEvent({ createdAt: new Date(1_800_000_000_000 + seconds * 1000).toISOString(), id: `event-${index + 1}`, - payload: {}, + payload: + stage === 'retrieving' + ? { + results: [ + { + chunkCount: 3, + createdAt: '2027-01-15T08:00:02.000Z', + question: 'Refund policy', + sourceId: 'internal-source-id', + }, + ], + sourceCount: 2, + unknownTotal: 9, + } + : {}, researchTaskJobId: 'research-completed', sequence: index + 1, stage, @@ -308,11 +322,11 @@ describe('RetrievalTestPage', () => { renderPage() await user.click(screen.getByText('Compare the refund policies')) - expect( - screen.getByRole('button', { - name: 'dataset.newKnowledge.retrievalTest.processLog', - }), - ).toHaveAttribute('aria-pressed', 'true') + const processLog = screen.getByRole('button', { + name: 'dataset.newKnowledge.retrievalTest.processLog', + }) + expect(processLog).toHaveAttribute('aria-pressed', 'false') + await user.click(processLog) await waitFor(() => expect(apiMock.streamCapability).toHaveBeenCalledWith({ @@ -329,10 +343,48 @@ describe('RetrievalTestPage', () => { expect(screen.getByText('5s')).toBeInTheDocument() expect(screen.getByText('7s')).toBeInTheDocument() expect(screen.getByText('11s')).toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.sources: 2')).toBeInTheDocument() + expect( + screen.getByText('Refund policy · 3 dataset.newKnowledge.chunkCount'), + ).toBeInTheDocument() + expect(screen.queryByText('internal-source-id')).not.toBeInTheDocument() + expect(screen.queryByText('2027-01-15T08:00:02.000Z')).not.toBeInTheDocument() + expect(screen.queryByText(/unknown.*9/i)).not.toBeInTheDocument() expect(apiMock.refetchTasks).not.toHaveBeenCalled() expect(apiMock.refetchPartials).not.toHaveBeenCalled() }) + it('replaces the just-now label after the first minute', () => { + vi.useFakeTimers() + try { + const now = Date.parse('2026-07-29T09:04:30Z') + vi.setSystemTime(now) + apiMock.traces = [ + { + completed: true, + created_at: new Date(now - 30_000).toISOString(), + id: 'trace-recent', + mode: 'fast', + profile: {}, + query: 'A recent retrieval run', + scores: {}, + stages: [], + }, + ] + + renderPage() + + expect(screen.getByText('dataset.newKnowledge.retrievalTest.justNow')).toBeInTheDocument() + act(() => vi.advanceTimersByTime(30_001)) + expect( + screen.queryByText('dataset.newKnowledge.retrievalTest.justNow'), + ).not.toBeInTheDocument() + expect(screen.getByText('A recent retrieval run')).toBeInTheDocument() + } finally { + vi.useRealTimers() + } + }) + it('renders generated Research answer deltas while the task is still active', async () => { apiMock.researchTasks = [ { @@ -729,6 +781,39 @@ describe('RetrievalTestPage', () => { expect(screen.getByText('provider timed out')).toBeInTheDocument() }) + it('cancels a stalled fast run and restores the composer', async () => { + let streamSignal: AbortSignal | undefined + apiMock.streamQuery.mockImplementationOnce( + ({ signal }: { signal?: AbortSignal }) => + new Promise((_resolve, reject) => { + streamSignal = signal + signal?.addEventListener('abort', () => { + reject(new DOMException('Aborted', 'AbortError')) + }) + }), + ) + const user = userEvent.setup() + renderPage() + + const queryInput = screen.getByLabelText('dataset.newKnowledge.retrievalTest.queryPlaceholder') + await user.type(queryInput, 'A query that never completes') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retrievalTest.run' })) + + expect(queryInput).toBeDisabled() + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.retrievalTest.cancel', + }), + ) + + expect(streamSignal?.aborted).toBe(true) + expect(queryInput).toBeEnabled() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.retrievalTest.run' }), + ).toBeEnabled() + expect(screen.getByText('dataset.newKnowledge.retrievalTest.emptyTitle')).toBeInTheDocument() + }) + it('maps an empty unpublished knowledge space to the designed no-results state', async () => { apiMock.streamQuery.mockRejectedValueOnce( new Response('Published runtime snapshot unavailable', { status: 503 }), @@ -749,5 +834,40 @@ describe('RetrievalTestPage', () => { screen.queryByText('dataset.newKnowledge.retrievalTest.failedTitle'), ).not.toBeInTheDocument() expect(screen.getAllByText('Anything here?')).toHaveLength(2) + expect( + screen.queryByRole('button', { + name: 'dataset.newKnowledge.retrievalTest.makeBadCase', + }), + ).not.toBeInTheDocument() + }) + + it('does not guess a trace id for a trace-less local run', async () => { + apiMock.traces = [ + { + completed: true, + created_at: new Date().toISOString(), + id: 'different-trace', + mode: 'fast', + profile: {}, + query: 'Repeated question', + scores: {}, + stages: [], + }, + ] + const user = userEvent.setup() + renderPage() + + await user.type( + screen.getByLabelText('dataset.newKnowledge.retrievalTest.queryPlaceholder'), + 'Repeated question', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retrievalTest.run' })) + + expect( + screen.queryByRole('button', { + name: 'dataset.newKnowledge.retrievalTest.makeBadCase', + }), + ).not.toBeInTheDocument() + expect(apiMock.createBadCase).not.toHaveBeenCalled() }) }) diff --git a/web/features/new-rag/__tests__/sources-page.spec.tsx b/web/features/new-rag/__tests__/sources-page.spec.tsx index 8a9841b997a..b58e768df4b 100644 --- a/web/features/new-rag/__tests__/sources-page.spec.tsx +++ b/web/features/new-rag/__tests__/sources-page.spec.tsx @@ -488,7 +488,8 @@ describe('SourcesPage', () => { expect(openSource).toHaveAttribute('rel', 'noopener noreferrer') }) - it('hides the row action menu when a read-only source has no openable URI', () => { + it('opens an Amazon S3 source in the AWS console for read-only users', async () => { + const user = userEvent.setup() permissionState.workspacePermissionKeys = ['dataset.acl.readonly'] sourcesQuery.data = { pages: [ @@ -496,7 +497,7 @@ describe('SourcesPage', () => { items: [ source({ type: 'object-storage', - uri: 's3://private-bucket/product-documentation', + uri: 's3://private-bucket/Product%20documentation/文档', }), ], }, @@ -505,11 +506,17 @@ describe('SourcesPage', () => { render() - expect( - screen.queryByRole('button', { + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.sourceActions:{"name":"Product documentation"}', }), - ).not.toBeInTheDocument() + ) + expect( + screen.getByRole('menuitem', { name: 'dataset.newKnowledge.editSource' }), + ).toHaveAttribute( + 'href', + 'https://s3.console.aws.amazon.com/s3/buckets/private-bucket?prefix=Product+documentation%2F%E6%96%87%E6%A1%A3', + ) }) it('uses dataset.external.connect for every source mutation action', async () => { diff --git a/web/features/new-rag/__tests__/website-crawl-preview.spec.tsx b/web/features/new-rag/__tests__/website-crawl-preview.spec.tsx index f09ca7da7fd..9f3d2b9d1d6 100644 --- a/web/features/new-rag/__tests__/website-crawl-preview.spec.tsx +++ b/web/features/new-rag/__tests__/website-crawl-preview.spec.tsx @@ -800,7 +800,7 @@ describe('WebsiteCrawlPreview', () => { expect(status).toHaveAttribute('role', 'status') expect(status.querySelector('button')).not.toBeInTheDocument() const skeletons = screen.getAllByTestId('crawl-page-skeleton') - expect(skeletons).toHaveLength(2) + expect(skeletons).toHaveLength(4) expect(skeletons[0]?.closest('ul')).toBe(screen.getByRole('list')) await act(async () => vi.advanceTimersByTime(1500)) expect(await screen.findByText('Three')).toBeInTheDocument() diff --git a/web/features/new-rag/add-source-page.tsx b/web/features/new-rag/add-source-page.tsx index d0b5c375bed..428a5bb20e7 100644 --- a/web/features/new-rag/add-source-page.tsx +++ b/web/features/new-rag/add-source-page.tsx @@ -1159,7 +1159,7 @@ export function AddSourcePage({ return ( <> -
+

{t(($) => $['newKnowledge.addSource'])} @@ -1168,7 +1168,7 @@ export function AddSourcePage({ {t(($) => $['newKnowledge.addSourceDescription'])}

-
+
{ diff --git a/web/features/new-rag/components/knowledge-settings-members.tsx b/web/features/new-rag/components/knowledge-settings-members.tsx index 8d866ea59f4..dea1bc55116 100644 --- a/web/features/new-rag/components/knowledge-settings-members.tsx +++ b/web/features/new-rag/components/knowledge-settings-members.tsx @@ -13,7 +13,6 @@ import { SelectItem, SelectItemIndicator, SelectItemText, - SelectLabel, SelectTrigger, } from '@langgenius/dify-ui/select' import { useState } from 'react' @@ -52,7 +51,6 @@ export function KnowledgeSettingsMembers({ const [open, setOpen] = useState(false) const [search, setSearch] = useState('') - const owner = members.find((member) => member.id === ownerAccountId) const selectedMembers = members.filter( (member) => member.id !== ownerAccountId && selectedMemberIds.includes(member.id), ) @@ -86,8 +84,11 @@ export function KnowledgeSettingsMembers({ if (value) onVisibilityChange(value as KnowledgeFsControlSpaceVisibility) }} > - {tSettings(($) => $['form.permissions'])} - + $['form.permissions'])} + className="h-9 w-full" + disabled={disabled} + > {visibilityLabel(visibility)} @@ -107,12 +108,6 @@ export function KnowledgeSettingsMembers({ hasError && 'ring-1 ring-text-destructive', )} > - {owner && ( - - - {owner.name} - - )} {selectedMembers.map((member) => ( {filteredMembers.map((member) => { const isOwner = member.id === ownerAccountId - const isSelected = isOwner || selectedMemberIds.includes(member.id) + const isSelected = selectedMemberIds.includes(member.id) return (