test(calendar): cover the window paths the split added

The domain branch ratchet went from 93 to 92.3: the new hand-over code brought
branches with no case behind them. Three cases and one simplification, no
threshold touched.

Covered: a split rental whose endpoints carry no IANA zone and fall back to
their coordinates; a bare end clock, which resolves against the start date and
therefore does NOT split; and an unsplit rental with a day but no clock, whose
hand-over has nothing to place and correctly emits nothing.

The metadata parse now happens once, inside describeReservation, rather than
three times. The two extra copies were only there to hand `meta` to it, and one
of their arms is unreachable from SQLite, where the column is always text.
@
This commit is contained in:
Maurice
2026-08-27 17:59:10 +02:00
committed by Maurice
parent bbe4d62c90
commit 88c34f6415
2 changed files with 53 additions and 5 deletions
+4 -5
View File
@@ -503,13 +503,12 @@ export class CalendarService {
if (windowSplit.has(r.id)) continue;
const timeLines = buildReservationTimeLines(r);
if (!timeLines) continue;
const meta = r.metadata ? (typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata) : {};
let ev = `BEGIN:VEVENT\r\nUID:${uid(r.id, 'res')}\r\nDTSTAMP:${now}\r\n`;
ev += timeLines;
ev += `SUMMARY:${esc(r.title)}\r\n`;
const desc = describeReservation(r, meta);
const desc = describeReservation(r);
if (desc) ev += `DESCRIPTION:${esc(desc)}\r\n`;
if (r.location) ev += `LOCATION:${esc(r.location)}\r\n`;
ev += `END:VEVENT\r\n`;
@@ -520,7 +519,8 @@ export class CalendarService {
// event loop because a window booking's two hand-over events carry the same
// lines: dropping the block must not drop the route or the confirmation with
// it (#2068).
function describeReservation(r: any, meta: any): string {
function describeReservation(r: any): string {
const meta = r.metadata ? (typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata) : {};
let desc = r.type ? `Type: ${r.type}` : '';
if (r.confirmation_number) desc += `\nConfirmation: ${r.confirmation_number}`;
if (meta.airline) desc += `\nAirline: ${meta.airline}`;
@@ -627,8 +627,7 @@ export class CalendarService {
if (!split && r.type !== 'car') continue;
const { start, end } = split ?? windowSidesOf(r);
const meta = r.metadata ? (typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata) : {};
const desc = split ? describeReservation(r, meta) : '';
const desc = split ? describeReservation(r) : '';
const handover = (kind: 'pickup' | 'dropoff', side: WindowSide, summary: string) => {
let ev = `BEGIN:VEVENT\r\nUID:${uid(r.id, kind)}\r\nDTSTAMP:${now}\r\n`;
@@ -1192,6 +1192,55 @@ describe('car rentals', () => {
expect(ics).toContain('DTEND;VALUE=DATE:20260705');
expect(ics).not.toContain('Pickup: Night Bus');
});
it('CAL-052: a split rental whose endpoints carry no zone falls back to their coordinates', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Road Trip' });
const rental = createReservation(testDb, trip.id, { title: 'Europcar', type: 'car' });
testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL WHERE id=?').run(rental.id);
// An older import that geocoded both ends but stored no IANA zone.
insertEndpoint(rental.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, null, '09:00', '2026-07-07');
insertEndpoint(rental.id, 'to', 1, 'Berlin Office', 52.5, 13.4, null, '10:30', '2026-07-14');
const { ics } = svc.exportICS(trip.id);
expect(ics).toContain('DTSTART;TZID=Europe/Paris:20260707T090000');
expect(ics).toContain('DTSTART;TZID=Europe/Berlin:20260714T103000');
});
// reservation_end_time is frequently a bare clock next to the booking's own
// date, which puts both hand-overs on the same day — so it is one sitting.
it('CAL-053: a bare end clock resolves against the start date rather than splitting', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Road Trip' });
const parking = createReservation(testDb, trip.id, { title: 'Street Bay', type: 'parking' });
testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?')
.run('2026-07-01T08:00', '18:30', parking.id);
const { ics } = svc.exportICS(trip.id);
expect(ics).toContain('SUMMARY:Street Bay\r\n');
expect(ics).not.toContain('Drop-off: Street Bay');
expect(ics).not.toContain('Pickup: Street Bay');
});
it('CAL-054: an unsplit rental with a day but no clock emits no hand-over of its own', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Road Trip' });
const day = createDay(testDb, trip.id, { date: '2026-07-03' });
const rental = createReservation(testDb, trip.id, { title: 'Dayless', type: 'car' });
// A day but no end day and no clock: one side resolves, and it has no time,
// so there is nothing to place — the all-day block carries it instead.
testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL, day_id=?, end_day_id=NULL WHERE id=?')
.run(day.id, rental.id);
const { ics } = svc.exportICS(trip.id);
expect(ics).toContain('SUMMARY:Dayless\r\n');
expect(ics).toContain('DTSTART;VALUE=DATE:20260703');
expect(ics).not.toContain('Pickup: Dayless');
expect(ics).not.toContain('Drop-off: Dayless');
});
});
describe('folded quirk branches', () => {