90901a4458
build / build (push) Successful in 1m1s
新增本周直接查询按钮,所选范围自动分批读取并校验总数、重复与游标完整性;删除手动加载更多,固定展示完整字段,空列不隐藏,不伪造译文或评估。 验证:全量、固定列及本周入口回归先红后绿;完整make verify通过,前端273通过;真实只读源浏览器默认及快捷查询均639条全部唯一展示,14必需字段按序,右侧字段可达、手机无外溢、JS异常0。无新增迁移或源库写入。
89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { fetchAllQARecords } from './qaOps'
|
|
const row = (id: string) => ({
|
|
id,
|
|
status: 'succeeded',
|
|
question: '明确合成问题',
|
|
answer: '明确合成答案',
|
|
asked_at: '2026-10-14T03:00:00Z',
|
|
time_source: 'request_message.created_at',
|
|
asker: '合成夹具',
|
|
trigger_person: '',
|
|
department: '',
|
|
region: '',
|
|
references: [],
|
|
media: [],
|
|
source_coverage: { verdict: '', reason_code: '', evaluator_version: '' },
|
|
feedback: { asker_value: '', up: 0, down: 0 },
|
|
coverage: { state: 'pending', rule_version: 'fixture-rule' },
|
|
})
|
|
const page = (ids: string[], total: number, cursor = '') => ({
|
|
items: ids.map(row),
|
|
columns: [],
|
|
total,
|
|
has_more: Boolean(cursor),
|
|
next_cursor: cursor,
|
|
as_of: '2026-10-14T04:00:00Z',
|
|
})
|
|
const query = { start: '2026-10-11T16:00:00Z', end: '2026-10-18T08:00:00Z', keyword: '%_原文', cursor: 'stale-cursor' }
|
|
const respond = (data: unknown) => new Response(JSON.stringify({ data }), { status: 200 })
|
|
afterEach(() => vi.unstubAllGlobals())
|
|
describe('all QA records without manual pagination', () => {
|
|
it('rejects incomplete, duplicate, drifting and cycling batches', async () => {
|
|
const cases = [
|
|
[page(['a'], 2)],
|
|
[page(['a'], 2, 'n'), page(['a'], 2)],
|
|
[page(['a'], 2, 'n'), page(['b'], 3)],
|
|
[page(['a'], 3, 'n'), page(['b'], 3, 'n')],
|
|
]
|
|
for (const pages of cases) {
|
|
const fetcher = vi.fn()
|
|
pages.forEach((data) => fetcher.mockResolvedValueOnce(respond(data)))
|
|
vi.stubGlobal('fetch', fetcher)
|
|
await expect(fetchAllQARecords(query)).rejects.toThrow()
|
|
}
|
|
})
|
|
it('surfaces a later source failure instead of returning the first batch', async () => {
|
|
const fetcher = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(respond(page(['a'], 2, 'n')))
|
|
.mockResolvedValueOnce(new Response(JSON.stringify({ error: '明确合成源不可用' }), { status: 503 }))
|
|
vi.stubGlobal('fetch', fetcher)
|
|
await expect(fetchAllQARecords(query)).rejects.toThrow('明确合成源不可用')
|
|
})
|
|
it('accepts a genuinely empty complete scope', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(respond(page([], 0))))
|
|
expect((await fetchAllQARecords(query)).items).toEqual([])
|
|
})
|
|
it('does not start or continue requests after cancellation', async () => {
|
|
const controller = new AbortController()
|
|
controller.abort()
|
|
const fetcher = vi.fn().mockResolvedValue(respond(page([], 0)))
|
|
vi.stubGlobal('fetch', fetcher)
|
|
await expect(fetchAllQARecords(query, controller.signal)).rejects.toMatchObject({ name: 'AbortError' })
|
|
expect(fetcher).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('reads every transport batch and verifies final count before returning', async () => {
|
|
const fetcher = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(respond(page(['a', 'b'], 3, 'next')))
|
|
.mockResolvedValueOnce(respond(page(['c'], 3)))
|
|
vi.stubGlobal('fetch', fetcher)
|
|
const progress = vi.fn()
|
|
const result = await fetchAllQARecords(query, undefined, progress)
|
|
expect(result.items.map((r) => r.id)).toEqual(['a', 'b', 'c'])
|
|
expect(result.has_more).toBe(false)
|
|
expect(result.total).toBe(3)
|
|
expect(progress.mock.calls).toEqual([
|
|
[2, 3],
|
|
[3, 3],
|
|
])
|
|
const urls = fetcher.mock.calls.map((call) => new URL(String(call[0]), 'http://localhost'))
|
|
expect(urls.map((url) => url.searchParams.get('cursor'))).toEqual(['', 'next'])
|
|
expect(
|
|
urls.every((url) => url.searchParams.get('limit') === '100' && url.searchParams.get('keyword') === query.keyword)
|
|
).toBe(true)
|
|
})
|
|
})
|