137 lines
11 KiB
TypeScript
137 lines
11 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { Hono } from 'hono';
|
|
import { mapFault } from './model.js';
|
|
import { faultDashboard, dashboardAnalysis, filterDashboardRows } from './dashboard.js';
|
|
import { createFaultRouter } from './index.js';
|
|
import type { AuthUser } from '../../auth/types.js';
|
|
const updatedAt='2026-10-13T08:00:00Z';
|
|
const q={start:'2026-10-12',end:'2026-10-13'};
|
|
const fault=(id:string,extra:Record<string,unknown>={})=>mapFault({id,fault_code:`F${id}`,matched_vehicle_id:'v1',plate_number:'沪A001',model_name:'车型A',province_name:'上海市',customer_name:'客户A',business_dept:'部门A',reported_at:'2026-10-13 08:00:00',handling_status:'待处理',fault_level:1,fault_categories:'底盘故障,三电故障',...extra},new Map());
|
|
const rows=[fault('1'),fault('2',{handling_status:'已归档',archived_at:'2026-10-13 12:00:00'}),fault('3',{reported_at:'2026-10-01 08:00:00',handling_status:'处理中',deadline_at:'2026-10-12 15:00:00',matched_vehicle_id:'v2'}),fault('4',{matched_vehicle_id:null,plate_number:null,fault_level:null,fault_categories:null,customer_name:null}),fault('5',{reported_at:'2026-10-10 08:00:00',handling_status:'挂起'})];
|
|
test('dashboard separates reporting cohort from all-date current workload, preserves unknowns and deduplicates vehicles',()=>{
|
|
const d=faultDashboard(rows,q,updatedAt);
|
|
assert.equal(d.totals.reported,3);assert.equal(d.totals.vehicles,1);assert.equal(d.totals.customers,1);
|
|
assert.equal(d.totals.open,3);assert.equal(d.totals.waiting+d.totals.processing,d.totals.open);assert.equal(d.totals.overdue,1);assert.equal(d.totals.longestOverdueDays,2);
|
|
assert.equal(d.totals.previous,1);assert.equal(d.totals.change,200);
|
|
assert.equal(d.quality.missingLevel,1);assert.equal(d.quality.missingVehicle,1);assert.equal(d.quality.missingCategory,1);
|
|
assert.equal(d.focus[0].id,'3');assert.equal(d.distributions.region.reduce((n,r)=>n+r.reported,0),3);
|
|
const m=rows[0].assetCell.model;assert.equal(d.matrix.totals[m],3);assert.equal(d.matrix.cells.find(c=>c.model===m&&c.category==='底盘故障')?.count,2);
|
|
assert.equal(d.matrix.cells.find(c=>c.model===m&&c.category==='三电故障')?.count,2);
|
|
});
|
|
test('dashboard drill scope and all filters reconcile to visible aggregates without leaking internal fields',()=>{
|
|
const d=faultDashboard(rows,q,updatedAt);const group=d.distributions.customer.find(r=>r.label!=='未关联客户')!;
|
|
const details=dashboardAnalysis(rows,{...q,customer:group.value},updatedAt);
|
|
assert.equal(details.rows.length,group.reported);assert.ok(details.rows.every(r=>!('vehicleKey'in r)&&!('managerId'in r)));
|
|
assert.deepEqual(dashboardAnalysis(rows,{...q,scope:'overdue'},updatedAt).rows.map(r=>r.id),['3']);
|
|
assert.equal(dashboardAnalysis(rows,{...q,scope:'ledger',id:'5'},updatedAt).rows.length,1);
|
|
assert.equal(filterDashboardRows(rows,{level:'未标注',category:'未标注'}).length,1);
|
|
assert.equal(dashboardAnalysis(rows,{...q,scope:'cohort',level:'未标注'},updatedAt).rows.length,1);
|
|
const empty=faultDashboard(rows,{...q,region:'不存在'},updatedAt);assert.equal(empty.totals.reported,0);assert.equal(empty.totals.change,null);assert.equal(empty.totals.longestOverdueDays,0);
|
|
});
|
|
test('dashboard and report endpoints enforce authentication, permissions, dates and shared filters',async()=>{
|
|
let loads=0;const router=createFaultRouter(async()=>{loads++;return{rows,updatedAt};});const anonymous=new Hono();anonymous.route('/f',router);
|
|
assert.equal((await anonymous.request('/f/dashboard')).status,401);assert.equal(loads,0);
|
|
const user:AuthUser={userId:'test',loginName:'test',userName:'测试',roles:[],permissionLevel:'department',depName:'部门A',depCode:'a'};
|
|
const app=new Hono<{Variables:{user:AuthUser}}>();app.use('*',async(c,next)=>{c.set('user',user);await next();});app.route('/f',router);
|
|
assert.equal((await app.request('/f/dashboard?start=bad')).status,400);assert.equal(loads,0);
|
|
assert.equal((await app.request('/f/dashboard-analysis?scope=unknown')).status,400);assert.equal(loads,0);
|
|
const report=await(await app.request('/f/dashboard?start=2026-10-12&end=2026-10-13')).json();assert.equal(report.totals.reported,2);assert.equal(report.options.level.some((o:{value:string})=>o.value==='未标注'),false);
|
|
const trend=await(await app.request('/f/trend?start=2026-10-12&end=2026-10-13&level=未标注')).json();assert.equal(trend.totals.reported,0);
|
|
const weekly=await(await app.request('/f/weekly-report?start=2026-10-12&end=2026-10-13&level=未标注')).json();assert.equal(weekly.totals.reported,0);
|
|
});
|
|
|
|
test('focus contains only L1 unclosed faults across all dates and keeps totals and full drill consistent', () => {
|
|
const focusRows = [
|
|
fault('waiting', { reported_at: '2026-09-01 08:00:00' }),
|
|
fault('processing', { handling_status: '处理中', deadline_at: '2026-10-12 15:00:00' }),
|
|
fault('paused', { handling_status: '挂起', deadline_at: '2026-10-01 08:00:00' }),
|
|
fault('review', { handling_status: '审核中' }),
|
|
fault('unknown', { handling_status: '其他状态' }),
|
|
fault('sixth'),
|
|
fault('seventh'),
|
|
fault('closed', { handling_status: '已归档' }),
|
|
fault('l2-overdue', { fault_level: 2, deadline_at: '2026-10-01 08:00:00' }),
|
|
fault('l3', { fault_level: 3 }),
|
|
fault('l4', { fault_level: 4 }),
|
|
fault('unmarked', { fault_level: null }),
|
|
];
|
|
const dashboard = faultDashboard(focusRows, q, updatedAt);
|
|
assert.deepEqual(dashboard.focusTotals, { total: 7, overdue: 1 });
|
|
assert.equal(dashboard.focus.length, 5);
|
|
assert.equal(dashboard.focus[0].id, 'processing');
|
|
assert.ok(dashboard.focus.every(row => row.level === 'L1-特急' && row.status !== '已归档'));
|
|
const allFocus = dashboardAnalysis(focusRows, { ...q, scope: 'focus' }, updatedAt).rows;
|
|
assert.equal(allFocus.length, dashboard.focusTotals.total);
|
|
assert.ok(allFocus.some(row => row.id === 'waiting'));
|
|
assert.ok(allFocus.some(row => row.id === 'paused'));
|
|
assert.ok(allFocus.some(row => row.id === 'review'));
|
|
assert.ok(allFocus.every(row => !('vehicleKey' in row) && !('managerId' in row)));
|
|
assert.deepEqual(dashboardAnalysis(focusRows, { ...q, scope: 'focus', id: 'closed' }, updatedAt).rows, []);
|
|
const filtered = faultDashboard(focusRows, { ...q, level: 'L2-紧急' }, updatedAt);
|
|
assert.deepEqual(filtered.focus, []);
|
|
assert.deepEqual(filtered.focusTotals, { total: 0, overdue: 0 });
|
|
assert.deepEqual(dashboardAnalysis(focusRows, { ...q, scope: 'focus', level: 'L2-紧急' }, updatedAt).rows, []);
|
|
});
|
|
test('focus dashboard and full drill enforce permissions and business filters before selecting L1 faults', async () => {
|
|
const scopedRows = [fault('allowed'), fault('outside', { business_dept: '部门B' }), fault('closed', { handling_status: '已归档' }), fault('lower', { fault_level: 2 })];
|
|
const user: AuthUser = { userId: 'test', loginName: 'test', userName: '测试', roles: [], permissionLevel: 'department', depName: '部门A', depCode: 'a' };
|
|
const app = new Hono<{ Variables: { user: AuthUser } }>();
|
|
app.use('*', async (c, next) => { c.set('user', user); await next(); });
|
|
app.route('/f', createFaultRouter(async () => ({ rows: scopedRows, updatedAt })));
|
|
const report = await (await app.request('/f/dashboard?start=2026-10-12&end=2026-10-13')).json();
|
|
assert.deepEqual(report.focus.map((row: { id: string }) => row.id), ['allowed']);
|
|
assert.deepEqual(report.focusTotals, { total: 1, overdue: 0 });
|
|
const response = await app.request('/f/dashboard-analysis?start=2026-10-12&end=2026-10-13&scope=focus');
|
|
assert.equal(response.status, 200);
|
|
const detail = await response.json();
|
|
assert.deepEqual(detail.rows.map((row: { id: string }) => row.id), ['allowed']);
|
|
const empty = await (await app.request('/f/dashboard-analysis?scope=focus&department=不存在')).json();
|
|
assert.deepEqual(empty.rows, []);
|
|
});
|
|
|
|
test('report source follows explicit OneOS report method before legacy task linkage', () => {
|
|
const cases: [Record<string, unknown>, 'operations' | 'customer'][] = [
|
|
[{ report_method: '运维上报', source_work_order_id: '9007199254740993' }, 'operations'],
|
|
[{ report_method: '客户上报', source_work_order_id: null }, 'customer'],
|
|
[{ report_method: ' 客户上报 ', source_work_order_id: '123' }, 'customer'],
|
|
[{ report_method: null, source_work_order_id: '9007199254740993' }, 'customer'],
|
|
[{ report_method: '', source_work_order_id: null }, 'operations'],
|
|
[{ report_method: null, source_work_order_id: null, fault_source: 6 }, 'operations'],
|
|
];
|
|
for (const [raw, expected] of cases) assert.equal(fault('source', raw).reportMethod, expected);
|
|
});
|
|
test('reported source counts reconcile with cohort, filtered drills, permissions and export labels', async () => {
|
|
const sourceRows = [
|
|
fault('ops-linked', { report_method: '运维上报', source_work_order_id: '123' }),
|
|
fault('ai-explicit', { report_method: '客户上报', source_work_order_id: null }),
|
|
fault('ai-legacy', { report_method: null, source_work_order_id: '9007199254740993', handling_status: '已归档' }),
|
|
fault('ops-legacy', { report_method: null, source_work_order_id: null }),
|
|
fault('previous-ai', { report_method: '客户上报', reported_at: '2026-10-10 10:00:00' }),
|
|
fault('hidden-ai', { report_method: '客户上报', business_dept: '部门B' }),
|
|
];
|
|
const user: AuthUser = { userId: 'test', loginName: 'test', userName: '测试', roles: [], permissionLevel: 'department', depName: '部门A', depCode: 'a' };
|
|
const app = new Hono<{ Variables: { user: AuthUser } }>();
|
|
app.use('*', async (c, next) => { c.set('user', user); await next(); });
|
|
app.route('/f', createFaultRouter(async () => ({ rows: sourceRows, updatedAt })));
|
|
const query = new URLSearchParams(q);
|
|
const report = await (await app.request(`/f/dashboard?${query}`)).json();
|
|
assert.equal(report.totals.reported, 4);
|
|
assert.equal(report.totals.operationsReported, 2);
|
|
assert.equal(report.totals.customerReported, 2);
|
|
assert.equal(report.totals.operationsReported + report.totals.customerReported, report.totals.reported);
|
|
assert.equal(report.options.reportMethod.find((o: { value: string }) => o.value === 'customer').label, 'AI提报');
|
|
assert.equal(report.options.reportMethod.find((o: { value: string }) => o.value === 'operations').label, '运维提报');
|
|
for (const [method, expected] of [['operations', ['ops-linked', 'ops-legacy']], ['customer', ['ai-explicit', 'ai-legacy']]] as const) {
|
|
const response = await app.request(`/f/dashboard-analysis?${query}&scope=cohort&reportMethod=${method}`);
|
|
assert.equal(response.status, 200);
|
|
const detail = await response.json();
|
|
assert.deepEqual(detail.rows.map((row: { id: string }) => row.id).sort(), [...expected].sort());
|
|
const filtered = await (await app.request(`/f/dashboard?${query}&reportMethod=${method}`)).json();
|
|
assert.equal(filtered.totals.reported, expected.length);
|
|
assert.equal(filtered.totals.operationsReported + filtered.totals.customerReported, filtered.totals.reported);
|
|
}
|
|
const empty = await (await app.request(`/f/dashboard?${query}®ion=不存在`)).json();
|
|
assert.equal(empty.totals.operationsReported, 0); assert.equal(empty.totals.customerReported, 0);
|
|
});
|