add passenger telemetry

This commit is contained in:
2026-05-21 00:09:11 +03:00
parent 66c84ecdeb
commit e57fea321f
15 changed files with 1411 additions and 6 deletions

View File

@@ -0,0 +1,142 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const { PassengerTelemetryAggregator } = require('../src/services/passengerTelemetryAggregator');
const NOW = new Date('2026-05-20T12:30:00Z');
const routeRows = [{
id: 1,
route_number: '12',
name: 'Route 12',
type: 'bus',
color: '#0066CC',
stops: [
{ stop_id: 1, sequence: 1, latitude: 55.485, longitude: 28.768 },
{ stop_id: 2, sequence: 2, latitude: 55.485, longitude: 28.778 }
]
}];
function createPool(rows = routeRows) {
return {
async query(sql, params = []) {
if (params.length > 0) {
return { rows: rows.filter((row) => row.id === params[0]) };
}
return { rows };
}
};
}
function createAggregator(options = {}) {
return new PassengerTelemetryAggregator({
pool: createPool(options.rows),
nowProvider: () => NOW,
...options
});
}
async function ingestCluster(aggregator, accuracyM = 18) {
await aggregator.ingest({
session_id: 'session-a',
latitude: 55.48502,
longitude: 28.7700,
accuracy_m: accuracyM,
speed_kmh: 31,
heading: 90,
observed_at: '2026-05-20T12:29:55Z',
route_id: 1
});
await aggregator.ingest({
session_id: 'session-b',
latitude: 55.48498,
longitude: 28.77015,
accuracy_m: accuracyM,
speed_kmh: 33,
heading: 92,
observed_at: '2026-05-20T12:29:58Z',
route_id: 1
});
}
test('one passenger point does not create a virtual vehicle', async () => {
const aggregator = createAggregator();
const result = await aggregator.ingest({
session_id: 'session-a',
latitude: 55.485,
longitude: 28.770,
accuracy_m: 18,
speed_kmh: 32,
heading: 90,
observed_at: '2026-05-20T12:29:55Z',
route_id: 1
});
assert.equal(result.accepted, true);
assert.equal(result.matched, true);
assert.deepEqual(result.virtualVehicles, []);
assert.deepEqual(await aggregator.getVirtualVehiclesForRoute(1), []);
});
test('nearby points from distinct sessions create a virtual crowdsourced vehicle', async () => {
const aggregator = createAggregator();
await ingestCluster(aggregator);
const vehicles = await aggregator.getVirtualVehiclesForRoute(1);
assert.equal(vehicles.length, 1);
assert.match(vehicles[0].vehicle_id, /^crowd:1:/);
assert.equal(vehicles[0].source, 'crowdsourced');
assert.equal(vehicles[0].source_ref, null);
assert.equal(vehicles[0].is_estimated, true);
assert.equal('session_id' in vehicles[0], false);
assert.equal('session_key' in vehicles[0], false);
});
test('poor accuracy lowers crowdsourced confidence', async () => {
const goodAggregator = createAggregator();
await ingestCluster(goodAggregator, 15);
const [goodVehicle] = await goodAggregator.getVirtualVehiclesForRoute(1);
const poorAggregator = createAggregator();
await ingestCluster(poorAggregator, 150);
const [poorVehicle] = await poorAggregator.getVirtualVehiclesForRoute(1);
assert.ok(goodVehicle.confidence > poorVehicle.confidence);
assert.equal(poorVehicle.source, 'crowdsourced');
});
test('points outside route geometry are accepted but ignored', async () => {
const aggregator = createAggregator();
const result = await aggregator.ingest({
session_id: 'session-a',
latitude: 56.0,
longitude: 29.5,
accuracy_m: 20,
observed_at: '2026-05-20T12:29:55Z',
route_id: 1
});
assert.equal(result.accepted, true);
assert.equal(result.matched, false);
assert.deepEqual(await aggregator.getVirtualVehiclesForRoute(1), []);
});
test('official telemetry suppresses nearby crowdsourced virtual vehicle', async () => {
const aggregator = createAggregator();
await ingestCluster(aggregator);
const [vehicle] = await aggregator.getVirtualVehiclesForRoute(1);
assert.ok(vehicle);
const suppressed = await aggregator.getVirtualVehiclesForRoute(1, {
officialVehicles: [{
vehicle_id: 99,
latitude: vehicle.latitude,
longitude: vehicle.longitude,
is_online: true
}]
});
assert.deepEqual(suppressed, []);
});

View File

@@ -0,0 +1,110 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const { PassengerTelemetryController } = require('../src/controllers/passengerTelemetryController');
const { decorateOfficialVehicle } = require('../src/services/liveVehicleMapper');
function createResponse() {
return {
statusCode: null,
body: null,
status(code) {
this.statusCode = code;
return this;
},
json(payload) {
this.body = payload;
return this;
}
};
}
test('POST passenger telemetry ingest returns accepted response and broadcasts virtual vehicles', async () => {
const broadcasts = [];
const service = {
async ingest(payload) {
assert.equal(payload.session_id, 'temporary-anonymous-id');
return {
accepted: true,
routeId: 12,
virtualVehicles: [
{
vehicle_id: 'crowd:12:abc123',
route_id: 12,
source: 'crowdsourced',
source_ref: null,
is_estimated: true
}
]
};
}
};
const controller = new PassengerTelemetryController({
service,
broadcastToRoute: (routeId, message) => broadcasts.push({ routeId, message })
});
controller.getRouteSnapshot = async () => [
{
vehicle_id: 'crowd:12:abc123',
route_id: 12,
source: 'crowdsourced',
source_ref: null,
is_estimated: true
}
];
const res = createResponse();
await controller.ingest({
body: {
session_id: 'temporary-anonymous-id',
latitude: 55.485,
longitude: 28.768
}
}, res);
assert.equal(res.statusCode, 202);
assert.deepEqual(res.body, { accepted: true });
assert.equal(broadcasts.length, 1);
assert.equal(broadcasts[0].routeId, 12);
assert.equal(broadcasts[0].message.type, 'snapshot:route');
assert.equal(broadcasts[0].message.vehicles[0].source, 'crowdsourced');
assert.equal('session_id' in broadcasts[0].message.vehicles[0], false);
});
test('passenger telemetry controller maps validation failures to 400', async () => {
const service = {
async ingest() {
const error = new Error('latitude must be between -90 and 90');
error.status = 400;
throw error;
}
};
const controller = new PassengerTelemetryController({
service,
broadcastToRoute: () => {}
});
const res = createResponse();
await controller.ingest({ body: {} }, res);
assert.equal(res.statusCode, 400);
assert.deepEqual(res.body, { error: 'latitude must be between -90 and 90' });
});
test('official live vehicle mapping adds optional realtime fields', () => {
const vehicle = decorateOfficialVehicle({
vehicle_id: 7,
vehicle_number: 'DEMO-7A',
route_id: 12,
latitude: '55.485',
longitude: '28.768',
accuracy_meters: '5.5',
last_seen_at: '2026-05-20T12:29:58Z'
});
assert.equal(vehicle.source, 'official');
assert.equal(vehicle.confidence, 1);
assert.equal(vehicle.accuracy_m, 5.5);
assert.equal(vehicle.is_estimated, false);
});

View File

@@ -0,0 +1,70 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const { validatePassengerTelemetryPayload } = require('../src/services/passengerTelemetryValidator');
const NOW = new Date('2026-05-20T12:30:00Z');
test('validates and normalizes passenger telemetry payload', () => {
const payload = validatePassengerTelemetryPayload({
session_id: ' temporary-anonymous-id ',
latitude: '55.485',
longitude: '28.768',
accuracy_m: '18.5',
speed_kmh: '32',
heading: '91',
observed_at: '2026-05-20T12:29:30Z',
route_id: '12',
stop_id: '123',
app_state: 'foreground'
}, { now: NOW });
assert.equal(payload.sessionId, 'temporary-anonymous-id');
assert.equal(payload.latitude, 55.485);
assert.equal(payload.longitude, 28.768);
assert.equal(payload.accuracyM, 18.5);
assert.equal(payload.speedKmh, 32);
assert.equal(payload.heading, 91);
assert.equal(payload.routeId, 12);
assert.equal(payload.stopId, 123);
assert.equal(payload.appState, 'foreground');
});
test('rejects invalid latitude and longitude', () => {
assert.throws(() => validatePassengerTelemetryPayload({
session_id: 's1',
latitude: 91,
longitude: 28.768
}, { now: NOW }), /latitude must be between -90 and 90/);
assert.throws(() => validatePassengerTelemetryPayload({
session_id: 's1',
latitude: 55.485,
longitude: 181
}, { now: NOW }), /longitude must be between -180 and 180/);
});
test('rejects stale and far-future observations', () => {
assert.throws(() => validatePassengerTelemetryPayload({
session_id: 's1',
latitude: 55.485,
longitude: 28.768,
observed_at: '2026-05-20T12:10:00Z'
}, { now: NOW }), /observed_at is too old/);
assert.throws(() => validatePassengerTelemetryPayload({
session_id: 's1',
latitude: 55.485,
longitude: 28.768,
observed_at: '2026-05-20T12:40:00Z'
}, { now: NOW }), /observed_at is too far in the future/);
});
test('rejects implausible city transit speed', () => {
assert.throws(() => validatePassengerTelemetryPayload({
session_id: 's1',
latitude: 55.485,
longitude: 28.768,
speed_kmh: 180
}, { now: NOW }), /speed_kmh must be between 0 and 120/);
});