feat: 收口组件样式并调整移动端侧栏
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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<typeof fetch>()
|
||||
.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<HTMLInputElement>('[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<typeof fetch>()
|
||||
.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<typeof fetch>()
|
||||
.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<typeof fetch>().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<typeof fetch>().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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { getOperation, runOperation } from '@/features/workflows/operations'
|
||||
|
||||
describe('项目级长请求互斥', () => {
|
||||
it('同一项目不同时执行两个 graph,错误不伪装成功', async () => {
|
||||
let fail!: (reason: Error) => void
|
||||
const action = vi.fn<() => Promise<unknown>>(
|
||||
() =>
|
||||
new Promise((_resolve, reject) => {
|
||||
fail = reject
|
||||
})
|
||||
)
|
||||
const first = runOperation('operation-test', '拆解', action)
|
||||
expect(await runOperation('operation-test', '改写', action)).toBe(false)
|
||||
expect(action).toHaveBeenCalledTimes(1)
|
||||
fail(new Error('连接中断'))
|
||||
expect(await first).toBe(false)
|
||||
expect(getOperation('operation-test')).toMatchObject({ pending: false, error: '连接中断', notice: '' })
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { breakdownSnapshot, recoveryOptions, workflowCheckpoints } from '@/features/workflows/selectors'
|
||||
import type { Checkpoint } from '@/features/workflows/types'
|
||||
import type { BreakdownState } from '@/features/breakdown/types'
|
||||
|
||||
/** 只构造测试所需的真实 checkpoint 字段,避免页面依赖虚构 DTO。 */
|
||||
function checkpoint(index: number, state: BreakdownState, workflowName = 'breakdown'): Checkpoint {
|
||||
return { checkpointId: String(index), workflowName, createdAt: new Date(index * 1000).toISOString(), state }
|
||||
}
|
||||
|
||||
describe('Checkpoint 选择与恢复', () => {
|
||||
it('按 graph 隔离并保持输入不可变', () => {
|
||||
const records = [checkpoint(2, {}), checkpoint(1, {}, 'create-drama')]
|
||||
expect(workflowCheckpoints(records, 'breakdown').map(item => item.checkpointId)).toEqual(['2'])
|
||||
expect(records[0]?.checkpointId).toBe('2')
|
||||
})
|
||||
|
||||
it('只有错误的最新 checkpoint 仍能展示上一份成果,同时保留最新失败状态', () => {
|
||||
const result = breakdownSnapshot([
|
||||
checkpoint(1, {
|
||||
breakdownResult: { subjectCandidates: [] },
|
||||
runConfig: { groupSize: 3, modules: ['character'], episodeGroups: [] }
|
||||
}),
|
||||
checkpoint(2, {
|
||||
workflowExecution: {
|
||||
executionId: 'e',
|
||||
status: 'failed',
|
||||
startedAt: '',
|
||||
errorMessage: '模型未返回 JSON'
|
||||
}
|
||||
})
|
||||
])
|
||||
expect(result?.runConfig?.groupSize).toBe(3)
|
||||
expect(result?.workflowExecution?.status).toBe('failed')
|
||||
})
|
||||
|
||||
it('新阶段不能继承上一轮 completed 状态', () => {
|
||||
const result = breakdownSnapshot([
|
||||
checkpoint(1, {
|
||||
workflowExecution: { executionId: 'e', status: 'completed', startedAt: '' },
|
||||
breakdownResult: {}
|
||||
}),
|
||||
checkpoint(2, { runConfig: { groupSize: 2, modules: ['scene'], episodeGroups: [] } })
|
||||
])
|
||||
expect(result?.workflowExecution).toBeUndefined()
|
||||
expect(result?.runConfig?.groupSize).toBe(2)
|
||||
})
|
||||
|
||||
it('恢复窗口和后端一致,过旧的失败任务不启用重试', () => {
|
||||
const records = Array.from({ length: 11 }, (_, index) =>
|
||||
checkpoint(
|
||||
index,
|
||||
index === 0
|
||||
? {
|
||||
tasks: [
|
||||
{
|
||||
taskId: 't',
|
||||
module: 'prop',
|
||||
status: 'failed',
|
||||
attempt: 1,
|
||||
group: { groupId: 'g', groupNo: 1, startEpisodeNo: 1, endEpisodeNo: 1, episodes: [] }
|
||||
}
|
||||
]
|
||||
}
|
||||
: {}
|
||||
)
|
||||
)
|
||||
expect(recoveryOptions(records).retry).toBe(false)
|
||||
expect(recoveryOptions(records.slice(0, 10)).retry).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user