feat: add intelligent support workflow
This commit is contained in:
303
frontend/src/features/tickets/TicketDetailPage.test.tsx
Normal file
303
frontend/src/features/tickets/TicketDetailPage.test.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user