fix(tours): a recorded track is neither turned round nor closed by update_tour

A GPX import keeps no points of its own, and the tour read makes up a start
and an end from its line. update_tour with reshape turned or closed those two
made up points, so the recorded line was replaced by a router line between its
ends. A reshape of a tour without points of its own is now refused with the
recorded track wording, before anything is planned or saved.
This commit is contained in:
Maurice
2026-10-24 01:53:51 +00:00
parent 508e17bc11
commit 1a941a6f1d
2 changed files with 34 additions and 6 deletions
+8 -5
View File
@@ -137,11 +137,14 @@ function repointingRefusal(saved: readonly TourWaypoint[], points: readonly Hike
return null;
}
/** The change with the saved points reshaped as its new points, or refused with why (a hike only). */
function withReshape(request: TourUpdateRequest, saved: readonly TourWaypoint[], type: string): TourUpdateRequest {
/**
* The change with the saved points reshaped as its new points, or refused with why (a hike only). A
* recorded track has no points of its own to turn: the two its read gives are made up from its line's ends.
*/
function withReshape(request: TourUpdateRequest, { tour, waypoints }: TourDetailResponse): TourUpdateRequest {
const { reshape, ...rest } = request;
if (!reshape || type !== 'hike') return rest;
const reshaped = reshapedHikePoints(saved, reshape);
if (!reshape || tour.tour_type !== 'hike') return rest;
const reshaped = reshapedHikePoints(tour.has_waypoints === true ? waypoints : [], reshape);
if ('refused' in reshaped) throw badRequest(reshaped.refused);
return { ...rest, waypoints: reshaped.points };
}
@@ -395,7 +398,7 @@ export class HikePlanService {
const { placeId } = asked;
const saved = await this.tours.getTour(String(tripId), String(placeId));
// PR1-01, PR1-02: a reshape is the saved points turned or closed, then planned as new points are.
const request = asked.reshape ? withReshape(asked, saved.waypoints, saved.tour.tour_type) : asked;
const request = asked.reshape ? withReshape(asked, saved) : asked;
const { tour } = saved;
if (tour.tour_type !== 'hike') {
throw badRequest(
@@ -4,12 +4,15 @@
* points are. The way is planned by a stub of HikeRoutingService; the write
* is the real one over a snapshot test db.
*/
import { TourWaypoints } from '../../../src/db/entities/TourWaypoints.entity';
import { Tours } from '../../../src/db/entities/Tours.entity';
import type { McpContext } from '../../../src/nest-mcp';
import { HikePlanMcp } from '../../../src/nest/tours/hike-plan.mcp';
import { HikeRoutingService, type PlannedHike } from '../../../src/nest/tours/hike-routing.service';
import { ToursModule } from '../../../src/nest/tours/tours.module';
import { createSnapshotTestDb } from '../../helpers/db-mock';
import { makePlace } from '../../helpers/factories/places';
import { countRows, findRow } from '../../helpers/factories/rows';
import { addTourWaypoints, makeTour } from '../../helpers/factories/tours';
import { makeTrip } from '../../helpers/factories/trips';
import { makeUser } from '../../helpers/factories/users';
@@ -20,7 +23,7 @@ import type { GeometryPoint, HikePlanRequest } from '@trek/shared';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
// HIKE-RESHAPE-001 to HIKE-RESHAPE-004
// HIKE-RESHAPE-001 to HIKE-RESHAPE-005
const db = createSnapshotTestDb();
const plan = vi.fn<HikeRoutingService['plan']>();
@@ -142,4 +145,26 @@ describe('update_tour with reshape', () => {
mcp.updateTour({ tripId: trip.id, placeId: place.id, expected_revision: 1, reshape: 'reverse' }, ctx(user.id)),
).rejects.toThrow('Only a hike is changed here');
});
it('HIKE-RESHAPE-005: a recorded track is neither turned round nor closed, and nothing is planned or saved', async () => {
const { user } = await makeUser(orm());
const trip = await makeTrip(orm(), user.id);
// A GPX import from A to B: its line only, and no points of its own (its read makes up the two ends).
const place = await makePlace(orm(), trip.id, {
name: 'Recorded',
route_geometry: JSON.stringify([
[47.5, 11.1, 800],
[47.51, 11.12, 900],
[47.52, 11.1, 850],
]),
});
await makeTour(orm(), place.id, { origin: 'gpx' });
const reshaped = (reshape: 'reverse' | 'close_loop') =>
mcp.updateTour({ tripId: trip.id, placeId: place.id, expected_revision: 1, reshape }, ctx(user.id));
await expect(reshaped('reverse')).rejects.toThrow('recorded track without points of its own');
await expect(reshaped('close_loop')).rejects.toThrow('recorded track without points of its own');
expect(plan).not.toHaveBeenCalled();
expect(await countRows(orm(), TourWaypoints, { place: place.id })).toBe(0);
expect((await findRow(orm(), Tours, { place: place.id }))?.revision).toBe(1);
});
});