feat: enhance Telegram import functionality to support delays and cancellations
- Updated telegramCancellationParser to include parsing for delay notices. - Added new test cases for handling delay messages and stop-point cancellations. - Modified telegramImport integration tests to validate new delay handling. - Enhanced documentation to reflect the addition of delay notifications. - Updated TelegramImportPanel component to accommodate new alert types. - Introduced new fixture files for testing delay and cancellation messages.
This commit is contained in:
@@ -2,7 +2,7 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { parseCancellation, resolveCancellationStops, matchCancellationRoutes, buildCancellationAlert, normalizeStopKey, dayBounds } = require('../src/services/telegramCancellationParser');
|
||||
const { parseTransitNotice, buildTransitNoticeAlert, parseCancellation, resolveCancellationStops, matchCancellationRoutes, buildCancellationAlert, normalizeStopKey, dayBounds } = require('../src/services/telegramCancellationParser');
|
||||
const hospitalText = fs.readFileSync(path.join(__dirname, 'fixtures/telegram-hospital-cancellation.txt'), 'utf8');
|
||||
|
||||
const text = `Уважаемые пассажиры!
|
||||
@@ -128,8 +128,8 @@ test('dictionary selects the actual stop ID and uses its canonical name in conci
|
||||
const matched = matchCancellationRoutes(parsed, routes);
|
||||
assert.deepEqual(matched.plans[0].routeIds, [1, 2, 3]);
|
||||
const alert = buildCancellationAlert(matched.plans[0], '2026-10-02', 'https://t.me/avp2Polotsk/1166', '2026-10-02T12:52Z', 1);
|
||||
assert.equal(alert.message, '02.10.2026: не выполняются рейсы:\nАвтовокзал — 15:43, 16:43\nЗодчего Иоанна — 15:58, 16:58\nИсточник: https://t.me/avp2Polotsk/1166');
|
||||
assert.doesNotMatch(alert.message, /Уважаемые|С УВАЖЕНИЕМ/);
|
||||
assert.equal(alert.message, '02.10.2026: не выполняются рейсы:\nАвтовокзал — 15:43, 16:43\nЗодчего Иоанна — 15:58, 16:58');
|
||||
assert.doesNotMatch(alert.message, /Уважаемые|С УВАЖЕНИЕМ|Источник:/);
|
||||
assert.equal(normalizeStopKey(' « ЗОД-ЧЕГО » '), normalizeStopKey('Зодчего'));
|
||||
});
|
||||
|
||||
@@ -163,3 +163,117 @@ test('a shared explicit departure list may name several route numbers', () => {
|
||||
assert.ok(parsed.groups.every(g => g.departures[0].times.join(',') === '18:00,19:00'));
|
||||
}
|
||||
});
|
||||
|
||||
const delayText = fs.readFileSync(path.join(__dirname, 'fixtures/telegram-delay-route-4.txt'), 'utf8');
|
||||
const stopPointText = fs.readFileSync(path.join(__dirname, 'fixtures/telegram-stop-point-cancellation.txt'), 'utf8');
|
||||
const route4Stops = [{ id: 41, name: 'Мариненко' }, { id: 42, name: 'Боровуха' }];
|
||||
const route4 = [{ id: 4, route_number: '4' }, { id: 40, route_number: '4' }];
|
||||
|
||||
function route4Alert(parsed, publishedAt) {
|
||||
const resolved = resolveCancellationStops(parsed, route4Stops, []);
|
||||
const matched = matchCancellationRoutes(resolved, route4);
|
||||
assert.deepEqual(matched.reasons, []);
|
||||
return buildTransitNoticeAlert(matched.plans, parsed.serviceDate, 'https://t.me/avp2Polotsk/1700', publishedAt, 1, parsed);
|
||||
}
|
||||
|
||||
test('screenshot delay is a delay notice with both departures and Minsk publication-day expiry', () => {
|
||||
const publishedAt = '2026-10-08T16:14:01Z';
|
||||
const parsed = parseTransitNotice({ text: delayText, publishedAt });
|
||||
assert.equal(parsed.status, 'ready');
|
||||
assert.equal(parsed.alertType, 'delay');
|
||||
assert.equal(parsed.serviceDate, '2026-10-08');
|
||||
assert.deepEqual(parsed.groups[0].departures.map(d => d.times), [['19:05'], ['20:00']]);
|
||||
const alert = route4Alert(parsed, publishedAt);
|
||||
assert.equal(alert.alert_type, 'delay');
|
||||
assert.deepEqual(alert.route_numbers, ['4']);
|
||||
assert.equal(alert.title, 'Задержка отдельных рейсов №4');
|
||||
assert.equal(alert.message, '08.10.2026: с опозданием выполняются рейсы:\nМариненко — 19:05\nБоровуха — 20:00');
|
||||
assert.equal(alert.start_time, '2026-10-08T16:14:01.000Z');
|
||||
assert.equal(alert.end_time, '2026-10-08T21:00:00.000Z');
|
||||
});
|
||||
|
||||
test('screenshot stop-point heading and quoted continuation preserve four cancelled departures', () => {
|
||||
const publishedAt = '2026-10-08T02:03:13Z';
|
||||
const parsed = parseTransitNotice({ text: stopPointText, publishedAt, editedAt: '2026-10-09T01:00Z' }, { preserveSourceStops: true });
|
||||
assert.equal(parsed.status, 'ready');
|
||||
assert.equal(parsed.alertType, 'cancellation');
|
||||
assert.equal(parsed.serviceDate, '2026-10-08');
|
||||
assert.deepEqual(parsed.groups, [{ routeNumber: '4', departures: [
|
||||
{ stop: 'Мариненко', sourceStop: 'Мариненко', times: ['06:00', '07:30'] },
|
||||
{ stop: 'Боровуха', sourceStop: 'Боровуха', times: ['06:40', '08:15'] }
|
||||
] }]);
|
||||
const alert = route4Alert(parsed, publishedAt);
|
||||
assert.equal(alert.alert_type, 'cancellation');
|
||||
assert.equal(alert.message, '08.10.2026: не выполняются рейсы:\nМариненко — 06:00, 07:30\nБоровуха — 06:40, 08:15');
|
||||
assert.equal(alert.end_time, '2026-10-08T21:00:00.000Z');
|
||||
});
|
||||
|
||||
test('stop-point heading supports quote variants, mixed prefixes and real dates independently from times', () => {
|
||||
for (const [opening, closing] of [['"', '"'], ['«', '»'], ['“', '”']]) {
|
||||
const text = stopPointText.replace(/"(Мариненко|Боровуха)"/g, (_, name) => opening + name + closing).replace('6.00', '09.10');
|
||||
const parsed = parseTransitNotice({ text: 'На 09.10\n' + text, publishedAt: '2026-10-08T02:03:13Z' });
|
||||
assert.equal(parsed.status, 'ready');
|
||||
assert.equal(parsed.serviceDate, '2026-10-09');
|
||||
assert.deepEqual(parsed.groups[0].departures[0].times, ['09:10', '07:30']);
|
||||
}
|
||||
const mixed = stopPointText.replace('"Боровуха"', 'ОТ ОСТАНОВКИ «Боровуха»');
|
||||
assert.equal(parseTransitNotice({ text: mixed, publishedAt: '2026-10-08T02:03:13Z' }).status, 'ready');
|
||||
for (const text of [stopPointText.replace('6.00', '25.00'), 'На 31.02.2026\n' + stopPointText,
|
||||
stopPointText.replace('с остановочного пункта', ''), stopPointText + '\nДополнительно в 09:00']) {
|
||||
assert.equal(parseTransitNotice({ text, publishedAt: '2026-10-08T02:03:13Z' }).status, 'review');
|
||||
}
|
||||
});
|
||||
|
||||
test('possible delay stays possible and unspecified duration is never invented or silently dropped', () => {
|
||||
const publishedAt = '2026-10-08T16:14:01Z';
|
||||
for (const wording of ['могут выполняться рейсы с опозданием', 'рейсы могут прибыть с опозданием', 'рейсы могут прийти с опозданием']) {
|
||||
const parsed = parseTransitNotice({ text: delayText.replace('будут выполняться рейсы с опозданием', wording), publishedAt });
|
||||
assert.equal(parsed.status, 'ready');
|
||||
assert.equal(parsed.delayPossible, true);
|
||||
const alert = route4Alert(parsed, publishedAt);
|
||||
assert.match(alert.title, /Возможное опоздание/);
|
||||
assert.match(alert.message, /возможны опоздания рейсов/);
|
||||
assert.doesNotMatch(alert.message, /минут|час/);
|
||||
}
|
||||
for (const duration of [' на 20 минут', ' на 20 мин.', ' на 1 час']) {
|
||||
const parsed = parseTransitNotice({ text: delayText.replace('с опозданием:', 'с опозданием' + duration + ':'), publishedAt });
|
||||
assert.equal(parsed.status, 'review');
|
||||
assert.equal(parsed.alertType, 'delay');
|
||||
assert.match(parsed.reasons[0], /величина опоздания/);
|
||||
}
|
||||
});
|
||||
|
||||
test('mixed types, negated delay, conditions and incomplete departures stay on review; unrelated execution is ignored', () => {
|
||||
const publishedAt = '2026-10-08T16:14:01Z';
|
||||
for (const text of [delayText + '\n' + stopPointText,
|
||||
delayText.replace('будут выполняться', 'не могут выполняться'),
|
||||
delayText + '\nДвижение восстановлено.',
|
||||
delayText.replace(' НА 20.00', ''),
|
||||
delayText + '\nОстальные рейсы будут выполняться.']) {
|
||||
assert.equal(parseTransitNotice({ text, publishedAt }).status, 'review', text);
|
||||
}
|
||||
assert.equal(parseTransitNotice({ text: 'Будут выполняться рейсы по маршруту №4.', publishedAt }).status, 'ignored');
|
||||
assert.equal(parseTransitNotice({ text: 'С опозданием поздравляем с праздником!', publishedAt }).status, 'ignored');
|
||||
const withMedia = parseTransitNotice({ text: delayText, publishedAt, hasMedia: true });
|
||||
assert.equal(withMedia.status, 'review');
|
||||
assert.equal(withMedia.alertType, 'delay');
|
||||
});
|
||||
|
||||
test('multiple delayed routes are combined while every stop and departure remains visible', () => {
|
||||
const publishedAt = '2026-10-08T16:14:01Z';
|
||||
const parsed = parseTransitNotice({ text: delayText + '\nПО МАРШРУТУ №27 С ОСТАНОВКИ АВ/ВОКЗАЛ НА 20.15,21.00.', publishedAt });
|
||||
const resolved = resolveCancellationStops(parsed, [...route4Stops, { id: 1, name: 'Автовокзал' }], []);
|
||||
const matched = matchCancellationRoutes(resolved, [...route4, { id: 27, route_number: '27' }]);
|
||||
const alert = buildTransitNoticeAlert(matched.plans, parsed.serviceDate, '', publishedAt, 1, parsed);
|
||||
assert.equal(alert.alert_type, 'delay');
|
||||
assert.deepEqual(alert.route_numbers, ['4', '27']);
|
||||
assert.match(alert.message, /Маршрут №4:\nМариненко — 19:05\nБоровуха — 20:00/);
|
||||
assert.match(alert.message, /Маршрут №27:\nАвтовокзал — 20:15, 21:00/);
|
||||
});
|
||||
|
||||
test('calendar words in a stop name are not mistaken for a service date', () => {
|
||||
const parsed = parseTransitNotice({ text: stopPointText.replace('Мариненко', '1 Мая'), publishedAt: '2026-10-08T02:03:13Z' });
|
||||
assert.equal(parsed.status, 'ready');
|
||||
assert.equal(parsed.serviceDate, '2026-10-08');
|
||||
assert.equal(parsed.groups[0].departures[0].stop, '1 Мая');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user