mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
test(server): plan 4 — full-key parity for Days/DayAssignments/Places roadtrip reads (3d carry)
Task 8b-2 item 1 (remainder): the 3 non-share/public-api reads from the 3d Task 7 review's missing-parity list — DaysRepository.listPlanDays (RPL1), DayAssignmentsRepository.listRoadtripVisits (RPL2, the correlated-subquery LEFT JOIN), and PlacesRepository.isTrackInTrip (RT13, the review's "findTrackInTrip" — the actual method name). Completes the 11-file carry together with 434113937's 8 share/public-api reads.
This commit is contained in:
@@ -795,3 +795,75 @@ describe('DayAssignmentsRepository.setAccommodation (TP57)', () => {
|
||||
expect((testDb.prepare('SELECT accommodation_id FROM day_assignments WHERE id = ?').get(other.id) as { accommodation_id: number | null }).accommodation_id).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Plan 4 Task 8b-2, item 1 (3d Task 7 review's "DayAssignments.listRoadtripVisits"
|
||||
// carry): RPL2 (`roadtrip-plan.service.ts::context`'s `visits` read) had no
|
||||
// repository-level toEqual(<legacy raw>) test — a correlated scalar subquery
|
||||
// inside a LEFT JOIN ON clause, exercised here with every nullable column
|
||||
// both null (no stay, no leg transport, coalesce falls back to the place)
|
||||
// and set (a matching stay + its checkout day, explicit assignment overrides).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const LEGACY_LIST_ROADTRIP_VISITS = `
|
||||
SELECT a.id, a.day_id, a.place_id, p.name, p.lat, p.lng,
|
||||
COALESCE(a.assignment_time, p.place_time) AS time,
|
||||
COALESCE(a.assignment_end_time, p.end_time) AS end_time,
|
||||
p.duration_minutes, a.end_day,
|
||||
a.leg_transport_mode, a.incoming_leg_transport_mode, p.stop_type, p.fill_percent,
|
||||
stay.id AS stay_id, stay.check_in, stay.check_out, checkout.day_number AS checkout_day
|
||||
FROM day_assignments a
|
||||
JOIN days d ON d.id = a.day_id
|
||||
JOIN places p ON p.id = a.place_id
|
||||
LEFT JOIN day_accommodations stay
|
||||
ON stay.id = (SELECT id FROM day_accommodations WHERE place_id = p.id AND start_day_id = d.id ORDER BY id LIMIT 1)
|
||||
LEFT JOIN days checkout ON checkout.id = stay.end_day_id
|
||||
WHERE d.trip_id = ?
|
||||
ORDER BY d.day_number, a.order_index, a.created_at
|
||||
`;
|
||||
|
||||
describe('DayAssignmentsRepository.listRoadtripVisits (RPL2, roadtrip-plan.service.ts::context)', () => {
|
||||
it('ASSIGNREPO-031: matches the legacy statement — no stay, coalesce falls back to the place, leg transport NULL', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day = createDay(testDb, trip.id);
|
||||
const place = createPlace(testDb, trip.id, { name: 'Museum' });
|
||||
testDb.prepare('UPDATE places SET place_time = ?, end_time = ?, duration_minutes = ?, stop_type = ?, fill_percent = ? WHERE id = ?')
|
||||
.run('09:00', '11:00', 90, null, null, place.id);
|
||||
createDayAssignment(testDb, day.id, place.id);
|
||||
|
||||
const legacy = testDb.prepare(LEGACY_LIST_ROADTRIP_VISITS).all(trip.id);
|
||||
const typed = await assignments.listRoadtripVisits(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed[0]).toMatchObject({ time: '09:00', end_time: '11:00', stay_id: null, check_in: null, check_out: null, checkout_day: null, leg_transport_mode: null });
|
||||
});
|
||||
|
||||
it('ASSIGNREPO-032: matches the legacy statement — a matching stay populates stay/checkout columns, explicit overrides win over coalesce', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day = createDay(testDb, trip.id, { day_number: 1 });
|
||||
const checkoutDay = createDay(testDb, trip.id, { day_number: 2 });
|
||||
const place = createPlace(testDb, trip.id, { name: 'Grand Hotel' });
|
||||
testDb.prepare('UPDATE places SET place_time = ?, end_time = ?, duration_minutes = ?, stop_type = ?, fill_percent = ? WHERE id = ?')
|
||||
.run('08:00', '10:00', 45, 'lodging', 80, place.id);
|
||||
const stay = createDayAccommodation(testDb, trip.id, place.id, day.id, checkoutDay.id, { check_in: '15:00', check_out: '11:00' });
|
||||
const assignment = createDayAssignment(testDb, day.id, place.id);
|
||||
testDb.prepare('UPDATE day_assignments SET assignment_time = ?, assignment_end_time = ?, leg_transport_mode = ?, incoming_leg_transport_mode = ?, end_day = ? WHERE id = ?')
|
||||
.run('14:00', '14:30', 'driving', 'walking', 1, assignment.id);
|
||||
|
||||
const legacy = testDb.prepare(LEGACY_LIST_ROADTRIP_VISITS).all(trip.id);
|
||||
const typed = await assignments.listRoadtripVisits(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed[0]).toMatchObject({
|
||||
time: '14:00', end_time: '14:30', stay_id: stay.id, check_in: '15:00', check_out: '11:00',
|
||||
checkout_day: 2, leg_transport_mode: 'driving', incoming_leg_transport_mode: 'walking', end_day: 1,
|
||||
stop_type: 'lodging', fill_percent: 80, duration_minutes: 45,
|
||||
});
|
||||
});
|
||||
|
||||
it('ASSIGNREPO-033: empty array for a trip with no assignments', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
expect(await assignments.listRoadtripVisits(trip.id)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -352,4 +352,76 @@ describe('DaysRepository.listForPublicApi (Plan 4 Task 1, public-api.service.ts:
|
||||
const trip = createTrip(testDb, user.id);
|
||||
expect(await days.listForPublicApi(trip.id)).toEqual([]);
|
||||
});
|
||||
|
||||
it('DAYREPO-029 (rule 19): title/notes SET (not just the default NULL) still matches the legacy row, key for key', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day = createDay(testDb, trip.id, { date: '2026-06-03', title: 'Departure' });
|
||||
testDb.prepare('UPDATE days SET notes = ? WHERE id = ?').run('Pack early', day.id);
|
||||
|
||||
const legacy = testDb.prepare('SELECT id, day_number, date, title, notes FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(trip.id);
|
||||
const rows = await days.listForPublicApi(trip.id);
|
||||
expect(rows).toEqual(legacy);
|
||||
expect(rows).toEqual([{ id: day.id, day_number: day.day_number, date: '2026-06-03', title: 'Departure', notes: 'Pack early' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DaysRepository.listPlanDays (Plan 4 Task 8b-2, RPL1 — roadtrip-plan.service.ts::context)', () => {
|
||||
it('DAYREPO-030: id/day_number/date/title/default_transport_mode, ordered by day_number, both NULL and SET nullable columns', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const other = createTrip(testDb, user.id);
|
||||
const day2 = createDay(testDb, trip.id, { day_number: 2, date: '2026-06-02', title: 'Day two' });
|
||||
testDb.prepare("UPDATE days SET default_transport_mode = ? WHERE id = ?").run('cycling', day2.id);
|
||||
const day1 = createDay(testDb, trip.id, { day_number: 1 }); // date/title/default_transport_mode all NULL
|
||||
createDay(testDb, other.id, { day_number: 1 });
|
||||
|
||||
const legacy = testDb
|
||||
.prepare('SELECT id, day_number, date, title, default_transport_mode FROM days WHERE trip_id = ? ORDER BY day_number')
|
||||
.all(trip.id);
|
||||
const rows = await days.listPlanDays(trip.id);
|
||||
|
||||
expect(rows).toEqual(legacy);
|
||||
expect(rows).toEqual([
|
||||
{ id: day1.id, day_number: 1, date: null, title: null, default_transport_mode: null },
|
||||
{ id: day2.id, day_number: 2, date: '2026-06-02', title: 'Day two', default_transport_mode: 'cycling' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('DAYREPO-031: empty array for a trip with no days', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
expect(await days.listPlanDays(trip.id)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Plan 4 Task 8b-2, item 1 (3d Task 7 review's "Days.listPlanDays" carry):
|
||||
// RPL1 (`roadtrip-plan.service.ts::context`) had no repository-level
|
||||
// toEqual(<legacy raw>) test.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('DaysRepository.listPlanDays (RPL1, roadtrip-plan.service.ts::context)', () => {
|
||||
it('DAYREPO-029: id/day_number/date/title/default_transport_mode, ordered by day_number, default_transport_mode both NULL and set', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const other = createTrip(testDb, user.id);
|
||||
const day2 = createDay(testDb, trip.id, { day_number: 2, date: '2026-06-02', title: 'Day 2' });
|
||||
const day1 = createDay(testDb, trip.id, { day_number: 1, date: '2026-06-01', title: null });
|
||||
testDb.prepare('UPDATE days SET default_transport_mode = ? WHERE id = ?').run('walking', day2.id);
|
||||
createDay(testDb, other.id, { day_number: 1 });
|
||||
|
||||
const legacy = testDb.prepare('SELECT id, day_number, date, title, default_transport_mode FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(trip.id);
|
||||
const rows = await days.listPlanDays(trip.id);
|
||||
expect(rows).toEqual(legacy);
|
||||
expect(rows.map((r) => r.id)).toEqual([day1.id, day2.id]);
|
||||
expect(rows[0].default_transport_mode).toBeNull();
|
||||
expect(rows[1].default_transport_mode).toBe('walking');
|
||||
});
|
||||
|
||||
it('DAYREPO-030: empty array for a trip with no days', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
expect(await days.listPlanDays(trip.id)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1255,4 +1255,127 @@ describe('PlacesRepository.listAssignedForPublicApi (Plan 4 Task 1, public-api.s
|
||||
const trip = createTrip(testDb, user.id);
|
||||
expect(await places.listAssignedForPublicApi(trip.id)).toEqual([]);
|
||||
});
|
||||
|
||||
it('PLACEREPO-032 (rule 19): matches the legacy JOIN run raw, every nullable projected column both NULL and SET', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const category = createCategory(testDb, { name: 'Landmark' });
|
||||
const day = createDay(testDb, trip.id);
|
||||
const sparse = createPlace(testDb, trip.id, { name: 'Sparse' });
|
||||
testDb.prepare(
|
||||
'UPDATE places SET address = NULL, lat = NULL, lng = NULL, place_time = NULL, end_time = NULL, duration_minutes = NULL, notes = NULL, transport_mode = NULL, category_id = NULL WHERE id = ?',
|
||||
).run(sparse.id);
|
||||
const full = createPlace(testDb, trip.id, { name: 'Full', lat: 10.5, lng: 20.5, category_id: category.id });
|
||||
testDb.prepare(
|
||||
"UPDATE places SET address = ?, place_time = ?, end_time = ?, duration_minutes = ?, notes = ?, transport_mode = ? WHERE id = ?",
|
||||
).run('123 Main St', '09:00', '10:00', 60, 'Bring a jacket', 'walking', full.id);
|
||||
createDayAssignment(testDb, day.id, sparse.id, { order_index: 0 });
|
||||
createDayAssignment(testDb, day.id, full.id, { order_index: 1 });
|
||||
|
||||
const legacy = testDb.prepare(`
|
||||
SELECT da.day_id, p.name, p.address, p.lat, p.lng, p.place_time, p.end_time, p.duration_minutes, p.notes, p.transport_mode, c.name as category
|
||||
FROM day_assignments da JOIN places p ON p.id = da.place_id LEFT JOIN categories c ON c.id = p.category_id
|
||||
WHERE p.trip_id = ? AND da.accommodation_id IS NULL
|
||||
ORDER BY da.day_id ASC, da.order_index ASC`).all(trip.id);
|
||||
|
||||
const rows = await places.listAssignedForPublicApi(trip.id);
|
||||
expect(rows).toEqual(legacy);
|
||||
expect(rows).toEqual([
|
||||
{ day_id: day.id, name: 'Sparse', address: null, lat: null, lng: null, place_time: null, end_time: null, duration_minutes: null, notes: null, transport_mode: null, category: null },
|
||||
{ day_id: day.id, name: 'Full', address: '123 Main St', lat: 10.5, lng: 20.5, place_time: '09:00', end_time: '10:00', duration_minutes: 60, notes: 'Bring a jacket', transport_mode: 'walking', category: 'Landmark' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PlacesRepository.isTrackInTrip (Plan 4 Task 8b-2, RT13 — RoadtripService.trackExists)', () => {
|
||||
it('PLACEREPO-033: true only when route_geometry is set AND non-empty AND the place belongs to the trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const other = createTrip(testDb, user.id);
|
||||
const withTrack = createPlace(testDb, trip.id, { name: 'Has track' });
|
||||
testDb.prepare('UPDATE places SET route_geometry = ? WHERE id = ?').run('{"type":"LineString"}', withTrack.id);
|
||||
const nullGeometry = createPlace(testDb, trip.id, { name: 'Null geometry' });
|
||||
const emptyGeometry = createPlace(testDb, trip.id, { name: 'Empty geometry' });
|
||||
testDb.prepare("UPDATE places SET route_geometry = '' WHERE id = ?").run(emptyGeometry.id);
|
||||
const foreignTrack = createPlace(testDb, other.id, { name: 'Foreign track' });
|
||||
testDb.prepare('UPDATE places SET route_geometry = ? WHERE id = ?').run('{"type":"LineString"}', foreignTrack.id);
|
||||
|
||||
const legacy = (id: number) =>
|
||||
testDb.prepare("SELECT id FROM places WHERE id = ? AND trip_id = ? AND route_geometry IS NOT NULL AND route_geometry != ''").get(id, trip.id);
|
||||
|
||||
expect(await places.isTrackInTrip(withTrack.id, trip.id)).toBe(true);
|
||||
expect(!!legacy(withTrack.id)).toBe(true);
|
||||
|
||||
expect(await places.isTrackInTrip(nullGeometry.id, trip.id)).toBe(false);
|
||||
expect(!!legacy(nullGeometry.id)).toBe(false);
|
||||
|
||||
expect(await places.isTrackInTrip(emptyGeometry.id, trip.id)).toBe(false);
|
||||
expect(!!legacy(emptyGeometry.id)).toBe(false);
|
||||
|
||||
// Belongs to a different trip — the trip_id predicate excludes it even though its geometry is set.
|
||||
expect(await places.isTrackInTrip(foreignTrack.id, trip.id)).toBe(false);
|
||||
expect(!!legacy(foreignTrack.id)).toBe(false);
|
||||
});
|
||||
|
||||
it('PLACEREPO-034: an unknown place id is false', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
expect(await places.isTrackInTrip(999999, trip.id)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Plan 4 Task 8b-2, item 1 (3d Task 7 review's "Places.findTrackInTrip" carry,
|
||||
// the actual method is `isTrackInTrip`): RT13 (`RoadtripService.trackExists`)
|
||||
// had no repository-level parity test. `SELECT id FROM places WHERE id = ?
|
||||
// AND trip_id = ? AND route_geometry IS NOT NULL AND route_geometry != ''`,
|
||||
// both nullable/empty-string edges pinned against the legacy predicate.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('PlacesRepository.isTrackInTrip (RT13, RoadtripService.trackExists)', () => {
|
||||
const legacyIsTrackInTrip = (id: number, tripId: number): boolean =>
|
||||
!!testDb.prepare(
|
||||
"SELECT id FROM places WHERE id = ? AND trip_id = ? AND route_geometry IS NOT NULL AND route_geometry != ''",
|
||||
).get(id, tripId);
|
||||
|
||||
it('PLACEREPO-032: a place with route_geometry set matches the legacy predicate — true', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
testDb.prepare('UPDATE places SET route_geometry = ? WHERE id = ?').run('{"type":"LineString"}', place.id);
|
||||
|
||||
expect(await places.isTrackInTrip(place.id, trip.id)).toBe(legacyIsTrackInTrip(place.id, trip.id));
|
||||
expect(await places.isTrackInTrip(place.id, trip.id)).toBe(true);
|
||||
});
|
||||
|
||||
it('PLACEREPO-033: route_geometry NULL matches the legacy predicate — false', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
testDb.prepare('UPDATE places SET route_geometry = NULL WHERE id = ?').run(place.id);
|
||||
|
||||
expect(await places.isTrackInTrip(place.id, trip.id)).toBe(legacyIsTrackInTrip(place.id, trip.id));
|
||||
expect(await places.isTrackInTrip(place.id, trip.id)).toBe(false);
|
||||
});
|
||||
|
||||
it('PLACEREPO-034: route_geometry \'\' (empty string) matches the legacy predicate — false', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
testDb.prepare("UPDATE places SET route_geometry = '' WHERE id = ?").run(place.id);
|
||||
|
||||
expect(await places.isTrackInTrip(place.id, trip.id)).toBe(legacyIsTrackInTrip(place.id, trip.id));
|
||||
expect(await places.isTrackInTrip(place.id, trip.id)).toBe(false);
|
||||
});
|
||||
|
||||
it('PLACEREPO-035: a track on a DIFFERENT trip is not found — false', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const other = createTrip(testDb, user.id);
|
||||
const place = createPlace(testDb, other.id);
|
||||
testDb.prepare('UPDATE places SET route_geometry = ? WHERE id = ?').run('{"type":"LineString"}', place.id);
|
||||
|
||||
expect(await places.isTrackInTrip(place.id, trip.id)).toBe(legacyIsTrackInTrip(place.id, trip.id));
|
||||
expect(await places.isTrackInTrip(place.id, trip.id)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user