chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import { afterEach, beforeAll, describe, expect, test } from 'bun:test'
|
||||
|
||||
type DocumentsRequest = {
|
||||
status_filter?: 'pending' | 'processing' | 'preprocessed' | 'processed' | 'failed' | null
|
||||
page: number
|
||||
page_size: number
|
||||
sort_field: 'created_at' | 'updated_at' | 'id' | 'file_path'
|
||||
sort_direction: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
type LightragApiModule = typeof import('./lightrag')
|
||||
|
||||
const storageMock = () => {
|
||||
const data = new Map<string, string>()
|
||||
|
||||
return {
|
||||
getItem: (key: string) => data.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
data.set(key, value)
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
data.delete(key)
|
||||
},
|
||||
clear: () => {
|
||||
data.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let apiModule: LightragApiModule
|
||||
|
||||
beforeAll(async () => {
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
value: storageMock(),
|
||||
configurable: true
|
||||
})
|
||||
Object.defineProperty(globalThis, 'sessionStorage', {
|
||||
value: storageMock(),
|
||||
configurable: true
|
||||
})
|
||||
|
||||
apiModule = await import('./lightrag')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
apiModule.__resetPaginatedDocumentRequestsForTests()
|
||||
})
|
||||
|
||||
describe('getDocumentsPaginated', () => {
|
||||
test('issues a fresh request after aborting a timed-out in-flight request', async () => {
|
||||
const request: DocumentsRequest = {
|
||||
status_filter: null,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
sort_field: 'updated_at',
|
||||
sort_direction: 'desc'
|
||||
}
|
||||
|
||||
let callCount = 0
|
||||
const resolvers: Array<(value: any) => void> = []
|
||||
|
||||
apiModule.__setPaginatedDocumentsPostForTests((_request, controller) => {
|
||||
callCount += 1
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
resolvers.push(resolve)
|
||||
controller.signal.addEventListener(
|
||||
'abort',
|
||||
() => reject(new DOMException('Aborted', 'AbortError')),
|
||||
{ once: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const firstRequest = apiModule.getDocumentsPaginated(request)
|
||||
const secondRequest = apiModule.getDocumentsPaginated(request)
|
||||
|
||||
expect(callCount).toBe(1)
|
||||
|
||||
apiModule.abortDocumentsPaginated(request)
|
||||
const [firstResult, secondResult] = await Promise.allSettled([
|
||||
firstRequest,
|
||||
secondRequest
|
||||
])
|
||||
expect(firstResult.status).toBe('rejected')
|
||||
expect(secondResult.status).toBe('rejected')
|
||||
|
||||
const thirdRequest = apiModule.getDocumentsPaginated(request)
|
||||
expect(callCount).toBe(2)
|
||||
|
||||
resolvers[1]({
|
||||
documents: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total_count: 0,
|
||||
total_pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false
|
||||
},
|
||||
status_counts: { all: 0 }
|
||||
})
|
||||
|
||||
await expect(thirdRequest).resolves.toEqual({
|
||||
documents: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total_count: 0,
|
||||
total_pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false
|
||||
},
|
||||
status_counts: { all: 0 }
|
||||
})
|
||||
})
|
||||
|
||||
test('times out hanging requests and allows a fresh retry', async () => {
|
||||
const request: DocumentsRequest = {
|
||||
status_filter: null,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
sort_field: 'updated_at',
|
||||
sort_direction: 'desc'
|
||||
}
|
||||
|
||||
let callCount = 0
|
||||
const resolvers: Array<(value: any) => void> = []
|
||||
|
||||
apiModule.__setPaginatedDocumentsPostForTests((_request, controller) => {
|
||||
callCount += 1
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
resolvers.push(resolve)
|
||||
controller.signal.addEventListener(
|
||||
'abort',
|
||||
() => reject(new DOMException('Aborted', 'AbortError')),
|
||||
{ once: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
await expect(
|
||||
apiModule.getDocumentsPaginatedWithTimeout(request, 1)
|
||||
).rejects.toThrow('Document fetch timeout')
|
||||
|
||||
expect(callCount).toBe(1)
|
||||
|
||||
const retryRequest = apiModule.getDocumentsPaginated(request)
|
||||
expect(callCount).toBe(2)
|
||||
|
||||
resolvers[1]({
|
||||
documents: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total_count: 0,
|
||||
total_pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false
|
||||
},
|
||||
status_counts: { all: 0 }
|
||||
})
|
||||
|
||||
await expect(retryRequest).resolves.toEqual({
|
||||
documents: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total_count: 0,
|
||||
total_pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false
|
||||
},
|
||||
status_counts: { all: 0 }
|
||||
})
|
||||
})
|
||||
|
||||
test('does not abort a shared request when only one timeout subscriber expires', async () => {
|
||||
const request: DocumentsRequest = {
|
||||
status_filter: null,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
sort_field: 'updated_at',
|
||||
sort_direction: 'desc'
|
||||
}
|
||||
|
||||
let callCount = 0
|
||||
let resolveSharedRequest: ((value: any) => void) | undefined
|
||||
let abortCount = 0
|
||||
|
||||
apiModule.__setPaginatedDocumentsPostForTests((_request, controller) => {
|
||||
callCount += 1
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
resolveSharedRequest = resolve
|
||||
controller.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
abortCount += 1
|
||||
reject(new DOMException('Aborted', 'AbortError'))
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const shortTimeoutRequest = apiModule.getDocumentsPaginatedWithTimeout(request, 1)
|
||||
const longTimeoutRequest = apiModule.getDocumentsPaginatedWithTimeout(request, 100)
|
||||
|
||||
await expect(shortTimeoutRequest).rejects.toThrow('Document fetch timeout')
|
||||
|
||||
expect(callCount).toBe(1)
|
||||
expect(abortCount).toBe(0)
|
||||
|
||||
resolveSharedRequest?.({
|
||||
documents: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total_count: 0,
|
||||
total_pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false
|
||||
},
|
||||
status_counts: { all: 0 }
|
||||
})
|
||||
|
||||
await expect(longTimeoutRequest).resolves.toEqual({
|
||||
documents: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total_count: 0,
|
||||
total_pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false
|
||||
},
|
||||
status_counts: { all: 0 }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isUserAbortError', () => {
|
||||
// Regression: the Stop button must suppress query cancellation everywhere it
|
||||
// surfaces — both the main stream catch and the guest-token retry catch (which
|
||||
// otherwise redirects an aborting guest to the login page). Both sites share
|
||||
// this predicate, so locking down its behavior guards both fixes.
|
||||
test('treats an aborted signal as a user abort regardless of the error', () => {
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
expect(apiModule.isUserAbortError(controller.signal, new Error('boom'))).toBe(true)
|
||||
})
|
||||
|
||||
test('treats an AbortError as a user abort even when the signal is absent', () => {
|
||||
const abortError = new DOMException('Aborted', 'AbortError')
|
||||
expect(apiModule.isUserAbortError(undefined, abortError)).toBe(true)
|
||||
})
|
||||
|
||||
test('does not treat a real failure on a live signal as a user abort', () => {
|
||||
const controller = new AbortController()
|
||||
expect(apiModule.isUserAbortError(controller.signal, new Error('network down'))).toBe(false)
|
||||
expect(apiModule.isUserAbortError(undefined, new Error('network down'))).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user