feat(operations): add automated reconciliation center
This commit is contained in:
@@ -5,7 +5,8 @@ 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()
|
||||
vehicleSourceDiagnostic: vi.fn(), updateVehicleSourcePolicy: vi.fn(),
|
||||
reconciliationSummary: vi.fn(), reconciliationIssues: vi.fn(), reconciliationIssue: vi.fn(), updateReconciliationIssue: vi.fn()
|
||||
}));
|
||||
vi.mock('../../api/client', () => ({ api: mocks }));
|
||||
|
||||
@@ -13,8 +14,60 @@ afterEach(() => { cleanup(); Object.values(mocks).forEach((mock) => mock.mockRes
|
||||
|
||||
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: [], 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 } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
|
||||
expect(await screen.findByText('数据差异中心')).toBeInTheDocument();
|
||||
fireEvent.click(await screen.findByText('多来源实时位置漂移'));
|
||||
expect(await screen.findByText('规则证据')).toBeInTheDocument();
|
||||
expect(screen.getByText('1286')).toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('处置状态'), { target: { value: 'confirmed_source_a' } });
|
||||
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('reconciles service identities with bound and identity-required vehicles', async () => {
|
||||
seedSession();
|
||||
mocks.opsHealth.mockResolvedValue({
|
||||
@@ -92,8 +145,12 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '粤A' } });
|
||||
expect(await screen.findByText('粤A00001')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('粤A00001'));
|
||||
const candidateButton = await waitFor(() => {
|
||||
const button = document.querySelector<HTMLButtonElement>('.v2-source-candidates button');
|
||||
expect(button).toBeTruthy();
|
||||
return button!;
|
||||
});
|
||||
fireEvent.click(candidateButton);
|
||||
expect(await screen.findByText('当前推荐 G7')).toBeInTheDocument();
|
||||
expect(screen.getByText('终端 133****0001')).toBeInTheDocument();
|
||||
expect(screen.getByText('10s')).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user