feat(intelligent-eval): paginate the eval list
All checks were successful
CI / test (push) Successful in 4m5s
All checks were successful
CI / test (push) Successful in 4m5s
GET /api/intelligent-evals 支持 page/page_size(默认不传仍返回全部,向后兼容): repository 加 count/list_page,lifecycle 加 list_evals_page,router 返回 total。 前端服务端分页:useIntelligentEvalRead 接 page/pageSize,list 存 total, IntelligentEvals 表格 showSizeChanger + 页码切换重新加载;5s 轮询保持当前页。 测试:+3 后端分页 + hook 页码透传/总数断言,895 passed,vitest 19 passed
This commit is contained in:
parent
b4f9c887f4
commit
cc2ac8da89
@ -218,6 +218,12 @@ def list_evals(session: Session) -> list[IntelligentEval]:
|
|||||||
return IntelligentEvalRepository(session).list_all()
|
return IntelligentEvalRepository(session).list_all()
|
||||||
|
|
||||||
|
|
||||||
|
def list_evals_page(session: Session, offset: int, limit: int) -> tuple[list[IntelligentEval], int]:
|
||||||
|
"""Return one page of evals (newest first) plus the total count."""
|
||||||
|
repo = IntelligentEvalRepository(session)
|
||||||
|
return repo.list_page(offset, limit), repo.count()
|
||||||
|
|
||||||
|
|
||||||
def _get_session_or_raise(repo: IntelligentEvalSessionRepository, session_id: str) -> IntelligentEvalSession:
|
def _get_session_or_raise(repo: IntelligentEvalSessionRepository, session_id: str) -> IntelligentEvalSession:
|
||||||
obj = repo.get(session_id)
|
obj = repo.get(session_id)
|
||||||
if obj is None:
|
if obj is None:
|
||||||
|
|||||||
@ -101,6 +101,20 @@ class IntelligentEvalRepository:
|
|||||||
statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc())
|
statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc())
|
||||||
return [self._from_db(r) for r in self.session.exec(statement).all()]
|
return [self._from_db(r) for r in self.session.exec(statement).all()]
|
||||||
|
|
||||||
|
def list_page(self, offset: int, limit: int) -> list[IntelligentEval]:
|
||||||
|
"""Return one page of evals (created_at desc) with the same ordering as ``list_all``."""
|
||||||
|
statement = (
|
||||||
|
select(IntelligentEvalDB)
|
||||||
|
.order_by(IntelligentEvalDB.created_at.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
return [self._from_db(r) for r in self.session.exec(statement).all()]
|
||||||
|
|
||||||
|
def count(self) -> int:
|
||||||
|
"""Total number of evaluations (for pagination metadata)."""
|
||||||
|
return self.session.exec(select(func.count()).select_from(IntelligentEvalDB)).one()
|
||||||
|
|
||||||
def get(self, eval_id: str) -> Optional[IntelligentEval]:
|
def get(self, eval_id: str) -> Optional[IntelligentEval]:
|
||||||
db = self.session.get(IntelligentEvalDB, eval_id)
|
db = self.session.get(IntelligentEvalDB, eval_id)
|
||||||
return self._from_db(db) if db else None
|
return self._from_db(db) if db else None
|
||||||
|
|||||||
@ -94,10 +94,29 @@ async def create_eval(request: CreateEvalRequest, session: Session = Depends(get
|
|||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def list_evals(session: Session = Depends(get_db)) -> dict:
|
async def list_evals(
|
||||||
evals = lifecycle.list_evals(session)
|
page: int | None = None,
|
||||||
|
page_size: int = 20,
|
||||||
|
session: Session = Depends(get_db),
|
||||||
|
) -> dict:
|
||||||
|
"""List intelligent evaluations, optionally paginated.
|
||||||
|
|
||||||
|
不传 ``page`` 时返回全部(向后兼容);传 ``page``(从 1 起)时按
|
||||||
|
``created_at`` 倒序分页,返回 ``total`` 供前端服务端分页。
|
||||||
|
"""
|
||||||
reader = IntelligentEvalReadModel(session)
|
reader = IntelligentEvalReadModel(session)
|
||||||
|
if page is None:
|
||||||
|
evals = lifecycle.list_evals(session)
|
||||||
return {"intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)]}
|
return {"intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)]}
|
||||||
|
page_size = max(1, min(page_size, 100))
|
||||||
|
offset = (max(1, page) - 1) * page_size
|
||||||
|
evals, total = lifecycle.list_evals_page(session, offset, page_size)
|
||||||
|
return {
|
||||||
|
"intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)],
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tasks")
|
@router.get("/tasks")
|
||||||
|
|||||||
@ -785,7 +785,11 @@ export interface TaskQueueList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const intelligentEvalsApi = {
|
export const intelligentEvalsApi = {
|
||||||
list: () => api.get<{ intelligent_evals: IntelligentEval[] }>('/intelligent-evals'),
|
list: (params?: { page?: number; page_size?: number }) =>
|
||||||
|
api.get<{ intelligent_evals: IntelligentEval[]; total?: number; page?: number; page_size?: number }>(
|
||||||
|
'/intelligent-evals',
|
||||||
|
{ params },
|
||||||
|
),
|
||||||
listTasks: (params?: { status?: TaskQueueStatus; limit?: number }) =>
|
listTasks: (params?: { status?: TaskQueueStatus; limit?: number }) =>
|
||||||
api.get<TaskQueueList>('/intelligent-evals/tasks', { params }),
|
api.get<TaskQueueList>('/intelligent-evals/tasks', { params }),
|
||||||
get: (id: string) => api.get<IntelligentEval>(`/intelligent-evals/${id}`),
|
get: (id: string) => api.get<IntelligentEval>(`/intelligent-evals/${id}`),
|
||||||
|
|||||||
@ -38,10 +38,12 @@ export default function IntelligentEvalsPage() {
|
|||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
const [createOpen, setCreateOpen] = useState(false)
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
const [taskQueueOpen, setTaskQueueOpen] = useState(false)
|
const [taskQueueOpen, setTaskQueueOpen] = useState(false)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(20)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [form] = Form.useForm<CreateFormValues>()
|
const [form] = Form.useForm<CreateFormValues>()
|
||||||
|
|
||||||
const { list, detail, reloadList, reloadDetail } = useIntelligentEvalRead(selectedId)
|
const { list, detail, reloadList, reloadDetail } = useIntelligentEvalRead(selectedId, undefined, page, pageSize)
|
||||||
const evals = list.value
|
const evals = list.value
|
||||||
const loading = list.phase === 'loading'
|
const loading = list.phase === 'loading'
|
||||||
const { data: targets } = useResource(
|
const { data: targets } = useResource(
|
||||||
@ -173,7 +175,18 @@ export default function IntelligentEvalsPage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={evals ?? []}
|
dataSource={evals ?? []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={(evals?.length ?? 0) > 20 ? { pageSize: 20, showTotal: (t) => `共 ${t} 个` } : false}
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize,
|
||||||
|
total: list.total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: [10, 20, 50],
|
||||||
|
showTotal: (t) => `共 ${t} 个`,
|
||||||
|
onChange: (p, ps) => {
|
||||||
|
setPage(p)
|
||||||
|
setPageSize(ps)
|
||||||
|
},
|
||||||
|
}}
|
||||||
locale={{ emptyText: <Empty description="还没有智能评估" /> }}
|
locale={{ emptyText: <Empty description="还没有智能评估" /> }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,20 +12,25 @@ describe('intelligent evaluation read state', () => {
|
|||||||
const loading = intelligentEvalReadReducer(initialIntelligentEvalReadState, { type: 'list_requested' })
|
const loading = intelligentEvalReadReducer(initialIntelligentEvalReadState, { type: 'list_requested' })
|
||||||
expect(loading.list.phase).toBe('loading')
|
expect(loading.list.phase).toBe('loading')
|
||||||
|
|
||||||
const ready = intelligentEvalReadReducer(loading, { type: 'list_succeeded', value: [evaluation] })
|
const ready = intelligentEvalReadReducer(loading, {
|
||||||
expect(ready.list).toEqual({ phase: 'ready', value: [evaluation], error: null })
|
type: 'list_succeeded',
|
||||||
|
value: [evaluation],
|
||||||
|
total: 42,
|
||||||
|
})
|
||||||
|
expect(ready.list).toEqual({ phase: 'ready', value: [evaluation], error: null, total: 42 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the last list snapshot during silent refresh and failure', () => {
|
it('keeps the last list snapshot during silent refresh and failure', () => {
|
||||||
const ready: IntelligentEvalReadState = {
|
const ready: IntelligentEvalReadState = {
|
||||||
...initialIntelligentEvalReadState,
|
...initialIntelligentEvalReadState,
|
||||||
list: { phase: 'ready', value: [evaluation], error: null },
|
list: { phase: 'ready', value: [evaluation], error: null, total: 1 },
|
||||||
}
|
}
|
||||||
const refreshing = intelligentEvalReadReducer(ready, { type: 'list_requested', silent: true })
|
const refreshing = intelligentEvalReadReducer(ready, { type: 'list_requested', silent: true })
|
||||||
expect(refreshing.list.phase).toBe('refreshing')
|
expect(refreshing.list.phase).toBe('refreshing')
|
||||||
|
expect(refreshing.list.total).toBe(1)
|
||||||
|
|
||||||
const afterFailure = intelligentEvalReadReducer(refreshing, { type: 'list_failed', error: '网络错误' })
|
const afterFailure = intelligentEvalReadReducer(refreshing, { type: 'list_failed', error: '网络错误' })
|
||||||
expect(afterFailure.list).toEqual({ phase: 'ready', value: [evaluation], error: null })
|
expect(afterFailure.list).toEqual({ phase: 'ready', value: [evaluation], error: null, total: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('surfaces an initial detail failure without an existing snapshot', () => {
|
it('surfaces an initial detail failure without an existing snapshot', () => {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export interface ReadSlot<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IntelligentEvalReadState {
|
export interface IntelligentEvalReadState {
|
||||||
list: ReadSlot<IntelligentEval[]>
|
list: ReadSlot<IntelligentEval[]> & { total: number }
|
||||||
detail: ReadSlot<IntelligentEval | null> & {
|
detail: ReadSlot<IntelligentEval | null> & {
|
||||||
selectedId: string | null
|
selectedId: string | null
|
||||||
requestId: number
|
requestId: number
|
||||||
@ -18,7 +18,7 @@ export interface IntelligentEvalReadState {
|
|||||||
|
|
||||||
export type IntelligentEvalReadAction =
|
export type IntelligentEvalReadAction =
|
||||||
| { type: 'list_requested'; silent?: boolean }
|
| { type: 'list_requested'; silent?: boolean }
|
||||||
| { type: 'list_succeeded'; value: IntelligentEval[] }
|
| { type: 'list_succeeded'; value: IntelligentEval[]; total: number }
|
||||||
| { type: 'list_failed'; error: string }
|
| { type: 'list_failed'; error: string }
|
||||||
| { type: 'detail_cleared'; requestId: number }
|
| { type: 'detail_cleared'; requestId: number }
|
||||||
| { type: 'detail_requested'; id: string; requestId: number; silent?: boolean }
|
| { type: 'detail_requested'; id: string; requestId: number; silent?: boolean }
|
||||||
@ -26,21 +26,28 @@ export type IntelligentEvalReadAction =
|
|||||||
| { type: 'detail_failed'; id: string; requestId: number; error: string }
|
| { type: 'detail_failed'; id: string; requestId: number; error: string }
|
||||||
|
|
||||||
export interface IntelligentEvalReadAdapter {
|
export interface IntelligentEvalReadAdapter {
|
||||||
list: () => Promise<IntelligentEval[]>
|
list: (page?: number, pageSize?: number) => Promise<{ items: IntelligentEval[]; total: number }>
|
||||||
get: (id: string) => Promise<IntelligentEval>
|
get: (id: string) => Promise<IntelligentEval>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const intelligentEvalReadAdapter: IntelligentEvalReadAdapter = {
|
export const intelligentEvalReadAdapter: IntelligentEvalReadAdapter = {
|
||||||
list: () => intelligentEvalsApi.list().then((response) => response.data.intelligent_evals),
|
list: (page, pageSize) =>
|
||||||
|
intelligentEvalsApi
|
||||||
|
.list(page != null ? { page, page_size: pageSize ?? 20 } : undefined)
|
||||||
|
.then((response) => ({
|
||||||
|
items: response.data.intelligent_evals,
|
||||||
|
// 兼容不分页响应:无 total 时以本页长度为总数
|
||||||
|
total: response.data.total ?? response.data.intelligent_evals.length,
|
||||||
|
})),
|
||||||
get: (id) => intelligentEvalsApi.get(id).then((response) => response.data),
|
get: (id) => intelligentEvalsApi.get(id).then((response) => response.data),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialIntelligentEvalReadState: IntelligentEvalReadState = {
|
export const initialIntelligentEvalReadState: IntelligentEvalReadState = {
|
||||||
list: { phase: 'idle', value: [], error: null },
|
list: { phase: 'idle', value: [], error: null, total: 0 },
|
||||||
detail: { phase: 'idle', value: null, error: null, selectedId: null, requestId: 0 },
|
detail: { phase: 'idle', value: null, error: null, selectedId: null, requestId: 0 },
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestPhase<T>(slot: ReadSlot<T>, silent: boolean | undefined): ReadSlot<T> {
|
function requestPhase<S extends ReadSlot<unknown>>(slot: S, silent: boolean | undefined): S {
|
||||||
return { ...slot, phase: silent && slot.phase === 'ready' ? 'refreshing' : 'loading', error: null }
|
return { ...slot, phase: silent && slot.phase === 'ready' ? 'refreshing' : 'loading', error: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +59,7 @@ export function intelligentEvalReadReducer(
|
|||||||
case 'list_requested':
|
case 'list_requested':
|
||||||
return { ...state, list: requestPhase(state.list, action.silent) }
|
return { ...state, list: requestPhase(state.list, action.silent) }
|
||||||
case 'list_succeeded':
|
case 'list_succeeded':
|
||||||
return { ...state, list: { phase: 'ready', value: action.value, error: null } }
|
return { ...state, list: { phase: 'ready', value: action.value, error: null, total: action.total } }
|
||||||
case 'list_failed':
|
case 'list_failed':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@ -28,19 +28,20 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
it('refreshes active evaluations every five seconds and stops at terminal state', async () => {
|
it('refreshes active evaluations every five seconds and stops at terminal state', async () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
const adapter: IntelligentEvalReadAdapter = {
|
const adapter: IntelligentEvalReadAdapter = {
|
||||||
list: vi.fn().mockResolvedValue([evaluation]),
|
list: vi.fn().mockResolvedValue({ items: [evaluation], total: 1 }),
|
||||||
get: vi.fn().mockResolvedValue(evaluation),
|
get: vi.fn().mockResolvedValue(evaluation),
|
||||||
}
|
}
|
||||||
renderHook(() => useIntelligentEvalRead(null, adapter))
|
const { result } = renderHook(() => useIntelligentEvalRead(null, adapter))
|
||||||
await settle()
|
await settle()
|
||||||
expect(adapter.list).toHaveBeenCalledTimes(1)
|
expect(adapter.list).toHaveBeenCalledTimes(1)
|
||||||
|
expect(result.current.list.total).toBe(1)
|
||||||
|
|
||||||
await act(async () => { await vi.advanceTimersByTimeAsync(5000) })
|
await act(async () => { await vi.advanceTimersByTimeAsync(5000) })
|
||||||
await settle()
|
await settle()
|
||||||
expect(adapter.list).toHaveBeenCalledTimes(2)
|
expect(adapter.list).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
const terminal = { ...evaluation, status: 'completed' } as IntelligentEval
|
const terminal = { ...evaluation, status: 'completed' } as IntelligentEval
|
||||||
vi.mocked(adapter.list).mockResolvedValue([terminal])
|
vi.mocked(adapter.list).mockResolvedValue({ items: [terminal], total: 1 })
|
||||||
await act(async () => { await vi.advanceTimersByTimeAsync(5000) })
|
await act(async () => { await vi.advanceTimersByTimeAsync(5000) })
|
||||||
await settle()
|
await settle()
|
||||||
await act(async () => { await vi.advanceTimersByTimeAsync(10000) })
|
await act(async () => { await vi.advanceTimersByTimeAsync(10000) })
|
||||||
@ -51,7 +52,7 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
const adapter: IntelligentEvalReadAdapter = {
|
const adapter: IntelligentEvalReadAdapter = {
|
||||||
list: vi.fn()
|
list: vi.fn()
|
||||||
.mockResolvedValueOnce([evaluation])
|
.mockResolvedValueOnce({ items: [evaluation], total: 1 })
|
||||||
.mockRejectedValueOnce(new Error('网络错误')),
|
.mockRejectedValueOnce(new Error('网络错误')),
|
||||||
get: vi.fn().mockResolvedValue(evaluation),
|
get: vi.fn().mockResolvedValue(evaluation),
|
||||||
}
|
}
|
||||||
@ -65,12 +66,29 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
expect(result.current.list.error).toBeNull()
|
expect(result.current.list.error).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('passes page/pageSize to the adapter and reloads on change', async () => {
|
||||||
|
const adapter: IntelligentEvalReadAdapter = {
|
||||||
|
list: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
||||||
|
get: vi.fn().mockResolvedValue(evaluation),
|
||||||
|
}
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ page }) => useIntelligentEvalRead(null, adapter, page, 20),
|
||||||
|
{ initialProps: { page: 1 } },
|
||||||
|
)
|
||||||
|
await settle()
|
||||||
|
expect(adapter.list).toHaveBeenCalledWith(1, 20)
|
||||||
|
|
||||||
|
rerender({ page: 2 })
|
||||||
|
await settle()
|
||||||
|
expect(adapter.list).toHaveBeenCalledWith(2, 20)
|
||||||
|
})
|
||||||
|
|
||||||
it('clears the old detail and ignores a response for the previous selection', async () => {
|
it('clears the old detail and ignores a response for the previous selection', async () => {
|
||||||
const first = deferred<IntelligentEval>()
|
const first = deferred<IntelligentEval>()
|
||||||
const second = deferred<IntelligentEval>()
|
const second = deferred<IntelligentEval>()
|
||||||
const secondEvaluation = { ...evaluation, id: 'eval-2', name: '评估二' } as IntelligentEval
|
const secondEvaluation = { ...evaluation, id: 'eval-2', name: '评估二' } as IntelligentEval
|
||||||
const adapter: IntelligentEvalReadAdapter = {
|
const adapter: IntelligentEvalReadAdapter = {
|
||||||
list: vi.fn().mockResolvedValue([]),
|
list: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
||||||
get: vi.fn((id: string) => (id === 'eval-1' ? first.promise : second.promise)),
|
get: vi.fn((id: string) => (id === 'eval-1' ? first.promise : second.promise)),
|
||||||
}
|
}
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
@ -91,7 +109,7 @@ describe('useIntelligentEvalRead', () => {
|
|||||||
|
|
||||||
it('surfaces failure for a newly selected detail instead of retaining the old snapshot', async () => {
|
it('surfaces failure for a newly selected detail instead of retaining the old snapshot', async () => {
|
||||||
const adapter: IntelligentEvalReadAdapter = {
|
const adapter: IntelligentEvalReadAdapter = {
|
||||||
list: vi.fn().mockResolvedValue([]),
|
list: vi.fn().mockResolvedValue({ items: [], total: 0 }),
|
||||||
get: vi.fn()
|
get: vi.fn()
|
||||||
.mockResolvedValueOnce(evaluation)
|
.mockResolvedValueOnce(evaluation)
|
||||||
.mockRejectedValueOnce(new Error('不存在')),
|
.mockRejectedValueOnce(new Error('不存在')),
|
||||||
|
|||||||
@ -21,6 +21,8 @@ function isActive(status: string | undefined): boolean {
|
|||||||
export function useIntelligentEvalRead(
|
export function useIntelligentEvalRead(
|
||||||
selectedId: string | null,
|
selectedId: string | null,
|
||||||
adapter: IntelligentEvalReadAdapter = intelligentEvalReadAdapter,
|
adapter: IntelligentEvalReadAdapter = intelligentEvalReadAdapter,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20,
|
||||||
): IntelligentEvalReadState & { reloadList: () => Promise<void>; reloadDetail: () => Promise<void> } {
|
): IntelligentEvalReadState & { reloadList: () => Promise<void>; reloadDetail: () => Promise<void> } {
|
||||||
const [state, dispatch] = useReducer(intelligentEvalReadReducer, initialIntelligentEvalReadState)
|
const [state, dispatch] = useReducer(intelligentEvalReadReducer, initialIntelligentEvalReadState)
|
||||||
const detailRequestId = useRef(0)
|
const detailRequestId = useRef(0)
|
||||||
@ -28,12 +30,12 @@ export function useIntelligentEvalRead(
|
|||||||
const loadList = useCallback(async (silent = false) => {
|
const loadList = useCallback(async (silent = false) => {
|
||||||
dispatch({ type: 'list_requested', silent })
|
dispatch({ type: 'list_requested', silent })
|
||||||
try {
|
try {
|
||||||
const value = await adapter.list()
|
const { items, total } = await adapter.list(page, pageSize)
|
||||||
dispatch({ type: 'list_succeeded', value })
|
dispatch({ type: 'list_succeeded', value: items, total })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: 'list_failed', error: errorMessage(error) })
|
dispatch({ type: 'list_failed', error: errorMessage(error) })
|
||||||
}
|
}
|
||||||
}, [adapter])
|
}, [adapter, page, pageSize])
|
||||||
|
|
||||||
const loadDetail = useCallback(async (id: string, silent = false) => {
|
const loadDetail = useCallback(async (id: string, silent = false) => {
|
||||||
const requestId = ++detailRequestId.current
|
const requestId = ++detailRequestId.current
|
||||||
|
|||||||
@ -274,3 +274,63 @@ def test_list_tasks_not_shadowed_by_eval_id(client: TestClient):
|
|||||||
response = client.get("/api/intelligent-evals/tasks")
|
response = client.get("/api/intelligent-evals/tasks")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "tasks" in response.json()
|
assert "tasks" in response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_eval(db_session: Session, name: str, days_ago: int) -> IntelligentEvalDB:
|
||||||
|
from agenteval.storage.db import utc_now
|
||||||
|
|
||||||
|
ev = IntelligentEvalDB(
|
||||||
|
name=name,
|
||||||
|
target_id="target1",
|
||||||
|
status=IntelligentEvalStatus.COMPLETED.value,
|
||||||
|
created_at=utc_now() - timedelta(days=days_ago),
|
||||||
|
started_at=utc_now() - timedelta(days=days_ago),
|
||||||
|
)
|
||||||
|
db_session.add(ev)
|
||||||
|
return ev
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_evals_backward_compatible_no_page(client: TestClient, db_session: Session):
|
||||||
|
"""Without page param the endpoint returns everything (no total key)."""
|
||||||
|
_make_eval(db_session, "A", days_ago=3)
|
||||||
|
_make_eval(db_session, "B", days_ago=2)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
response = client.get("/api/intelligent-evals")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert len(data["intelligent_evals"]) == 2
|
||||||
|
assert "total" not in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_evals_pagination(client: TestClient, db_session: Session):
|
||||||
|
"""page/page_size returns one slice (newest first) plus total."""
|
||||||
|
_make_eval(db_session, "oldest", days_ago=3)
|
||||||
|
_make_eval(db_session, "middle", days_ago=2)
|
||||||
|
_make_eval(db_session, "newest", days_ago=1)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
response = client.get("/api/intelligent-evals?page=1&page_size=2")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["total"] == 3
|
||||||
|
assert data["page"] == 1
|
||||||
|
assert data["page_size"] == 2
|
||||||
|
names = [e["name"] for e in data["intelligent_evals"]]
|
||||||
|
assert names == ["newest", "middle"] # created_at desc
|
||||||
|
|
||||||
|
response = client.get("/api/intelligent-evals?page=2&page_size=2")
|
||||||
|
names = [e["name"] for e in response.json()["intelligent_evals"]]
|
||||||
|
assert names == ["oldest"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_evals_page_size_clamped(client: TestClient, db_session: Session):
|
||||||
|
"""page_size is clamped to [1, 100]."""
|
||||||
|
for i in range(5):
|
||||||
|
_make_eval(db_session, f"eval{i}", days_ago=10 - i)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
response = client.get("/api/intelligent-evals?page=1&page_size=0")
|
||||||
|
data = response.json()
|
||||||
|
assert data["page_size"] == 1
|
||||||
|
assert len(data["intelligent_evals"]) == 1
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user