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)
}),
],
})