import assert from 'node:assert/strict'; import test from 'node:test'; import { fetchMileageSummary, parseMileageSummary, MileageSummaryError } from './summary-api.js'; const originalConfig = { url: process.env.ONEOS_MILEAGE_API_BASE_URL, key: process.env.ONEOS_MILEAGE_API_KEY }; test.before(() => { process.env.ONEOS_MILEAGE_API_BASE_URL = 'https://oneos.test'; process.env.ONEOS_MILEAGE_API_KEY = 'test-only'; }); test.after(() => { if (originalConfig.url === undefined) delete process.env.ONEOS_MILEAGE_API_BASE_URL; else process.env.ONEOS_MILEAGE_API_BASE_URL = originalConfig.url; if (originalConfig.key === undefined) delete process.env.ONEOS_MILEAGE_API_KEY; else process.env.ONEOS_MILEAGE_API_KEY = originalConfig.key; }); const start = '2026-10-01', end = '2026-10-02'; const vehicle = (plate: string) => ({ plateNumber: plate, vin: 'VIN', periodMileageKm: 0, endDateTotalMileageKm: null, endDateDataTime: null, endDateSourceProtocol: null, validDays: 1, missingDays: 1, anomalyDays: 0, status: 'PARTIAL_DATA', qualityReasons: ['CARRIED_FORWARD'], sourceProtocols: ['MQTT'], updatedAt: null }); const payload = () => ({ code: 'SUCCESS', startDate: start, endDate: end, timezone: 'Asia/Shanghai', vehicleCount: 1, data: { vehicles: [vehicle('TEST0')], dailyTrend: [ { date: start, mileageKm: 0, validVehicleCount: 1, missingVehicleCount: 0, anomalyVehicleCount: 0 }, { date: end, mileageKm: null, validVehicleCount: 0, missingVehicleCount: 1, anomalyVehicleCount: 0 }, ] }, snapshotId: 's1', generatedAt: '2026-10-27T00:00:00Z', expiresAt: '2099-01-01T00:00:00Z', nextCursor: null as string | null }); test('summary boundary preserves real zero and missing trend; rejects malformed counts/status/values', () => { const p = payload(); assert.equal(parseMileageSummary(p,start,end).vehicles[0].periodMileageKm,0); assert.equal(parseMileageSummary(p,start,end).dailyTrend[1].mileageKm,null); for (const mutate of [ (p: ReturnType) => { p.data.vehicles[0].validDays = 2; }, (p: ReturnType) => { p.data.vehicles[0].status = 'NORMAL'; }, (p: ReturnType) => { p.data.vehicles[0].periodMileageKm = NaN; }, (p: ReturnType) => { p.data.dailyTrend[1].mileageKm = 0; }, ]) { const bad = payload(); mutate(bad); assert.throws(() => parseMileageSummary(bad,start,end)); } }); test('summary pagination keeps the snapshot and reuses one fleet trend; empty scope never means all', async t => { t.mock.method(globalThis, 'fetch', async (_url: string, options: RequestInit) => { const body = JSON.parse(options.body as string); assert.deepEqual(body.plateNumbers, ['TEST0','TEST1']); assert.equal(body.includeDailyTrend,true); assert.equal(body.pageSize,5000); const p = payload(); p.vehicleCount = 2; p.data.vehicles = [vehicle(body.cursor ? 'TEST1':'TEST0')]; p.data.dailyTrend[0].validVehicleCount = 2; p.data.dailyTrend[1].missingVehicleCount = 2; p.nextCursor = body.cursor ? null:'next'; return Response.json(p); }); const result = await fetchMileageSummary(start,end,['MQTT'],['TEST0','TEST1']); assert.equal(result.vehicles.length,2); assert.equal(result.dailyTrend.length,2); assert.equal(result.dailyTrend[1].mileageKm,null); await assert.rejects(fetchMileageSummary(start,end,['MQTT'],[]), /空车牌/); }); test('summary authorization, throttling and timeout errors are explicit and never return empty success', async t => { for (const status of [401,403,429,504]) { const mock = t.mock.method(globalThis, 'fetch', async () => Response.json({code:'TEST_ERROR',traceId:'test-trace'}, {status,headers:{'Retry-After':'30'}})); await assert.rejects(fetchMileageSummary(start,end,['MQTT']), e => e instanceof MileageSummaryError && e.status === status && e.traceId === 'test-trace' && e.retryAfter === '30'); mock.mock.restore(); } }); test('snapshot changes and duplicate vehicles reject partial pagination', async t => { for (const changedSnapshot of [true,false]) { const mock=t.mock.method(globalThis,'fetch',async (_url:string,options:RequestInit)=>{ const body=JSON.parse(options.body as string), p=payload(); p.vehicleCount=2; p.data.dailyTrend[0].validVehicleCount=2; p.data.dailyTrend[1].missingVehicleCount=2; p.nextCursor=body.cursor?null:'next'; if(body.cursor && changedSnapshot)p.snapshotId='s2'; return Response.json(p); }); await assert.rejects(fetchMileageSummary(start,end,['MQTT']),changedSnapshot?/快照/:/重复车辆/); mock.mock.restore(); } });