Files
ai-ticket-platform/frontend/src/features/tickets/TicketDetailPage.test.tsx
2026-07-27 19:00:35 +08:00

304 lines
9.3 KiB
TypeScript

import * as React from 'react'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { CustomerReplyPanel, SimilarTicketsPanel, SupportAnalysisPanel } from './TicketDetailPage'
import type {
CustomerReplyTask,
Ticket,
TicketSimilaritySuggestion,
TicketSupportAnalysis
} from './types'
afterEach(cleanup)
const suggestions: TicketSimilaritySuggestion[] = [
{
ticket_id: 42,
ticket_no: 'TK-20260727-0042',
title: '财务系统登录后持续白屏',
status: 'resolved',
category_code: 'technical_issue',
score: 0.86,
title_similarity: 0.9,
keyword_score: 0.82,
matched_excerpt: '登录财务系统后页面一直白屏,清理缓存无效。',
algorithm_version: 'fts_trigram_v1',
created_at: '2026-07-27T08:00:00Z'
}
]
describe('SimilarTicketsPanel', () => {
it('展示非阻断提示、候选分数和工单链接', () => {
render(<SimilarTicketsPanel items={suggestions} />)
expect(screen.getByText('相似工单提示')).toBeInTheDocument()
expect(screen.getByText('仅供参考,不会合并工单')).toBeInTheDocument()
expect(screen.getByText('86%')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /财务系统登录后持续白屏/ })).toHaveAttribute('href', '#/tickets/42')
})
it('没有候选时不占用详情页空间', () => {
const { container } = render(<SimilarTicketsPanel items={[]} />)
expect(container).toBeEmptyDOMElement()
})
})
const activeTicket = {
id: 7,
status: 'in_progress'
} as Ticket
const completedAnalysis: TicketSupportAnalysis = {
id: 9,
ticket_id: 7,
run_public_id: '31ca95a8-e8d5-40d4-9304-17a178d25422',
agent_version: 1,
status: 'succeeded',
progress_stage: 'completed',
progress_message: '草稿生成完成',
query: '订单接口 TIMEOUT-504',
retrieval_policy: {
policy_version: 'published_scope_hybrid_rerank_v2'
},
references: [
{
rank: 1,
source_type: 'solution',
source_id: 12,
source_version: '2026-07-27T08:00:00Z',
title: '订单连接池恢复方案',
excerpt: '订单接口出现 TIMEOUT-504 时检查连接池。',
score: 0.91,
applicable_scope: { related_system: '订单系统' },
auto_resolve_policy: {
allow_auto_resolve: true,
requires_human_action: false,
requires_external_confirmation: false,
requires_production_change: false
}
}
],
final_output: {
analysis_summary: '现象与连接池耗尽的已发布方案一致。',
troubleshooting_steps: ['检查连接池占用', '核对慢请求'],
candidate_solution_ids: [12],
customer_reply_draft: '我们正在检查订单服务连接池状态。',
citations: [1],
auto_resolve_recommended: false
},
automation_decision: {
policy_version: 'support_auto_resolve_v1',
status: 'manual_recommendation',
threshold: 0.85,
solution_id: 12,
score: 0.91,
checks: {
agent_recommended: false,
global_policy_enabled: true
},
reasons: ['agent_not_recommended'],
applied: false,
evaluated_at: '2026-07-27T08:00:05Z'
},
applied_solution_id: null,
processed_at: '2026-07-27T08:00:05Z',
error_code: '',
error_message: '',
created_at: '2026-07-27T08:00:00Z',
started_at: '2026-07-27T08:00:01Z',
finished_at: '2026-07-27T08:00:05Z'
}
describe('SupportAnalysisPanel', () => {
it('未运行时提供明确启动入口', () => {
const onStart = vi.fn()
render(
<SupportAnalysisPanel
ticket={activeTicket}
items={[]}
error=""
starting={false}
onStart={onStart}
/>
)
expect(screen.getByText('尚未运行支持分析')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '开始分析' }))
expect(onStart).toHaveBeenCalledOnce()
})
it('展示结构化建议、回复草稿和按排名冻结的引用', () => {
render(
<SupportAnalysisPanel
ticket={activeTicket}
items={[completedAnalysis]}
error=""
starting={false}
onStart={vi.fn()}
/>
)
expect(screen.getByText('现象与连接池耗尽的已发布方案一致。')).toBeInTheDocument()
expect(screen.getByText('检查连接池占用')).toBeInTheDocument()
expect(screen.getByText('我们正在检查订单服务连接池状态。')).toBeInTheDocument()
expect(screen.getByText('策略降级为人工处理')).toBeInTheDocument()
expect(screen.getByText(/Agent 未建议自动解决/)).toBeInTheDocument()
expect(screen.getByText('订单连接池恢复方案')).toBeInTheDocument()
expect(screen.getByText('91%')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /查看运行轨迹/ })).toHaveAttribute(
'href',
'#/agent-runs/31ca95a8-e8d5-40d4-9304-17a178d25422'
)
})
it('运行中禁止重复启动并说明不会修改工单', () => {
render(
<SupportAnalysisPanel
ticket={activeTicket}
items={[{ ...completedAnalysis, status: 'running', final_output: null }]}
error=""
starting={false}
onStart={vi.fn()}
/>
)
expect(screen.getByRole('button', { name: '分析进行中' })).toBeDisabled()
expect(screen.getByText(/不会修改工单状态或覆盖人工字段/)).toBeInTheDocument()
})
})
const replyTask: CustomerReplyTask = {
id: 15,
ticket_id: 7,
support_analysis_id: 9,
status: 'draft',
draft_text: '我们正在检查订单服务连接池状态。',
final_text: '我们正在检查订单服务连接池状态。',
revision: 1,
created_by_id: 3,
updated_by_id: null,
approved_by_id: null,
approved_at: null,
sent_by_id: null,
sent_at: null,
created_at: '2026-07-27T08:00:05Z',
updated_at: '2026-07-27T08:00:05Z',
revisions: [
{
version: 1,
content: '我们正在检查订单服务连接池状态。',
source: 'agent',
edited_by_id: null,
created_at: '2026-07-27T08:00:05Z'
}
]
}
describe('CustomerReplyPanel', () => {
it('允许人工修订并携带当前版本保存', () => {
const onSave = vi.fn().mockResolvedValue(undefined)
render(
<CustomerReplyPanel
tasks={[replyTask]}
canEdit
canConfirm
canSendChannel
onSave={onSave}
onConfirm={vi.fn().mockResolvedValue(undefined)}
onSendChannel={vi.fn()}
/>
)
fireEvent.change(screen.getByLabelText('最终回复内容'), {
target: { value: '连接池已恢复,请重新尝试。' }
})
fireEvent.click(screen.getByRole('button', { name: '保存修订' }))
expect(onSave).toHaveBeenCalledWith(replyTask, '连接池已恢复,请重新尝试。')
})
it('未修改时要求人工确认已发送,并展示已发送状态', () => {
const onConfirm = vi.fn().mockResolvedValue(undefined)
const { rerender } = render(
<CustomerReplyPanel
tasks={[replyTask]}
canEdit
canConfirm
canSendChannel
onSave={vi.fn().mockResolvedValue(undefined)}
onConfirm={onConfirm}
onSendChannel={vi.fn()}
/>
)
fireEvent.click(screen.getByRole('button', { name: '确认已发送' }))
expect(onConfirm).toHaveBeenCalledWith(replyTask, replyTask.final_text)
rerender(
<CustomerReplyPanel
tasks={[{
...replyTask,
status: 'sent',
sent_at: '2026-07-27T08:05:00Z'
}]}
canEdit
canConfirm
canSendChannel
onSave={vi.fn().mockResolvedValue(undefined)}
onConfirm={onConfirm}
onSendChannel={vi.fn()}
/>
)
expect(screen.getByText('客户回复已确认发送')).toBeInTheDocument()
})
it('只允许把未修改的当前版本交给渠道发送', () => {
const onSendChannel = vi.fn()
render(
<CustomerReplyPanel
tasks={[replyTask]}
canEdit
canConfirm
canSendChannel
onSave={vi.fn().mockResolvedValue(undefined)}
onConfirm={vi.fn().mockResolvedValue(undefined)}
onSendChannel={onSendChannel}
/>
)
fireEvent.click(screen.getByRole('button', { name: '通过渠道发送' }))
expect(onSendChannel).toHaveBeenCalledWith(replyTask)
fireEvent.change(screen.getByLabelText('最终回复内容'), {
target: { value: '尚未保存的新版本' }
})
expect(screen.getByRole('button', { name: '通过渠道发送' })).toBeDisabled()
expect(screen.getByText('请先保存修订,再通过渠道发送')).toBeInTheDocument()
})
it('渠道审批后冻结内容并等待送达回执', () => {
render(
<CustomerReplyPanel
tasks={[{
...replyTask,
status: 'approved',
approved_by_id: 3,
approved_at: '2026-07-27T08:04:00Z'
}]}
canEdit
canConfirm
canSendChannel
onSave={vi.fn().mockResolvedValue(undefined)}
onConfirm={vi.fn().mockResolvedValue(undefined)}
onSendChannel={vi.fn()}
/>
)
expect(screen.getByText('渠道投递中')).toBeInTheDocument()
expect(screen.getByText('回复版本已冻结,正在等待渠道送达')).toBeInTheDocument()
expect(screen.getByLabelText('最终回复内容')).toBeDisabled()
expect(screen.queryByRole('button', { name: '确认已发送' })).not.toBeInTheDocument()
})
})