304 lines
9.3 KiB
TypeScript
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()
|
|
})
|
|
})
|