feat(platform): harden telemetry pipeline and unify Semi UI workspaces

This commit is contained in:
lingniu
2026-07-18 00:26:36 +08:00
parent 65b4e4f055
commit 159c80b0ae
136 changed files with 21616 additions and 1785 deletions

View File

@@ -8,9 +8,11 @@ const mocks = vi.hoisted(() => ({
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(); Object.values(mocks).forEach((mock) => mock.mockReset()); });
afterEach(() => { cleanup(); layout.mobile = false; Object.values(mocks).forEach((mock) => mock.mockReset()); });
function seedSession() {
mocks.session.mockResolvedValue({ name: '平台管理员', role: 'admin', userType: 'admin', authMode: 'enforce', menuKeys: ['operations'] });
@@ -41,7 +43,7 @@ function seedSession() {
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,
linkHealth: [{ name: 'MySQL', 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 }
});
@@ -57,10 +59,42 @@ test('renders reconciliation queue, loads evidence on demand and records review
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
expect(await screen.findByText('数据差异中心')).toBeInTheDocument();
fireEvent.click(await screen.findByText('多来源实时位置漂移'));
expect(screen.getByRole('tab', { name: '差异处置', selected: true })).toHaveClass('semi-button');
expect(screen.getByRole('tabpanel', { name: '差异处置' })).toBeInTheDocument();
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');
expect(document.querySelector('.v2-reconcile-kpi-card small')?.textContent).toBe('活跃差异');
expect(document.querySelectorAll('.v2-reconcile-kpi-card')).toHaveLength(4);
expect(screen.getByText('超过 24 小时').closest('.semi-card')).toHaveClass('v2-reconcile-sla');
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
const trendButton = screen.getByRole('button', { name: '30 天趋势' });
expect(trendButton).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(trendButton);
expect(screen.getByText('近 30 天趋势').closest('.semi-card')).toHaveClass('v2-reconcile-trend');
expect(screen.getByText('近 30 天趋势').closest('.v2-workspace-panel-header')).toHaveClass('is-compact');
expect(trendButton).toHaveAttribute('aria-expanded', 'true');
expect(document.querySelector('.v2-reconcile-table.semi-table-wrapper')).toBeInTheDocument();
const issueTitles = await screen.findAllByText('多来源实时位置漂移');
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');
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')).toBeInTheDocument();
expect(screen.getByText('1286')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('处置状态'), { target: { value: 'confirmed_source_a' } });
fireEvent.click(screen.getByRole('button', { name: '关闭差异详情' }));
expect(reconcileRow).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(issueTitles[0]);
expect(await screen.findByText('规则证据')).toBeInTheDocument();
fireEvent.click(screen.getByRole('radio', { name: '确认来源 A' }));
fireEvent.change(screen.getByLabelText('说明(必填)'), { target: { value: '来源 A 原始报文可信' } });
fireEvent.click(screen.getByRole('button', { name: '保存复核结论' }));
await waitFor(() => expect(mocks.updateReconciliationIssue).toHaveBeenCalledWith('issue-1', {
@@ -68,20 +102,65 @@ test('renders reconciliation queue, loads evidence on demand and records review
}));
});
test('reconciles service identities with bound and identity-required vehicles', async () => {
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 } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
expect(await screen.findByText('多来源实时位置漂移')).toBeInTheDocument();
expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).toBeInTheDocument();
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(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');
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 天趋势' }));
const trend = screen.getByText('近 30 天趋势').closest('.v2-reconcile-trend');
expect(trend).toBeInTheDocument();
expect(screen.getByText(/最近运行/)).toBeInTheDocument();
expect(document.querySelector('.v2-reconcile-trend-toggle')).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(screen.getByRole('button', { name: '收起趋势' }));
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
});
test('reconciles service identities with bound and identity-required vehicles', async () => {
seedSession();
mocks.opsHealth.mockResolvedValue({
linkHealth: [{ name: 'MySQL', 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: []
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 } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
fireEvent.click(screen.getByRole('tab', { name: '全局健康' }));
expect(await screen.findByText('1024 / 234')).toBeInTheDocument();
for (const key of [['ops-health-v2'], ['ops-source-readiness-v2']]) {
const query = client.getQueryCache().find({ queryKey: key });
@@ -91,6 +170,22 @@ test('reconciles service identities with bound and identity-required vehicles',
}
expect(screen.getByText('服务身份 / 在线')).toBeInTheDocument();
expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument();
expect(screen.getByText('服务身份 / 在线').closest('.semi-card')).toHaveClass('v2-ops-kpi-card');
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.semi-card')).toHaveTextContent('MySQL主库连接正常');
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.getByText('验收标准').closest('.semi-card')).toHaveClass('v2-ops-source-card');
expect(screen.queryByText('单车多来源诊断')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
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();
});
@@ -108,8 +203,10 @@ test('keeps health evidence visible when source readiness fails and retries that
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
fireEvent.click(screen.getByRole('tab', { name: '全局健康' }));
expect(await screen.findByText('来源就绪度暂时不可用')).toBeInTheDocument();
expect(screen.getByText('test-release')).toBeInTheDocument();
expect(screen.getByText('版本 test-release')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '数据链路', level: 5 })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /重试/ }));
expect(await screen.findByText('1024 / 234')).toBeInTheDocument();
@@ -146,6 +243,7 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '粤A' } });
const candidateButton = await waitFor(() => {
const button = document.querySelector<HTMLButtonElement>('.v2-source-candidates button');
@@ -154,8 +252,21 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
});
fireEvent.click(candidateButton);
expect(await screen.findByText('当前推荐 G7')).toBeInTheDocument();
expect(document.querySelectorAll('.v2-source-summary-card.semi-card')).toHaveLength(4);
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();
@@ -175,6 +286,7 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
});
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,
@@ -202,6 +314,7 @@ test('allows provider maintenance but keeps canonical source policy read only',
mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '沪A' } });
const candidateButton = await waitFor(() => {
const button = document.querySelector<HTMLButtonElement>('.v2-source-candidates button');
@@ -210,6 +323,14 @@ test('allows provider maintenance but keeps canonical source policy read only',
});
fireEvent.click(candidateButton);
expect(await screen.findByText('协议融合快照只能维护提供方')).toBeInTheDocument();
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.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();