feat: build vehicle data platform and production pipeline

This commit is contained in:
lingniu
2026-07-14 12:35:33 +08:00
parent b452be3b94
commit bb59303a4b
270 changed files with 88016 additions and 1975 deletions

View File

@@ -3,6 +3,85 @@ import { api } from './client';
afterEach(() => {
vi.restoreAllMocks();
window.sessionStorage.clear();
});
test('authenticated requests use the session-only bearer token', async () => {
window.sessionStorage.setItem('vehicle-platform.access-token', 'operator-secret-token');
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { name: 'operator-a', role: 'operator', authMode: 'enforce' } }) } as Response);
await api.session();
const [, init] = fetchMock.mock.calls[0];
expect(new Headers(init?.headers).get('Authorization')).toBe('Bearer operator-secret-token');
expect(window.localStorage.getItem('vehicle-platform.access-token')).toBeNull();
});
test('durable alert APIs keep versioned actions, rules and notification reads explicit', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {}, traceId: 'trace-alert', timestamp: 1 }) } as Response);
await api.alertEventsV2({ status: 'unprocessed', limit: 20, offset: 0 });
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/alerts/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'unprocessed', limit: 20, offset: 0 }) });
await api.actOnAlertV2('alert 1', { version: 2, action: 'acknowledge', note: '已确认' });
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/alerts/events/alert%201/actions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 2, action: 'acknowledge', note: '已确认' }) });
await api.readAlertNotificationsV2([7, 8]);
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/alerts/notifications/read', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: [7, 8] }) });
});
test('access APIs post one shared filter contract and version threshold updates', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-access', timestamp: 1783094400000 })
} as Response);
const query = { protocol: 'JT808', onlineState: 'offline', limit: 50, offset: 0 };
await api.accessVehicles(query);
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/vehicles', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
});
const unresolved = { protocol: 'JT808', limit: 20, offset: 0 };
await api.accessUnresolvedIdentities(unresolved);
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/unresolved-identities', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(unresolved)
});
await api.updateAccessThresholds({ version: 3, defaultThresholdSec: 300, delayThresholdSec: 30, longOfflineSec: 1800, protocols: [{ protocol: 'JT808', thresholdSec: 60 }] });
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/thresholds', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 3, defaultThresholdSec: 300, delayThresholdSec: 30, longOfflineSec: 1800, protocols: [{ protocol: 'JT808', thresholdSec: 60 }] })
});
});
test('vehicle profile API uses encoded VIN and optimistic version updates', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {}, traceId: 'trace-profile', timestamp: 1 }) } as Response);
const input = { modelName: '氢燃料重卡', vehicleType: '重卡', companyName: '示范物流', operationStatus: 'active' as const, accessProvider: '车厂平台', firstAccessAt: '2026-07-01T08:30', runtimeSeconds: 3600, version: 2 };
await api.updateVehicleProfile('VIN 001', input);
expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicles/VIN%20001/profile', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
});
});
test('latest telemetry uses the encoded vehicle identity path', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { categories: [], values: [] }, traceId: 'trace-telemetry', timestamp: 1 }) } as Response);
await api.latestTelemetry('粤A 001');
expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicles/%E7%B2%A4A%20001/telemetry/latest', undefined);
});
test('vehicle profile sync posts an explicit dry-run and conflict policy contract', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {}, traceId: 'trace-profile-sync', timestamp: 1 }) } as Response);
const input = {
sourceSystem: 'oem-tsp', sourceVersion: 'snapshot-1', conflictPolicy: 'preserve' as const, dryRun: true,
items: [{ vin: 'VIN001', modelName: '车型一', vehicleType: '重卡', companyName: '示范物流', operationStatus: 'active' as const, accessProvider: '车厂平台', firstAccessAt: '', runtimeSeconds: null }]
};
await api.syncVehicleProfiles(input);
expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicle-profiles/sync', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
});
});
test('trackPlayback preserves the bounded V2 query contract', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({ data: { vin: 'VIN001', points: [], events: [], sources: [], summary: { pointCount: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 })
} as Response);
await api.trackPlayback(new URLSearchParams({ keyword: '粤AG18312', maxPoints: '1200' }));
expect(fetchMock).toHaveBeenCalledWith('/api/v2/tracks?keyword=%E7%B2%A4AG18312&maxPoints=1200', undefined);
});
test('rawFramesQuery posts structured JSON instead of URL query strings', async () => {