import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter, useLocation } from 'react-router-dom'; import { afterEach, expect, test, vi } from 'vitest'; import OperationsPage from './OperationsPage'; const mocks = vi.hoisted(() => ({ opsHealth: vi.fn(), sourceReadiness: vi.fn(), session: vi.fn(), vehicleCoverage: vi.fn(), vehicleSourceDiagnostic: vi.fn(), updateVehicleSourcePolicy: vi.fn(), reconciliationSummary: vi.fn(), reconciliationIssues: vi.fn(), reconciliationIssue: vi.fn(), updateReconciliationIssue: vi.fn() })); const layout = vi.hoisted(() => ({ mobile: false })); vi.mock('../../api/client', () => ({ api: mocks })); vi.mock('../hooks/useMobileLayout', () => ({ useMobileLayout: () => layout.mobile })); afterEach(() => { cleanup(); layout.mobile = false; Object.values(mocks).forEach((mock) => mock.mockReset()); }); function LocationProbe() { const location = useLocation(); return {location.pathname}{location.search}; } function renderOperations(client: QueryClient, initialEntries = ['/operations']) { return render(); } function seedSession() { mocks.session.mockResolvedValue({ name: '平台管理员', role: 'admin', userType: 'admin', authMode: 'enforce', menuKeys: ['operations'] }); mocks.reconciliationSummary.mockResolvedValue({ active: 1, pending: 1, confirmed: 0, recovered: 2, overSla: 1, byRule: [{ name: 'POSITION_DRIFT', count: 1 }], bySeverity: [{ name: 'major', count: 1 }], trend: [{ date: '2026-07-16', detected: 1, new: 1, active: 1, recovered: 0 }], lastRunAt: '2026-07-16 02:15:00', asOf: '2026-07-16 10:00:00' }); mocks.reconciliationIssues.mockResolvedValue({ items: [{ id: 'issue-1', ruleCode: 'POSITION_DRIFT', category: 'location', severity: 'major', status: 'pending', vin: 'VIN001', plate: '粤A00001', protocolA: 'GB32960', protocolB: 'JT808', title: '多来源实时位置漂移', summary: '两个来源相差 1286 米', evidence: { distanceM: 1286 }, firstSeenAt: '2026-07-16 08:00:00', lastSeenAt: '2026-07-16 10:00:00', occurrenceCount: 3, recoveredAt: '', resolutionNote: '', resolvedBy: '', version: 1 }], total: 1, limit: 50, offset: 0 }); mocks.reconciliationIssue.mockResolvedValue({ id: 'issue-1', ruleCode: 'POSITION_DRIFT', category: 'location', severity: 'major', status: 'pending', vin: 'VIN001', plate: '粤A00001', protocolA: 'GB32960', protocolB: 'JT808', title: '多来源实时位置漂移', summary: '两个来源相差 1286 米', evidence: { distanceM: 1286 }, firstSeenAt: '2026-07-16 08:00:00', lastSeenAt: '2026-07-16 10:00:00', occurrenceCount: 3, recoveredAt: '', resolutionNote: '', resolvedBy: '', version: 1, actions: [{ id: 1, action: 'detect', fromStatus: '', toStatus: 'pending', actor: 'reconciliation-evaluator', note: '规则首次发现差异', createdAt: '2026-07-16 08:00:00' }] }); } test('renders reconciliation queue, loads evidence on demand and records review conclusion', async () => { seedSession(); mocks.opsHealth.mockResolvedValue({ linkHealth: [{ name: 'MySQL realtime', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5, tdengineWritable: true, mysqlWritable: true, runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } }); mocks.sourceReadiness.mockResolvedValue({ totalVehicles: 1, boundVehicles: 1, identityRequiredVehicles: 0, onlineVehicles: 1, sources: [] }); mocks.updateReconciliationIssue.mockResolvedValue({ ...(await mocks.reconciliationIssue()), status: 'confirmed_source_a', resolutionNote: '来源 A 原始报文可信', resolvedBy: '平台管理员', version: 2 }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); renderOperations(client); expect(await screen.findByText('质量问题队列')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '运行与数据质量', level: 5 })).toBeInTheDocument(); expect(screen.getByLabelText('运维质量操作')).toHaveClass('v2-workspace-command-bar', 'v2-ops-command-bar'); expect(screen.getByRole('button', { name: '刷新全局证据' })).toHaveClass('v2-workspace-mobile-tool-button', 'is-muted'); const navigation = screen.getByRole('navigation', { name: '运维质量视图' }); expect(navigation).toHaveClass('v2-ops-navigation'); expect(within(navigation).getByText('每日自动对账')).toBeInTheDocument(); expect(await within(navigation).findByText('1 项待复核')).toBeInTheDocument(); expect(screen.getByRole('tab', { name: '差异处置 1', selected: true })).toHaveClass('semi-button'); expect(screen.getByRole('tabpanel', { name: '差异处置' })).toHaveAttribute('tabindex', '0'); expect(document.querySelector('.v2-ops-page')).toHaveClass('is-reconciliation'); expect(screen.queryByText('先选择一辆车')).not.toBeInTheDocument(); expect(screen.getByText('质量问题队列').closest('.semi-card')).toHaveClass('v2-reconcile-center'); expect(screen.getByText('质量问题队列').closest('.v2-workspace-panel-header')).toHaveClass('v2-reconcile-heading'); const reconciliationMetrics = screen.getByRole('list', { name: '差异处置队列概览' }); expect(reconciliationMetrics.closest('.semi-card')).toHaveClass('v2-workspace-metric-rail', 'v2-reconcile-metric-rail'); expect(within(reconciliationMetrics).getAllByRole('listitem')).toHaveLength(4); expect(within(reconciliationMetrics).getByText('待复核').closest('[role="listitem"]')).toHaveClass('is-primary'); expect(within(reconciliationMetrics).getByText('SLA 超时').closest('[role="listitem"]')).toHaveClass('is-danger'); expect(within(reconciliationMetrics).getByText('已自动恢复').closest('[role="listitem"]')).toHaveClass('is-secondary', 'is-success'); expect(screen.getByText('当前筛选').closest('.v2-workspace-metric-context')).toHaveTextContent('1'); expect(document.querySelectorAll('.v2-reconcile-kpi-card')).toHaveLength(0); expect(screen.queryByText('活跃问题', { exact: true })).not.toBeInTheDocument(); expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument(); const trendButton = screen.getByRole('button', { name: '30 天趋势' }); expect(trendButton).toHaveClass('v2-workspace-mobile-tool-button', 'is-muted'); expect(screen.getByRole('button', { name: '刷新结果' })).toHaveClass('v2-workspace-mobile-tool-button', 'is-muted'); expect(trendButton).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(trendButton); const trendDialog = await screen.findByRole('dialog', { name: '30 天差异趋势' }); expect(trendDialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet', 'v2-reconcile-trend-sidesheet'); expect(within(trendDialog).getByText('趋势明细').closest('.semi-card')).toHaveClass('v2-reconcile-trend'); expect(within(trendDialog).getByText('趋势明细').closest('.v2-workspace-panel-header')).toHaveClass('is-compact'); expect(within(trendDialog).getByRole('list', { name: '30 天差异趋势摘要' })).toBeInTheDocument(); expect(trendButton).toHaveAttribute('aria-expanded', 'true'); fireEvent.click(within(trendDialog).getByRole('button', { name: '关闭 30 天差异趋势' })); expect(trendButton).toHaveAttribute('aria-expanded', 'false'); expect(document.querySelector('.v2-reconcile-table.semi-table-wrapper')).toBeInTheDocument(); expect(screen.getByLabelText('运维质量操作')).toHaveTextContent('上海时间'); expect(screen.getAllByText('2026-07-16 18:00:00').length).toBeGreaterThan(0); expect(document.querySelector('.v2-platform-time.v2-ops-time')).toBeInTheDocument(); expect((await screen.findAllByText('多来源实时位置漂移')).length).toBeGreaterThan(0); expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).not.toBeInTheDocument(); expect(document.querySelector('.v2-reconcile-table-wrap > table')).not.toBeInTheDocument(); const reconcileRow = screen.getByTestId('reconcile-row-issue-1'); expect(reconcileRow).toHaveAttribute('role', 'button'); expect(reconcileRow).toHaveAttribute('aria-expanded', 'false'); fireEvent.keyDown(reconcileRow, { key: ' ' }); expect(await screen.findByText('规则证据')).toBeInTheDocument(); expect(reconcileRow).toHaveAttribute('aria-expanded', 'true'); const detailDialog = screen.getByRole('dialog', { name: '差异证据与处置' }); expect(detailDialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet', 'v2-reconcile-detail-sidesheet'); expect(within(detailDialog).getByRole('list', { name: '差异证据与处置摘要' })).toBeInTheDocument(); expect(screen.getByText('规则证据').closest('.semi-card')).toHaveClass('v2-reconcile-evidence'); expect(screen.getByText('复核结论').closest('.semi-card')).toHaveClass('v2-reconcile-review'); expect(screen.getByText('处理履历').closest('.semi-card')).toHaveClass('v2-reconcile-actions'); expect(screen.getByText('规则证据').closest('.v2-workspace-panel-header')).toHaveClass('is-compact'); expect(document.querySelector('.v2-reconcile-detail-heading.v2-workspace-panel-header')).not.toBeInTheDocument(); expect(screen.getByText('复核提示')).toBeInTheDocument(); expect(screen.getByText('坐标差距')).toBeInTheDocument(); expect(screen.getByText('distanceM')).toBeInTheDocument(); expect(screen.getByText('1,286 m')).toBeInTheDocument(); fireEvent.click(within(detailDialog).getByRole('button', { name: '关闭差异证据与处置' })); expect(reconcileRow).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(reconcileRow); expect(await screen.findByText('规则证据')).toBeInTheDocument(); fireEvent.click(screen.getByRole('radio', { name: '采信 GB32960' })); fireEvent.change(screen.getByLabelText('说明(必填)'), { target: { value: '来源 A 原始报文可信' } }); fireEvent.click(screen.getByRole('button', { name: '保存结论并记录履历' })); await waitFor(() => expect(mocks.updateReconciliationIssue).toHaveBeenCalledWith('issue-1', { version: 1, status: 'confirmed_source_a', note: '来源 A 原始报文可信' })); }); test('renders only the compact mobile reconciliation surface and defers secondary controls', async () => { layout.mobile = true; seedSession(); mocks.opsHealth.mockResolvedValue({ linkHealth: [], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5, tdengineWritable: true, mysqlWritable: true, runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } }); mocks.sourceReadiness.mockResolvedValue({ totalVehicles: 1, boundVehicles: 1, identityRequiredVehicles: 0, onlineVehicles: 1, sources: [] }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); renderOperations(client); expect(await screen.findByText('多来源实时位置漂移')).toBeInTheDocument(); expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).toBeInTheDocument(); expect(screen.getByRole('region', { name: '差异队列,可上下滚动' })).toHaveAttribute('tabindex', '0'); expect(document.querySelector('.v2-reconcile-table.semi-table-wrapper')).not.toBeInTheDocument(); expect(mocks.reconciliationIssues).toHaveBeenCalledWith(expect.objectContaining({ limit: 20 }), expect.any(AbortSignal)); const mobileIssueAction = screen.getByRole('button', { name: '查看 粤A00001 差异证据' }); expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(mobileIssueAction); expect(await screen.findByRole('dialog', { name: '差异证据与处置' })).toBeInTheDocument(); expect(document.querySelector('.v2-reconcile-detail-sidesheet .v2-reconcile-detail.is-sheet')).toBeInTheDocument(); expect(document.querySelector('.v2-reconcile-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom'); expect(document.querySelector('.v2-reconcile-detail-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet'); expect(document.querySelector('.v2-reconcile-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(88dvh, 780px)' }); expect(await screen.findByRole('button', { name: '关闭差异证据与处置' })).toBeInTheDocument(); expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'true'); fireEvent.click(screen.getByRole('button', { name: '关闭差异证据与处置' })); expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'false'); const toolbar = document.querySelector('.v2-reconcile-toolbar'); expect(toolbar).toHaveClass('is-mobile-collapsed'); expect(screen.getByRole('button', { name: /修改筛选差异:活跃差异 · 1 条/ })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /修改筛选差异/ })); expect(toolbar).not.toHaveClass('is-mobile-collapsed'); expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '30 天趋势' })); expect(await screen.findByRole('dialog', { name: '30 天差异趋势' })).toBeInTheDocument(); expect(document.querySelector('.v2-reconcile-trend-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet'); expect(document.querySelector('.v2-reconcile-trend-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(72dvh, 620px)' }); const trend = screen.getByText('趋势明细').closest('.v2-reconcile-trend'); expect(trend).toBeInTheDocument(); expect(screen.getByText(/最近运行/)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '关闭 30 天差异趋势' })); expect(screen.queryByText('趋势明细')).not.toBeInTheDocument(); }); test('reconciles service identities with bound and identity-required vehicles', async () => { seedSession(); mocks.opsHealth.mockResolvedValue({ linkHealth: [{ name: 'MySQL realtime', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5, tdengineWritable: true, mysqlWritable: true, runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } }); mocks.sourceReadiness.mockResolvedValue({ totalVehicles: 1024, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234, kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: [{ protocol: 'GB32960', role: '仪表盘里程与整车状态', total: 1024, online: 234, severity: 'ok', evidence: '实时来源覆盖稳定', action: '保持在线率监测', acceptance: '在线率与上报周期持续满足阈值' }] }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); renderOperations(client, ['/operations?view=health']); expect(screen.getByRole('tab', { name: '全局健康', selected: true })).toBeInTheDocument(); expect(screen.getByTestId('operations-location')).toHaveTextContent('/operations?view=health'); expect(await screen.findByText('1024 / 234')).toBeInTheDocument(); expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('15 秒刷新运行证据')).toBeInTheDocument(); expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('运行正常')).toBeInTheDocument(); for (const key of [['ops-health-v2'], ['ops-source-readiness-v2']]) { const query = client.getQueryCache().find({ queryKey: key }); const liveOptions = query?.options as { refetchIntervalInBackground?: boolean; refetchOnWindowFocus?: boolean }; expect(liveOptions.refetchIntervalInBackground).toBe(false); expect(liveOptions.refetchOnWindowFocus).toBe(true); } expect(screen.getByText('车辆总数 / 在线')).toBeInTheDocument(); expect(screen.getByText('协议就绪 / 总数')).toBeInTheDocument(); expect(screen.getByText('全部协议达到当前口径')).toBeInTheDocument(); expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '运行概览', level: 5 })).toBeInTheDocument(); expect(screen.getByText('车辆总数 / 在线').closest('.v2-ops-metric')).toBeInTheDocument(); expect(screen.getByRole('list', { name: '运行健康指标' })).toHaveClass('v2-ops-metric-rail'); expect(screen.getByText('全部正常')).toBeInTheDocument(); expect(screen.getByText('运行时安全').closest('.semi-card')).toHaveClass('v2-ops-runtime'); expect(screen.getByText('协议来源就绪度').closest('.semi-card')).toHaveClass('v2-ops-sources'); expect(screen.getByRole('heading', { name: '数据链路', level: 5 })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '运行时安全', level: 5 })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '协议来源就绪度', level: 5 })).toBeInTheDocument(); expect(document.querySelector('.v2-ops-link-card')).toHaveTextContent('MySQL 实时连接MySQL realtime主库连接正常'); expect(document.querySelector('.v2-ops-link-card.semi-card')).not.toBeInTheDocument(); expect(document.querySelector('.v2-ops-runtime-descriptions.semi-descriptions')).toBeInTheDocument(); expect(screen.getByText('容量检查通过').closest('.semi-empty')).toHaveClass('v2-ops-capacity-empty'); expect(document.querySelector('.v2-ops-source-list.semi-card-group')).toBeInTheDocument(); expect(screen.getByText('GB32960').closest('.semi-card')).toHaveClass('v2-ops-source-card'); expect(screen.getByLabelText('GB32960 在线覆盖率')).toBeInTheDocument(); expect(screen.getByText('22.9% 在线')).toBeInTheDocument(); expect(screen.getByText('验收标准').closest('.semi-card')).toHaveClass('v2-ops-source-card'); expect(screen.queryByText('单车多来源诊断')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: '单车诊断' })); expect(screen.getByTestId('operations-location')).toHaveTextContent('/operations?view=diagnostic'); expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('按需读取单车来源')).toBeInTheDocument(); expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('精确诊断')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '诊断车辆', level: 5 })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '诊断车辆', level: 5 }).closest('.semi-card')).toHaveClass('v2-source-filter-panel', 'v2-workspace-filter-panel'); expect(screen.getByText('先选择一辆车').closest('.semi-empty')).toHaveClass('v2-source-empty'); expect(screen.getByText('单车多来源诊断').closest('.semi-card')).toHaveClass('v2-source-diagnostic'); expect(screen.queryByText('统一车辆视角')).not.toBeInTheDocument(); }); test('includes protocol readiness in the global health verdict and prioritizes its evidence', async () => { seedSession(); mocks.opsHealth.mockResolvedValue({ linkHealth: [{ name: 'MySQL realtime', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5, tdengineWritable: true, mysqlWritable: true, runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } }); mocks.sourceReadiness.mockResolvedValue({ totalVehicles: 1024, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234, sources: [ { protocol: 'GB32960', role: '仪表盘里程与整车状态', total: 494, online: 123, severity: 'warning', status: '覆盖不足', evidence: '在线覆盖偏低', action: '核对接入范围', acceptance: '缺失车辆下降' }, { protocol: 'YUTONG_MQTT', role: '宇通补充来源', total: 50, online: 12, severity: 'error', status: '链路异常', evidence: '投影停滞', action: '恢复投影与回补', acceptance: '日里程恢复' } ] }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); renderOperations(client, ['/operations?view=health']); expect(await screen.findByText('需要处理')).toBeInTheDocument(); expect(screen.getByText('协议覆盖 2 项 · 链路运行 0 项')).toBeInTheDocument(); expect(screen.getByText('0 / 2')).toBeInTheDocument(); expect(screen.getByText('2 个协议需要处置')).toBeInTheDocument(); expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('2 项关注')).toBeInTheDocument(); const sourcePanel = screen.getByRole('heading', { name: '协议来源就绪度', level: 5 }).closest('.v2-ops-sources'); const infrastructureGrid = document.querySelector('.v2-ops-grid'); expect(sourcePanel).toHaveClass('has-attention'); expect(sourcePanel).not.toBeNull(); expect(infrastructureGrid).not.toBeNull(); expect(Boolean(sourcePanel!.compareDocumentPosition(infrastructureGrid!) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true); }); test('keeps health evidence visible when source readiness fails and retries that section', async () => { seedSession(); mocks.opsHealth.mockResolvedValue({ linkHealth: [], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5, tdengineWritable: true, mysqlWritable: true, runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } }); mocks.sourceReadiness.mockRejectedValueOnce(new Error('来源就绪度暂时不可用')).mockResolvedValueOnce({ totalVehicles: 1024, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234, kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: [] }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); renderOperations(client); fireEvent.click(screen.getByRole('tab', { name: '全局健康' })); expect(await screen.findByText('来源就绪度暂时不可用')).toBeInTheDocument(); expect(screen.getByText('协议来源证据不可用')).toBeInTheDocument(); expect(screen.getByText('证据缺失')).toBeInTheDocument(); expect(screen.getByLabelText('运维质量操作')).toHaveTextContent('上海时间 · 版本 test-release'); expect(screen.getByRole('heading', { name: '数据链路', level: 5 })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /重试/ })); expect(await screen.findByText('1024 / 234')).toBeInTheDocument(); expect(screen.queryByText('来源就绪度暂时不可用')).not.toBeInTheDocument(); expect(mocks.sourceReadiness).toHaveBeenCalledTimes(2); }); test('fuzzy searches a vehicle and renders all source diagnosis evidence', async () => { seedSession(); mocks.opsHealth.mockResolvedValue({ linkHealth: [], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5, tdengineWritable: true, mysqlWritable: true, runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } }); mocks.sourceReadiness.mockResolvedValue({ totalVehicles: 1, boundVehicles: 1, identityRequiredVehicles: 0, onlineVehicles: 1, sources: [] }); mocks.vehicleCoverage.mockResolvedValue({ items: [{ vin: 'VIN001', plate: '粤A00001', protocols: ['JT808'], missingProtocols: [], sourceStatus: [], sourceCount: 2, onlineSourceCount: 2, online: true, lastSeen: '', bindingStatus: 'bound' }], total: 1, limit: 20, offset: 0 }); const diagnostic = { evidence: { vin: 'VIN001', plate: '粤A00001', mileageDate: '2026-07-16', recommendedLocationProtocol: 'JT808', recommendedLocationLabel: 'G7', locationConflict: true, conflictDistanceM: 233, locationSources: [{ protocol: 'JT808', sourceLabel: 'G7', providerOverride: '', terminalLabel: '终端 133****0001', sourceKind: 'PLATFORM', sourceRef: 'a'.repeat(64), selectedWithinProtocol: true, recommended: true, enabled: true, priority: 20, policyRemark: '保持当前优先级', online: true, qualityStatus: 'OK', qualityReason: '', longitude: 113.1, latitude: 23.1, speedKmh: 30, totalMileageKm: 1000, eventTime: '2026-07-16 10:00:00', receivedAt: '2026-07-16 10:00:01', firstSeenAt: '2026-03-01 08:00:00', reportIntervalSec: 10, reportSampleCount: 1000, selectionReason: '当前融合推荐' }], mileageSources: [], comparison: { locationMaxDistanceM: 233, totalMileageDeltaKm: 0, dailyMileageDeltaKm: 0, reportTimeDeltaSeconds: 0 }, asOf: '' }, policy: { vin: 'VIN001', version: 1, updatedBy: 'system', updatedAt: '', audit: [] }, recommendationReason: '当前推荐 G7', refreshHint: '下一次有效上报后重新选举' }; mocks.vehicleSourceDiagnostic.mockResolvedValue(diagnostic); mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); renderOperations(client); fireEvent.click(screen.getByRole('tab', { name: '单车诊断' })); fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '粤A' } }); const candidateButton = await waitFor(() => { const button = document.querySelector('.v2-source-candidates button'); expect(button).toBeTruthy(); return button!; }); fireEvent.click(candidateButton); expect(await screen.findByText('当前推荐 G7')).toBeInTheDocument(); expect(screen.getByText('1 个来源')).toBeInTheDocument(); expect(screen.getByRole('group', { name: '车辆来源诊断概览' })).toHaveClass('v2-source-summary-region'); expect(document.querySelector('.v2-source-summary-region > .v2-source-summary.semi-card-group')).toBeInTheDocument(); expect(document.querySelectorAll('.v2-source-summary-card.semi-card')).toHaveLength(4); expect(document.querySelector('.v2-source-summary-card.is-vehicle')).toHaveTextContent('粤A00001'); expect(document.querySelector('.v2-source-summary-card.is-recommended')).toHaveTextContent('G7'); expect(document.querySelector('.v2-source-summary-card.is-source-health')).toHaveClass('is-warning'); expect(document.querySelector('.v2-source-summary-card.is-policy')).toHaveTextContent('v1'); expect(document.querySelectorAll('.v2-source-summary-card')[1]?.querySelector('.semi-tag')).toHaveTextContent('JT808'); expect(screen.getByText('推荐说明').closest('.semi-card')).toHaveClass('v2-source-recommendation'); expect(screen.getByText('最近策略审计').closest('.semi-card')).toHaveClass('v2-source-audit'); expect(screen.getByText('终端 133****0001')).toBeInTheDocument(); expect(screen.getByText('10s')).toBeInTheDocument(); expect(document.querySelector('.v2-source-table.semi-table-wrapper')).toBeInTheDocument(); expect(document.querySelector('.v2-source-table-wrap > table')).not.toBeInTheDocument(); expect(document.querySelector('.v2-source-mobile-list')).not.toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: '来源 / 终端' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: '运维策略' })).toBeInTheDocument(); expect(screen.getByText('优先级 20')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '维护策略' })); expect(await screen.findByRole('dialog', { name: '车辆来源策略' })).toBeInTheDocument(); expect(document.querySelector('.v2-source-policy-sidesheet .semi-sidesheet-inner')).toBeInTheDocument(); fireEvent.change(screen.getByLabelText('G7 提供方'), { target: { value: 'G7s' } }); const save = screen.getByRole('button', { name: '保存策略' }); expect(save).toBeDisabled(); expect(screen.getByLabelText('G7 策略备注')).toHaveValue('保持当前优先级'); fireEvent.change(screen.getByLabelText('G7 提供方核验依据'), { target: { value: 'GPS 运维终端清单 2026-07-16' } }); fireEvent.click(save); await waitFor(() => expect(mocks.updateVehicleSourcePolicy).toHaveBeenCalledWith('VIN001', { version: 1, sourceRef: 'a'.repeat(64), providerName: 'G7s', providerEvidence: 'GPS 运维终端清单 2026-07-16', enabled: true, priority: 20, remark: '保持当前优先级' })); await waitFor(() => expect(mocks.vehicleSourceDiagnostic).toHaveBeenCalledWith('VIN001', expect.any(AbortSignal))); }); test('allows provider maintenance but keeps canonical source policy read only', async () => { layout.mobile = true; seedSession(); mocks.opsHealth.mockResolvedValue({ linkHealth: [], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5, tdengineWritable: true, mysqlWritable: true, runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false } }); mocks.sourceReadiness.mockResolvedValue({ totalVehicles: 1, boundVehicles: 1, identityRequiredVehicles: 0, onlineVehicles: 1, sources: [] }); mocks.vehicleCoverage.mockResolvedValue({ items: [{ vin: 'VIN-CANONICAL', plate: '沪A00001', protocols: ['JT808'], missingProtocols: [], sourceStatus: [], sourceCount: 1, onlineSourceCount: 1, online: true, lastSeen: '', bindingStatus: 'bound' }], total: 1, limit: 20, offset: 0 }); const diagnostic = { evidence: { vin: 'VIN-CANONICAL', plate: '沪A00001', mileageDate: '', recommendedLocationProtocol: 'JT808', recommendedLocationLabel: 'JT808', locationConflict: false, locationSources: [{ protocol: 'JT808', sourceLabel: 'JT808', providerOverride: '', terminalLabel: '', sourceKind: 'CANONICAL', sourceRef: 'b'.repeat(64), selectedWithinProtocol: true, recommended: true, enabled: true, priority: 100, policyRemark: '', online: true, qualityStatus: 'OK', qualityReason: '', longitude: 113.1, latitude: 23.1, eventTime: '2026-07-16 10:00:00', receivedAt: '2026-07-16 10:00:01', selectionReason: '协议融合快照只能维护提供方' }], mileageSources: [], comparison: { locationMaxDistanceM: 0, totalMileageDeltaKm: 0, dailyMileageDeltaKm: 0, reportTimeDeltaSeconds: 0 }, asOf: '' }, policy: { vin: 'VIN-CANONICAL', version: 1, updatedBy: 'system', updatedAt: '', audit: [] }, recommendationReason: '当前推荐 JT808 的协议融合结果', refreshHint: '等待下一次上报' }; mocks.vehicleSourceDiagnostic.mockResolvedValue(diagnostic); mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); renderOperations(client); fireEvent.click(screen.getByRole('tab', { name: '单车诊断' })); const filterToggle = screen.getByRole('button', { name: '修改诊断车辆:尚未选择车辆' }); expect(filterToggle).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(filterToggle); const filterDialog = await screen.findByRole('dialog', { name: '诊断车辆筛选' }); expect(document.querySelector('.v2-source-mobile-filter-sidesheet')).toHaveClass('semi-sidesheet-bottom'); expect(document.querySelector('.v2-source-mobile-filter-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(78dvh, 660px)' }); expect(within(filterDialog).getByText('输入车牌或 VIN 开始查找')).toBeInTheDocument(); fireEvent.change(within(filterDialog).getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '沪A' } }); const candidateButton = await waitFor(() => { const button = filterDialog.querySelector('.v2-source-candidates button'); expect(button).toBeTruthy(); return button!; }); fireEvent.click(candidateButton); expect(await screen.findByText('协议融合快照只能维护提供方')).toBeInTheDocument(); const selectedToggle = screen.getByRole('button', { name: '修改诊断车辆:已选 沪A00001' }); expect(selectedToggle).toHaveAttribute('aria-expanded', 'false'); expect(filterDialog).toHaveClass('semi-sidesheet-animation-content_hide_bottom'); fireEvent.click(selectedToggle); const selectedSearch = within(filterDialog).getByLabelText('按车牌或 VIN 搜索诊断车辆'); expect(selectedSearch).toHaveValue('沪A00001'); fireEvent.change(selectedSearch, { target: { value: '其他车辆' } }); fireEvent.click(within(filterDialog).getByRole('button', { name: '关闭诊断车辆筛选' })); fireEvent.click(selectedToggle); expect(selectedSearch).toHaveValue('沪A00001'); fireEvent.click(within(filterDialog).getByRole('button', { name: '关闭诊断车辆筛选' })); expect(document.querySelector('.v2-source-table')).not.toBeInTheDocument(); expect(document.querySelector('.v2-source-mobile-card.semi-card')).toBeInTheDocument(); expect(document.querySelector('.v2-source-mobile-descriptions.semi-descriptions')).toBeInTheDocument(); expect(screen.getByRole('group', { name: '车辆来源诊断概览' })).toHaveClass('v2-source-summary-region'); expect(document.querySelector('.v2-source-summary-region > .v2-source-summary.semi-card-group')).toBeInTheDocument(); expect(document.querySelectorAll('.v2-source-summary-card.semi-card')).toHaveLength(4); expect(screen.getByText('优先级 100')).toBeInTheDocument(); expect(screen.queryByLabelText('JT808 优先级')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '维护提供方' })); expect(await screen.findByRole('dialog', { name: '车辆来源策略' })).toBeInTheDocument(); expect(await screen.findByLabelText('JT808 优先级')).toBeDisabled(); expect(screen.getByLabelText('JT808 策略备注')).toBeDisabled(); expect(screen.getByRole('checkbox', { name: '启用' })).toBeDisabled(); expect(screen.getByLabelText('JT808 提供方')).toBeEnabled(); fireEvent.change(screen.getByLabelText('JT808 提供方'), { target: { value: '东方北斗' } }); fireEvent.change(screen.getByLabelText('JT808 提供方核验依据'), { target: { value: 'GPS 运维终端清单 2026-07-16' } }); fireEvent.click(screen.getByRole('button', { name: '保存提供方' })); await waitFor(() => expect(mocks.updateVehicleSourcePolicy).toHaveBeenCalledWith('VIN-CANONICAL', { version: 1, sourceRef: 'b'.repeat(64), providerName: '东方北斗', providerEvidence: 'GPS 运维终端清单 2026-07-16', enabled: true, priority: 100, remark: '' })); });