fix(mcp): keep transit out of the transport create enum

The transport form's picker has nine entries, not ten: a transit booking
carries a provider itinerary in metadata.transit and create_transit_journey
is what writes one, so letting a caller hand-make a bare transit row
produces a shape the transit UI does not expect.

Splits the one list in two - what may be asked for, and what counts as a
transport for the update gate - so a stored transit booking stays editable
through update_transport, which is where it was already reachable.

Also reports the ids set_reservation_travelers dropped instead of
returning a plain success with a shorter list.
This commit is contained in:
Maurice
2026-08-28 12:05:34 +02:00
committed by Maurice
parent 86c0c197db
commit aa5002b2ed
7 changed files with 87 additions and 16 deletions
@@ -592,6 +592,23 @@ describe('Tool: set_reservation_travelers', () => {
});
const data = parseToolResult(result) as any;
expect(data.travelers.map((t: any) => t.user_id)).toEqual([owner.id]);
// A caller that guessed an id for a name it could not resolve is told so
// rather than getting a plain success with a shorter list.
expect(data.ignored_user_ids).toEqual([outsider.id]);
});
});
it('says nothing about ignored ids when every id was on the trip', async () => {
const { user: owner } = createUser(testDb);
const trip = createTrip(testDb, owner.id);
await withHarness(owner.id, async (h) => {
const reservationId = await makeBooking(h, trip.id);
const result = await h.client.callTool({
name: 'set_reservation_travelers',
arguments: { tripId: trip.id, reservationId, user_ids: [owner.id] },
});
const data = parseToolResult(result) as any;
expect(data.ignored_user_ids).toBeUndefined();
});
});
+33 -3
View File
@@ -1052,11 +1052,11 @@ describe('Tool: update_transport (multi-leg)', () => {
/** client/src/components/Planner/TransportModal.tsx, in the picker's order. */
const PICKER_TRANSPORT_TYPES = [
'flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transit', 'transport_other',
'flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transport_other',
] as const;
/** The six the tools used to reject outright. */
const NEWLY_ACCEPTED = ['bus', 'taxi', 'bicycle', 'ferry', 'transit', 'transport_other'] as const;
/** The five the tools used to reject outright. */
const NEWLY_ACCEPTED = ['bus', 'taxi', 'bicycle', 'ferry', 'transport_other'] as const;
describe('Transport tools: the full type list', () => {
it.each(PICKER_TRANSPORT_TYPES)('create_transport accepts %s', async (type) => {
@@ -1172,6 +1172,36 @@ describe('Transport tools: the full type list', () => {
});
});
it('refuses to hand-make a transit booking, which create_transit_journey owns', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'create_transport',
arguments: { tripId: trip.id, type: 'transit', title: 'Tram 4' },
});
expect((result as { isError?: boolean }).isError).toBe(true);
});
});
it('still lets update_transport reach a stored transit booking', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
// As create_transit_journey leaves it: a transport row carrying an itinerary.
const reservationId = Number(testDb.prepare(
"INSERT INTO reservations (trip_id, title, type, status) VALUES (?, 'Tram 4', 'transit', 'pending')"
).run(trip.id).lastInsertRowid);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_transport',
arguments: { tripId: trip.id, reservationId, status: 'confirmed' },
});
expect((result as { isError?: boolean }).isError).toBeFalsy();
const row = testDb.prepare('SELECT status, type FROM reservations WHERE id = ?').get(reservationId) as any;
expect(row).toMatchObject({ status: 'confirmed', type: 'transit' });
});
});
it('still refuses legs on a mode whose form never writes them', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);