From 4fc0d8bf2a47f1b9d181d64761a0234dd9dc3ec0 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:33:53 +0800 Subject: [PATCH] fix(web): stabilize knowledge upgrade transitions --- .../__tests__/new-knowledge-list.spec.tsx | 47 ++++++++++++++++++- web/features/new-rag/new-knowledge-list.tsx | 15 ++++-- web/service/client.spec.ts | 31 ++++++++++++ web/service/client.ts | 14 +++++- 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx b/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx index 9eab1999240..c262b1257ec 100644 --- a/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx +++ b/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx @@ -111,7 +111,7 @@ const datasetListMock = vi.hoisted(() => ({ old_dataset_id: string snapshot_at: string stage: 'completed' | 'submitting_documents' - status: 'failed' | 'succeeded' + status: 'failed' | 'queued' | 'running' | 'succeeded' total_documents: number total_sources: number } | null @@ -412,6 +412,51 @@ describe('NewKnowledgeList', () => { }) }) + it('keeps an active upgrade card when its control space appears before migration completes', () => { + setResolvedPage([ + { + createdAt: '2026-08-18T00:00:00Z', + id: 'space-1', + name: 'space-1', + revision: 1, + slug: 'space-1', + tenantId: 'tenant-1', + updatedAt: '2026-08-18T00:00:00Z', + }, + ]) + datasetListMock.data = { + data: [ + { + description: 'Legacy knowledge', + id: 'dataset-1', + knowledge_fs_upgrade: { + can_retry: false, + can_upgrade: false, + job: { + completed_documents: 4, + completed_sources: 1, + id: 'upgrade-1', + new_control_space_id: 'space-1', + old_dataset_id: 'dataset-1', + snapshot_at: '2026-08-18T00:00:00Z', + stage: 'submitting_documents', + status: 'running', + total_documents: 10, + total_sources: 1, + }, + }, + name: 'Support knowledge', + tags: [], + }, + ], + } + + renderWithNuqs() + + expect(screen.getByText('upgrade:dataset-1:running')).toBeInTheDocument() + expect(screen.queryByRole('link', { name: 'space-1' })).not.toBeInTheDocument() + }) + it('does not highlight a knowledge space for a historical successful upgrade', () => { setResolvedPage([ { diff --git a/web/features/new-rag/new-knowledge-list.tsx b/web/features/new-rag/new-knowledge-list.tsx index e1e64303091..92a395c8a3e 100644 --- a/web/features/new-rag/new-knowledge-list.tsx +++ b/web/features/new-rag/new-knowledge-list.tsx @@ -141,12 +141,21 @@ export function NewKnowledgeList({ query: debouncedSearchValue, tagIds, }) && - (!upgrade.job.new_control_space_id || + (upgrade.job.status !== 'succeeded' || + !upgrade.job.new_control_space_id || !knowledgeSpaces.some( (knowledgeSpace) => knowledgeSpace.control_space_id === upgrade.job.new_control_space_id, )), ) - const hasVisibleKnowledge = knowledgeSpaces.length > 0 || pendingUpgradeCards.length > 0 + const pendingUpgradeControlSpaceIds = new Set( + pendingUpgradeCards.flatMap((upgrade) => + upgrade.job.new_control_space_id ? [upgrade.job.new_control_space_id] : [], + ), + ) + const visibleKnowledgeSpaces = knowledgeSpaces.filter( + (knowledgeSpace) => !pendingUpgradeControlSpaceIds.has(knowledgeSpace.control_space_id), + ) + const hasVisibleKnowledge = visibleKnowledgeSpaces.length > 0 || pendingUpgradeCards.length > 0 useEffect(() => { if (!highlightedControlSpaceId) return @@ -286,7 +295,7 @@ export function NewKnowledgeList({ onSucceeded={setHighlightedControlSpaceId} /> ))} - {knowledgeSpaces.map((knowledgeSpace) => ( + {visibleKnowledgeSpaces.map((knowledgeSpace) => ( { ) }) + it('should not report query cancellation as a transport error', async () => { + const request = vi.fn().mockRejectedValue(new DOMException('Aborted', 'AbortError')) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const consoleQuery = await loadConsoleQueryWithRequest(request) + const queryOptions = consoleQuery.datasets.get.queryOptions({ + input: { query: { limit: 30, page: 1 } }, + }) + + await Promise.resolve( + queryOptions.queryFn({ signal: new AbortController().signal } as QueryFunctionContext), + ).catch(() => undefined) + + expect(errorSpy).not.toHaveBeenCalled() + }) + + it('should continue reporting non-cancellation transport errors', async () => { + const networkError = new TypeError('Network failed') + const request = vi.fn().mockRejectedValue(networkError) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const consoleQuery = await loadConsoleQueryWithRequest(request) + const queryOptions = consoleQuery.datasets.get.queryOptions({ + input: { query: { limit: 30, page: 1 } }, + }) + + await Promise.resolve( + queryOptions.queryFn({ signal: new AbortController().signal } as QueryFunctionContext), + ).catch(() => undefined) + + expect(errorSpy).toHaveBeenCalledWith(networkError) + }) + it('should serialize trial app dataset ids as repeated query params', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response( diff --git a/web/service/client.ts b/web/service/client.ts index b5c40317145..9b40250132b 100644 --- a/web/service/client.ts +++ b/web/service/client.ts @@ -72,6 +72,16 @@ export type ConsoleClientContext = TanstackQueryOperationContext & { type ConsoleClientLink = ClientLink +function isAbortError(error: unknown): boolean { + if (!error || typeof error !== 'object') return false + if ('name' in error && error.name === 'AbortError') return true + return 'cause' in error && isAbortError(error.cause) +} + +function reportClientError(error: unknown) { + if (!isAbortError(error)) console.error(error) +} + function createConsoleOpenAPILink(contract: AnyContractRouter): ConsoleClientLink { return new OpenAPILink(contract, { url: getBaseURL(API_PREFIX), @@ -89,7 +99,7 @@ function createConsoleOpenAPILink(contract: AnyContractRouter): ConsoleClientLin }, interceptors: [ onError((error) => { - console.error(error) + reportClientError(error) }), ], }) @@ -106,7 +116,7 @@ const marketplaceLink = new OpenAPILink(marketplaceRouterContract, { }, interceptors: [ onError((error) => { - console.error(error) + reportClientError(error) }), ], })