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() 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() 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( ) expect(screen.getByText('尚未运行支持分析')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: '开始分析' })) expect(onStart).toHaveBeenCalledOnce() }) it('展示结构化建议、回复草稿和按排名冻结的引用', () => { render( ) 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( ) 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( ) 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( ) fireEvent.click(screen.getByRole('button', { name: '确认已发送' })) expect(onConfirm).toHaveBeenCalledWith(replyTask, replyTask.final_text) rerender( ) expect(screen.getByText('客户回复已确认发送')).toBeInTheDocument() }) it('只允许把未修改的当前版本交给渠道发送', () => { const onSendChannel = vi.fn() render( ) 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( ) expect(screen.getByText('渠道投递中')).toBeInTheDocument() expect(screen.getByText('回复版本已冻结,正在等待渠道送达')).toBeInTheDocument() expect(screen.getByLabelText('最终回复内容')).toBeDisabled() expect(screen.queryByRole('button', { name: '确认已发送' })).not.toBeInTheDocument() }) })