fix(web): stabilize knowledge upgrade transitions

This commit is contained in:
Stephen Zhou 2026-08-18 19:33:53 +08:00
parent b821021bd0
commit 4fc0d8bf2a
No known key found for this signature in database
4 changed files with 101 additions and 6 deletions

View File

@ -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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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([
{

View File

@ -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) => (
<KnowledgeSpaceCard
key={knowledgeSpace.control_space_id}
knowledgeSpace={knowledgeSpace}

View File

@ -376,6 +376,37 @@ describe('consoleQuery transport context', () => {
)
})
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(

View File

@ -72,6 +72,16 @@ export type ConsoleClientContext = TanstackQueryOperationContext & {
type ConsoleClientLink = ClientLink<ConsoleClientContext>
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<ConsoleClientContext>(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)
}),
],
})