import { describe, expect, it } from 'vitest'; import { aggregateDemoFlowResults, demoFlowStatusLabel, type DemoFlowSourceResult } from './demoFlowAggregation'; function result(id: number, index: number, status: string, resultData: Record): DemoFlowSourceResult { return { id, type: 'plugin', pluginId: `plugin-${id}`, status, resultData: { demoFlowIndex: index, ...resultData } }; } const nameOf = (item: DemoFlowSourceResult) => `算法${item.id}`; describe('aggregateDemoFlowResults', () => { it('按全流程顺序合并测量项,并保留来源和算法内顺序', () => { const model = aggregateDemoFlowResults([ result(20, 2, 'completed', { measurements: [{ name: 'B1', value: 2 }, { name: 'B2', value: 3 }] }), result(10, 1, 'completed', { measurements: [{ name: 'A1', value: 1 }] }), ], nameOf); expect(model.measurementRows.map((row) => [row.measurement.name, row.resultId, row.flowIndex, row.measurementIndex])).toEqual([ ['A1', 10, 1, 0], ['B1', 20, 2, 0], ['B2', 20, 2, 1], ]); }); it('原样保留空值、公差文本、基准长度和自定义结论文案,并忽略空测量', () => { const measurement = { name: '斜率', value: null, unit: 'mm', tolerance: 0.5, toleranceLabel: '+8.5/-3', baseline: 1000, status: 'warning' as const, passLabel: '达标', warningLabel: '复核', failLabel: '未达标', }; const model = aggregateDemoFlowResults([ result(10, 1, 'completed', { measurements: [] }), result(20, 2, 'completed', { measurements: [measurement] }), ], nameOf); expect(model.measurementRows).toHaveLength(1); expect(model.measurementRows[0].measurement).toEqual(measurement); }); it('默认不生成算法分组行,开启后按算法生成分组', () => { const inputs = [ result(10, 1, 'completed', { measurements: [{ name: 'A', value: 1 }] }), result(20, 2, 'completed', { measurements: [{ name: 'B', value: 2 }] }), ]; expect(aggregateDemoFlowResults(inputs, nameOf).tableRows.every((row) => row.kind === 'measurement')).toBe(true); expect(aggregateDemoFlowResults(inputs, nameOf, { includeAlgorithmGroups: true }).tableRows.map((row) => row.kind)).toEqual([ 'algorithm', 'measurement', 'algorithm', 'measurement', ]); }); it('过滤参考项,同时保留其他状态和未设置状态的测量项', () => { const model = aggregateDemoFlowResults([ result(10, 1, 'completed', { measurements: [ { name: '参考项', value: 0.1, status: 'info' }, { name: '合格项', value: 1, status: 'pass' }, { name: '警告项', value: 2, status: 'warning' }, { name: '失败项', value: 3, status: 'fail' }, { name: '无状态项', value: 4 }, ], }), ], nameOf); expect(model.measurementRows.map((row) => row.measurement.name)).toEqual([ '合格项', '警告项', '失败项', '无状态项', ]); expect(model.tableRows).toHaveLength(4); }); it('过滤全流程指定的中间指标,同时保持其他有效测量项的原顺序', () => { const model = aggregateDemoFlowResults([ result(10, 1, 'completed', { measurements: [ { name: '车体长度', value: 100, status: 'pass' }, { name: '前后端平移差', value: 0.1, status: 'pass' }, { name: '车体宽度', value: 20, status: 'warning' }, { name: 'ICP fitness', value: 0.98, status: 'pass' }, { name: '配准 RMSE', value: 0.02, status: 'pass' }, { name: '车体高度', value: 30, status: 'fail' }, ], }), ], nameOf); expect(model.measurementRows.map((row) => row.measurement.name)).toEqual([ '车体长度', '车体宽度', '车体高度', ]); }); it('全流程结果使用统一结论文案,不采用算法自定义文案', () => { expect(demoFlowStatusLabel({ status: 'pass' })).toBe('合格'); expect(demoFlowStatusLabel({ status: 'warning' })).toBe('警告'); expect(demoFlowStatusLabel({ status: 'fail' })).toBe('超差'); expect(demoFlowStatusLabel({ status: 'info' })).toBe('参考'); expect(demoFlowStatusLabel({})).toBe('参考'); const model = aggregateDemoFlowResults([ result(10, 1, 'completed', { measurements: [ { name: '通过项', value: 1, status: 'pass', passLabel: '通过' }, { name: '警告项', value: 2, status: 'warning', warningLabel: '部分成功' }, { name: '失败项', value: 3, status: 'fail', failLabel: '失败' }, ], }), ], nameOf); expect(model.measurementRows.map((row) => demoFlowStatusLabel(row.measurement))).toEqual([ '合格', '警告', '超差', ]); }); it('全部为参考项时进入空测量结果状态,且不生成算法分组行', () => { const model = aggregateDemoFlowResults([ result(10, 1, 'completed', { measurements: [ { name: '配准误差 p95', value: 0.1, status: 'info' }, { name: '配准误差均值', value: 0.05, status: 'info' }, ], }), ], nameOf, { includeAlgorithmGroups: true }); expect(model.measurementRows).toEqual([]); expect(model.tableRows).toEqual([]); }); it('让下载、图片、3D 和原始字段始终关联各自算法与结果 ID', () => { const model = aggregateDemoFlowResults([ result(10, 1, 'completed', { files: ['a.xlsx'], images: ['a.png'], overlay3d: { kind: 'ply', file: 'a.ply' }, extra: { source: 'a' }, }), result(20, 2, 'completed', { files: [{ name: '报表B', path: 'nested/b.xlsx' }], images: ['b.png'], overlay3d: { kind: 'none' }, extra: { source: 'b' }, }), ], nameOf); expect(model.artifactGroups).toMatchObject([ { resultId: 10, sourceAlgorithmId: 'plugin-10', files: [{ label: 'a.xlsx', path: 'a.xlsx' }], images: ['a.png'], extra: { source: 'a' } }, { resultId: 20, sourceAlgorithmId: 'plugin-20', files: [{ label: '报表B', path: 'nested/b.xlsx' }], images: ['b.png'], extra: { source: 'b' } }, ]); }); it('为运行中、排队、失败和停止状态生成提示,同时保留部分完成结果', () => { const model = aggregateDemoFlowResults([ result(10, 1, 'completed', { measurements: [{ name: '已完成', value: 1 }] }), result(20, 2, 'processing', { progress: 0.42, stage: '计算中' }), result(30, 3, 'pending', { stage: '等待资源' }), result(40, 4, 'error', { error: '输入无效' }), result(50, 5, 'cancelled', {}), ], nameOf); expect(model.measurementRows.map((row) => row.measurement.name)).toEqual(['已完成']); expect(model.statusNotices).toMatchObject([ { resultId: 20, status: 'processing', progress: 0.42, detail: '计算中' }, { resultId: 30, status: 'pending', detail: '等待资源' }, { resultId: 40, status: 'error', detail: '输入无效' }, { resultId: 50, status: 'cancelled' }, ]); }); });