feature: 增加功能 导出详细报告
This commit is contained in:
@@ -16,6 +16,7 @@ const mocks = vi.hoisted(() => ({
|
||||
retryErrors: vi.fn(),
|
||||
retryResult: vi.fn(),
|
||||
results: vi.fn(),
|
||||
exportRun: vi.fn(),
|
||||
reports: vi.fn(),
|
||||
report: vi.fn(),
|
||||
}))
|
||||
@@ -24,7 +25,7 @@ vi.mock('../../src/services', () => ({ services: {
|
||||
auth: { me: mocks.me, login: vi.fn(), logout: vi.fn(), changePassword: vi.fn(), listUsers: vi.fn().mockResolvedValue([]) },
|
||||
health: { get: mocks.health },
|
||||
providers: { list: mocks.providers },
|
||||
runs: { list: mocks.runs, get: mocks.run, resume: mocks.resume, retryErrors: mocks.retryErrors, retryResult: mocks.retryResult, results: mocks.results },
|
||||
runs: { list: mocks.runs, get: mocks.run, resume: mocks.resume, retryErrors: mocks.retryErrors, retryResult: mocks.retryResult, results: mocks.results, export: mocks.exportRun },
|
||||
reports: { list: mocks.reports, get: mocks.report },
|
||||
} }))
|
||||
|
||||
@@ -56,6 +57,7 @@ beforeEach(() => {
|
||||
mocks.retryErrors.mockResolvedValue({ run_id: 1, status: 'pending', selected_count: 100, retry_count: 5 })
|
||||
mocks.retryResult.mockResolvedValue({ run_id: 1, status: 'pending', selected_count: 1, execution_id: 'R0049' })
|
||||
mocks.results.mockResolvedValue([])
|
||||
mocks.exportRun.mockResolvedValue(new Blob(['# report'], { type: 'text/markdown' }))
|
||||
mocks.reports.mockResolvedValue([])
|
||||
mocks.report.mockResolvedValue({ run_id: 1, summary: { test_result: {}, admission: { decision: 'pass', coverage: {} } } })
|
||||
})
|
||||
@@ -64,6 +66,7 @@ afterEach(() => {
|
||||
cleanup()
|
||||
localStorage.clear()
|
||||
vi.clearAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('page mounting and route security', () => {
|
||||
@@ -122,6 +125,36 @@ describe('page mounting and route security', () => {
|
||||
expect(screen.getByText('逐条结果 (1 / 2)')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('exports terminal run results with the active filters', async () => {
|
||||
const createObjectURL = vi.fn(() => 'blob:report')
|
||||
const revokeObjectURL = vi.fn()
|
||||
class MockURL extends URL {}
|
||||
MockURL.createObjectURL = createObjectURL
|
||||
MockURL.revokeObjectURL = revokeObjectURL
|
||||
vi.stubGlobal('URL', MockURL)
|
||||
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined)
|
||||
mount('/runs/1')
|
||||
|
||||
const exportButton = await screen.findByRole('button', { name: '导出详细报告' })
|
||||
expect(exportButton.closest('.results-panel')).not.toBeNull()
|
||||
expect(document.querySelector('.page-actions')).not.toContainElement(exportButton)
|
||||
await userEvent.click(screen.getByRole('combobox', { name: '仲裁结论' }))
|
||||
await userEvent.click(await screen.findByText('不通过'))
|
||||
await userEvent.click(screen.getByRole('button', { name: '导出详细报告' }))
|
||||
|
||||
expect(mocks.exportRun).toHaveBeenCalledWith(1, { executionStatus: undefined, verdict: 'fail' })
|
||||
expect(createObjectURL).toHaveBeenCalledWith(expect.any(Blob))
|
||||
expect(click).toHaveBeenCalled()
|
||||
expect(click.mock.contexts[0]).toMatchObject({ download: 'run_1_results.md', href: 'blob:report' })
|
||||
expect(revokeObjectURL).toHaveBeenCalledWith('blob:report')
|
||||
}, 20_000)
|
||||
|
||||
it('disables report export while the run is not terminal', async () => {
|
||||
mocks.run.mockResolvedValueOnce({ ...run, status: 'running', terminal: false, phase: 'executing', progress_percent: 50, poll_after_seconds: 2 })
|
||||
mount('/runs/1')
|
||||
expect(await screen.findByRole('button', { name: '导出详细报告' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('searches run results by execution ID', async () => {
|
||||
mocks.results.mockResolvedValueOnce([
|
||||
{ execution_id: 'R0001', case_kind: 'risk', interaction_mode: 'single_turn', execution_status: 'completed', verdict: 'fail', model_input: {}, model_response: 'response', judge_result: {}, error_message: '' },
|
||||
|
||||
@@ -37,13 +37,13 @@ describe('service endpoint contracts', () => {
|
||||
await providers.create({ provider_id: 'target', base_url: 'https://example.com', chat_path: '/chat', models_path: '/models', model_name: 'm', auth_type: 'none', api_key: '', auth_header: 'Authorization', auth_prefix: 'Bearer', verify_ssl: true })
|
||||
await providers.update('target', { model_name: 'm2' }); await providers.check('target'); await providers.models('target'); await providers.remove('target')
|
||||
const idempotencyKey = '00000000-0000-4000-8000-000000000001'
|
||||
await runs.list(); await runs.get(1); await runs.start({ profile: 'smoke', auto_judge: true }, idempotencyKey); await runs.cancel(1); await runs.resume(1); await runs.retryErrors(1); await runs.retryResult(1, 'R0049'); await runs.results(1); await runs.remove(1)
|
||||
await runs.list(); await runs.get(1); await runs.start({ profile: 'smoke', auto_judge: true }, idempotencyKey); await runs.cancel(1); await runs.resume(1); await runs.retryErrors(1); await runs.retryResult(1, 'R0049'); await runs.results(1); await runs.export(1, { executionStatus: 'error', verdict: 'none' }); await runs.remove(1)
|
||||
await reports.list(); await reports.get(1)
|
||||
expect(vi.mocked(api.request).mock.calls.map(([path]) => path)).toEqual([
|
||||
'/health', '/providers', '/providers/target', '/providers', '/providers/target',
|
||||
'/providers/target/check', '/providers/target/models', '/providers/target',
|
||||
'/runs?limit=50', '/runs/1', '/runs', '/runs/1', '/runs/1/resume',
|
||||
'/runs/1/retry-errors', '/runs/1/results/R0049/retry', '/runs/1/results', '/runs/1', '/reports?limit=50', '/reports/1',
|
||||
'/runs/1/retry-errors', '/runs/1/results/R0049/retry', '/runs/1/results', '/runs/1/export?execution_status=error&verdict=none', '/runs/1', '/reports?limit=50', '/reports/1',
|
||||
])
|
||||
expect(vi.mocked(api.request).mock.calls[10][1]).toMatchObject({ headers: { 'Idempotency-Key': idempotencyKey } })
|
||||
})
|
||||
|
||||
@@ -17,6 +17,14 @@ describe('ApiClient', () => {
|
||||
).rejects.toMatchObject({ kind: 'validation' })
|
||||
})
|
||||
|
||||
it('returns file responses without parsing them as JSON', async () => {
|
||||
const blob = new Blob(['# report'], { type: 'text/markdown' })
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(blob))
|
||||
|
||||
await expect(new ApiClient('/api', () => 'token').request('/runs/1/export', { responseType: 'blob' })).resolves.toEqual(expect.any(Blob))
|
||||
expect(new Headers(fetchMock.mock.calls[0][1]?.headers).get('Accept')).toBe('text/markdown')
|
||||
})
|
||||
|
||||
it('refreshes and replays a business request once after 401', async () => {
|
||||
let token = 'old-token'
|
||||
const refresh = vi.fn(async (force?: boolean) => {
|
||||
|
||||
Reference in New Issue
Block a user