mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(roadtrip): a booked ferry is driven to, and one without a date says so
A ferry between two stops without a clock was seated by the clock alone, which files a ride with nothing timed before it at the end of the day. The desktop day list stored that slot, and the road trip drove overland to the far shore and back to the pier before the crossing. A ride that leaves and lands on the same day, with both terminals located, now takes the seat between the clocks that adds the fewest straight-line kilometres. One rule in shared seats it for the day list, the slot the desktop stores, the phone and PDF lists and the road trip in the browser and on the server. Slots already stored are left alone. A flight, train, ferry, cruise or bus on no day made no seam and said nothing, while the map still drew its arc. The rail and the phone tab now name such a booking with a way to open it, and calculate_roadtrip lists it under undatedRides.
This commit is contained in:
@@ -6,8 +6,10 @@
|
||||
* column the statement never selects would go unnoticed there. These run the statement.
|
||||
*/
|
||||
import { db } from '../../../src/db/database';
|
||||
import type { McpContext } from '../../../src/nest-mcp';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { RoadtripPlanService } from '../../../src/nest/roadtrip/roadtrip-plan.service';
|
||||
import { RoadtripPlanningMcp } from '../../../src/nest/roadtrip/roadtrip-planning.mcp';
|
||||
import { createDay, createDayAccommodation, createDayAssignment, createPlace, createTrip, createUser } from '../../helpers/factories';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
|
||||
@@ -177,6 +179,10 @@ describe('a booking the traveller rides (#2428)', () => {
|
||||
it('seats the terminals in the day and never asks the router for the ride', async () => {
|
||||
const { user, trip, visits, plans } = setup();
|
||||
db.prepare("UPDATE day_assignments SET assignment_time = '10:00' WHERE id = ?").run(visits[1].id);
|
||||
// Pinned to the minute the drive reaches it anyway, so the clock alone seats the
|
||||
// flight here. Untimed, Celle lies south of Lueneburg with the airport north of both,
|
||||
// and the flight would go where it adds the least road (#2461, see below).
|
||||
db.prepare("UPDATE day_assignments SET assignment_time = '15:30' WHERE id = ?").run(visits[2].id);
|
||||
const day = db.prepare('SELECT day_id FROM day_assignments WHERE id = ?').get(visits[0].id) as { day_id: number };
|
||||
const flightId = withFlight(day.day_id, trip.id);
|
||||
|
||||
@@ -185,7 +191,7 @@ describe('a booking the traveller rides (#2428)', () => {
|
||||
expect(context.carriers).toHaveLength(1);
|
||||
expect(context.carriers[0].endpoints).toHaveLength(2);
|
||||
const card = calculated.days[0];
|
||||
// Behind Lueneburg (10:00), before the untimed Celle: the day plan's own seat.
|
||||
// Behind Lueneburg (10:00), before Celle (15:30): the day plan's own seat.
|
||||
expect(card.stops.map((s) => s.carrier?.role ?? s.name)).toEqual(['Hamburg', 'Lueneburg', 'departure', 'arrival', 'Celle']);
|
||||
const departure = card.stops[2];
|
||||
expect(departure.carrier).toMatchObject({ reservationId: flightId, type: 'flight', code: 'HAM', at: '13:20' });
|
||||
@@ -266,3 +272,89 @@ describe('a booking the traveller rides (#2428)', () => {
|
||||
expect(calculated.totalStops).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The crossing from the report behind #2461: Amsterdam and Newcastle on one day, neither
|
||||
* with a clock, and a ferry from IJmuiden to the Port of Tyne. A day sailing here, so the
|
||||
* drive on from the far pier ends before midnight and the one-day trip stays one card.
|
||||
*/
|
||||
describe('a ferry across the day, and one on no day (#2461)', () => {
|
||||
function crossing() {
|
||||
const { user } = createUser(db);
|
||||
const trip = createTrip(db, user.id);
|
||||
const day = createDay(db, trip.id);
|
||||
for (const [name, lat, lng] of [
|
||||
['Amsterdam', 52.3731, 4.8926],
|
||||
['Newcastle', 54.9783, -1.6178],
|
||||
] as const) {
|
||||
createDayAssignment(db, day.id, createPlace(db, trip.id, { name, lat, lng }).id);
|
||||
}
|
||||
const router = hourlyRouter();
|
||||
const plans = new RoadtripPlanService(
|
||||
new DatabaseService(db),
|
||||
{ getUserSettings: () => ({}) } as never,
|
||||
{ read: () => ({}) } as never,
|
||||
router as never,
|
||||
{ listForTrip: () => [], tracksForTrip: () => [] } as never,
|
||||
{ list: () => [] } as never,
|
||||
);
|
||||
return { user, trip, day, router, plans };
|
||||
}
|
||||
|
||||
function withFerry(tripId: number, dayId: number | null, title: string, located = true): number {
|
||||
const id = Number(
|
||||
db
|
||||
.prepare(
|
||||
`INSERT INTO reservations (trip_id, title, type, day_id, end_day_id, reservation_time, reservation_end_time)
|
||||
VALUES (?, ?, 'ferry', ?, ?, '09:30', '15:00')`,
|
||||
)
|
||||
.run(tripId, title, dayId, dayId).lastInsertRowid,
|
||||
);
|
||||
if (located) {
|
||||
const endpoint = db.prepare(
|
||||
'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
);
|
||||
endpoint.run(id, 'from', 0, 'IJmuiden', null, 52.4581, 4.5879);
|
||||
endpoint.run(id, 'to', 1, 'Port of Tyne', null, 54.9925, -1.4522);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
it('drives to the pier and on from the far one, and never overland between the two shores', async () => {
|
||||
const { user, trip, day, router, plans } = crossing();
|
||||
withFerry(trip.id, day.id, 'IJmuiden to Newcastle');
|
||||
|
||||
const { calculated, undatedRides } = await plans.calculate(trip.id, user.id);
|
||||
|
||||
const card = calculated.days[0];
|
||||
// By the clock alone the ferry closed the day: Amsterdam to Newcastle overland, back
|
||||
// to IJmuiden, and only then the crossing.
|
||||
expect(card.stops.map((s) => s.carrier?.role ?? s.name)).toEqual(['Amsterdam', 'departure', 'arrival', 'Newcastle']);
|
||||
expect(card.legs.map((l) => l?.mode)).toEqual(['driving', 'ferry', 'driving']);
|
||||
// Two roads, one to each side of the crossing, and nothing between the two cities.
|
||||
expect(router.route).toHaveBeenCalledTimes(2);
|
||||
expect(card.distance).toBe(120_000);
|
||||
expect(undatedRides).toEqual([]);
|
||||
});
|
||||
|
||||
it('names a located ride on no day, and the tool answers with the same list', async () => {
|
||||
const { user, trip, day, plans } = crossing();
|
||||
const undated = withFerry(trip.id, null, 'Ferry without a date');
|
||||
withFerry(trip.id, day.id, 'Ferry on the day');
|
||||
withFerry(trip.id, null, 'Ferry without terminals', false);
|
||||
db.prepare("INSERT INTO reservations (trip_id, title, type) VALUES (?, 'Dinner', 'restaurant')").run(trip.id);
|
||||
|
||||
const plan = await plans.calculate(trip.id, user.id);
|
||||
|
||||
expect(plan.undatedRides).toEqual([{ id: undated, type: 'ferry', title: 'Ferry without a date' }]);
|
||||
// It is on the drive nowhere, which is why it is named.
|
||||
expect(plan.context.carriers.map((c) => c.id)).not.toContain(undated);
|
||||
expect(plan.calculated.days.flatMap((d) => d.stops).some((s) => s.carrier?.reservationId === undated)).toBe(false);
|
||||
|
||||
const tool = new RoadtripPlanningMcp(plans, {} as never, {} as never);
|
||||
const answer = await tool.calculate({ tripId: trip.id, includeGeometry: false }, { userId: user.id } as McpContext);
|
||||
const body = JSON.parse(answer.content[0].text as string);
|
||||
expect(body.undatedRides).toEqual(plan.undatedRides);
|
||||
expect(body.complete).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -241,6 +241,29 @@ describe('browser-independent roadtrip calculation', () => {
|
||||
expect(day).not.toHaveProperty('legLines');
|
||||
expect(day).not.toHaveProperty('arrivingLine');
|
||||
});
|
||||
it('answers calculate_roadtrip with the rides on no day, an empty list when there are none (#2461)', async () => {
|
||||
// Additive: the rest of the answer is what it was. The rides themselves are read off the
|
||||
// real tables in roadtrip-plan.service.test.ts; this pins the field and the words for it.
|
||||
const s = setup();
|
||||
const mcp = new RoadtripPlanningMcp(s.plans, {} as never, {} as never);
|
||||
const body = JSON.parse((await mcp.calculate({ tripId: 10, includeGeometry: false }, ctx)).content[0].text as string);
|
||||
expect(body.undatedRides).toEqual([]);
|
||||
expect(body.days.length).toBeGreaterThan(0);
|
||||
// And the tool says what the field is, or an assistant has no reason to read it.
|
||||
const addons = { isAddonEnabled: vi.fn(() => true) };
|
||||
const registry = createTestRegistry([new RoadtripPlanningMcp(s.plans, {} as never, addons as never)], {
|
||||
accessPolicy: trekMcpAccessPolicy,
|
||||
validateAccess: trekMcpValidateAccess,
|
||||
});
|
||||
const described = new Map<string, string>();
|
||||
const registrar = {
|
||||
registerTool: (name: string, config: { description?: string }) => {
|
||||
described.set(name, config.description ?? '');
|
||||
},
|
||||
};
|
||||
registry.attach(registrar as never, { ...ctx, scopes: ['trips:read'] });
|
||||
expect(described.get('calculate_roadtrip')).toContain('undatedRides');
|
||||
});
|
||||
it('keeps explicit end-day visits and manual boundaries in the shared planning path', async () => {
|
||||
const s = setup();
|
||||
s.visits[0].end_day = 1;
|
||||
|
||||
Reference in New Issue
Block a user