import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { afterEach, describe, expect, it, vi } from 'vitest' import WorkflowDiagnosticsDialog from '@/features/workflows/WorkflowDiagnosticsDialog.vue' import { loadWorkflowDiagnostics, type WorkflowTimelineGroup } from '@/features/workflows/diagnostics' import type { Checkpoint } from '@/features/workflows/types' let wrapper: VueWrapper | undefined afterEach(() => { wrapper?.unmount() wrapper = undefined document.body.innerHTML = '' vi.unstubAllGlobals() }) /** 后端将 checkpoint 固定标为 completed,测试确保界面不把它当执行成功。 */ const groups: WorkflowTimelineGroup[] = [ { phase: '其它', nodeCount: 22, durationMs: 22000, durationText: '22s', nodes: Array.from({ length: 22 }, (_, index) => ({ index: index + 1, checkpointId: `checkpoint-${index}`, nodeName: `node-${index}`, phase: '其它', status: 'completed', durationMs: 1000, durationText: '1s', retryCount: 0, createdAt: '2026-09-01T00:00:00Z' })) } ] const metrics = { projectId: 'diagnostics-test', nodeCount: 22, totalDurationText: '22s', retryCount: 1, successRate: 100, failedNodeCount: 0 } describe('运行观测入口', () => { it('关闭时不请求,打开只读两个接口,完整展示超过 18 条记录且不伪造成功率', async () => { const fetcher = vi .fn() .mockImplementation( async url => new Response(JSON.stringify({ data: String(url).endsWith('/metrics') ? metrics : groups })) ) vi.stubGlobal('fetch', fetcher) const checkpoints: Checkpoint[] = [ { checkpointId: 'checkpoint-0', workflowName: 'breakdown', createdAt: '2026-09-01T00:00:00Z', state: { workflowExecution: { status: 'failed', executionId: 'execution-1', startedAt: '2026-09-01T00:00:00Z' } } } ] wrapper = mount(WorkflowDiagnosticsDialog, { attachTo: document.body, props: { projectId: 'diagnostics-test', open: false, checkpoints } }) await flushPromises() expect(fetcher).not.toHaveBeenCalled() await wrapper.setProps({ open: true }) await flushPromises() expect(fetcher).toHaveBeenCalledTimes(2) expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true) expect(document.querySelectorAll('.diagnostics-records article')).toHaveLength(22) expect(document.body.textContent).toContain('包含失败记录') expect(document.body.textContent).not.toContain('100%') const input = document.querySelector('[aria-label="搜索运行记录"]')! input.value = 'checkpoint-21' input.dispatchEvent(new Event('input', { bubbles: true })) await flushPromises() expect(document.querySelectorAll('.diagnostics-records article')).toHaveLength(1) }) it('一个观测接口失败仍显示另一项,不启动恢复或生产', async () => { const fetcher = vi .fn() .mockImplementation(async url => String(url).endsWith('/metrics') ? new Response(JSON.stringify({ error: '指标不可用' }), { status: 500 }) : new Response(JSON.stringify({ data: groups })) ) vi.stubGlobal('fetch', fetcher) const result = await loadWorkflowDiagnostics('project/1') expect(result.metrics).toBeNull() expect(result.groups?.[0]?.nodes).toHaveLength(22) expect(result.errors[0]).toContain('指标') expect(fetcher.mock.calls[0]?.[0]).toBe('/api/projects/project%2F1/metrics') expect(fetcher.mock.calls[1]?.[0]).toBe('/api/projects/project%2F1/timeline/grouped') expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true) }) it('指标项目不匹配时拒绝展示', async () => { vi.stubGlobal( 'fetch', vi .fn() .mockImplementation( async url => new Response(JSON.stringify({ data: String(url).endsWith('/metrics') ? metrics : [] })) ) ) const result = await loadWorkflowDiagnostics('other-project') expect(result.metrics).toBeNull() expect(result.errors.join()).toContain('不匹配的项目') }) it('切项目会取消旧查询,晚到响应不会写进新项目', async () => { const pending: ((response: Response) => void)[] = [] const fetcher = vi.fn().mockImplementation(url => String(url).includes('/old/') ? new Promise(resolve => pending.push(resolve)) : Promise.resolve( new Response( JSON.stringify({ data: String(url).endsWith('/metrics') ? { ...metrics, projectId: 'new', nodeCount: 0 } : [] }) ) ) ) vi.stubGlobal('fetch', fetcher) wrapper = mount(WorkflowDiagnosticsDialog, { attachTo: document.body, props: { projectId: 'old', open: true, checkpoints: [] } }) await flushPromises() const signal = fetcher.mock.calls[0]?.[1]?.signal await wrapper.setProps({ projectId: 'new' }) await flushPromises() expect(signal?.aborted).toBe(true) pending[0]!(new Response(JSON.stringify({ data: { ...metrics, projectId: 'old' } }))) pending[1]!(new Response(JSON.stringify({ data: groups }))) await flushPromises() expect(document.querySelectorAll('.diagnostics-records article')).toHaveLength(0) expect(document.body.textContent).toContain('全项目记录 0') await wrapper.setProps({ open: false }) await flushPromises() expect(fetcher).toHaveBeenCalledTimes(4) }) it('关闭弹窗时取消仍未返回的查询', async () => { const finishes: ((response: Response) => void)[] = [] const fetcher = vi.fn().mockImplementation(() => new Promise(resolve => finishes.push(resolve))) vi.stubGlobal('fetch', fetcher) wrapper = mount(WorkflowDiagnosticsDialog, { attachTo: document.body, props: { projectId: 'diagnostics-test', open: true, checkpoints: [] } }) await flushPromises() await wrapper.setProps({ open: false }) await flushPromises() expect(fetcher.mock.calls.every(([, init]) => init?.signal?.aborted)).toBe(true) finishes[0]!(new Response(JSON.stringify({ data: metrics }))) finishes[1]!(new Response(JSON.stringify({ data: groups }))) await flushPromises() expect(fetcher).toHaveBeenCalledTimes(2) }) })