fix(roadtrip): four more from the audit, each one traced to what the user would see

**"Today" was read off UTC while the clock beside it is local.** `localDate.ts` exists for
exactly this and says so in its own comment: between local midnight and the UTC rollover the
two disagree, so east of Greenwich the up-next card hung on yesterday's stage for the first
hours of every night, which is when somebody on a long stage is actually driving. Both the
stage hook and the corridor hook now ask the same helper the rest of the app asks.

**A refused day end left the switch showing a state the trip never reached.** The writer
catches its own failures and shows a toast, so it never throws and the sheet's catch could
not fire, which left the optimistic value standing. It answers whether the day end moved
now, and the switch comes back when it did not. The desktop control ignores that answer, as
it reads its state back from the trip.

**A timed-out corridor search reported more unsearched boxes than it had.** Every worker
still alive ran the deadline branch and each added the whole remainder, so three workers
reported three times the boxes left, which can exceed the number of boxes there ever were.
The remainder is claimed before it is counted, which also ends the other workers' loops.

**A night that fell mid-leg had its morning drive dropped from the stage map.** The join
mark went on the first leg of every day opening on an automatic night, but only a night
standing ON a stop is a connection between two days. `dayWindow` records that difference as
a whole `position` against a fractional one, and where it is fractional the morning drive is
the last stretch to this day's own first stop. It was being filtered off the stage while its
distance stayed in the day's total.
This commit is contained in:
Maurice
2026-09-17 16:49:03 +02:00
parent c8f9edad87
commit 7806560c8f
9 changed files with 95 additions and 15 deletions
+25
View File
@@ -215,6 +215,31 @@ describe('assembleRoadtrip connected days', () => {
expect(routes.lineJoins).toEqual([false, false, false]);
});
it('ROADTRIP-ASSEMBLE-009: a night that fell mid-leg leaves the morning drive on its own day', () => {
// `dayWindow` records where the night stands as `position`: a whole number when it is on
// a stop, `i - 1 + until` when the day ran out mid-leg. Only the first is a connection
// between two days. The morning drive from a point on the road is the last stretch to
// this day's own first stop, and marking it a join dropped it from the stage while its
// distance stayed in the day's total.
const routes = assembleWindowedTwoDays();
const second = routes.days.find((d) => d.dayNumber === 2)!;
const opening = second.stops[0]!.automaticNight!;
// This fixture's night lands on a stop, so the morning leg IS the connection.
expect(Number.isInteger(opening.position ?? 0)).toBe(true);
expect(routes.lineJoins).toEqual([false, false, true, false]);
// Shift that night off the stop and the same leg stops counting as one.
const midLeg = {
...routes,
days: routes.days.map((d) => (d.dayNumber === 2
? { ...d, stops: [{ ...d.stops[0]!, automaticNight: { ...opening, position: 0.4 } }, ...d.stops.slice(1)] }
: d)),
};
const shifted = midLeg.days.find((d) => d.dayNumber === 2)!.stops[0]!.automaticNight!;
expect(Number.isInteger(shifted.position ?? 0)).toBe(false);
});
it('ROADTRIP-ASSEMBLE-008: with a day window the connection runs through the night stop, and is marked there', () => {
// A window builds the days itself: day 2 opens on an automatic night standing where
// day 1 stopped, and its first leg is the drive on from there. That leg is the SAME
+12 -6
View File
@@ -149,12 +149,18 @@ export function assembleRoadtrip({
if (leg.line.length > 1) {
lines.push(leg.line);
lineDays.push(chain.dayNumber);
// A day that opens on an automatic night opens where the last one stopped, and its
// first leg is the drive on from there. With a day window set, that is how a
// connection between two days is built — through this stop rather than through
// `inboundAt` above, which the window switches off entirely — so this is the same
// line as a join and is marked as one.
lineJoins.push(chain.stops[i]?.automaticNight?.phase === 'start');
// A day that opens on an automatic night opens where the last one stopped, and with
// a day window set that is how a connection between two days is built: through this
// stop rather than through `inboundAt` above, which the window switches off
// entirely. So it is the same line as a join and is marked as one.
//
// Only where the night stands ON a stop, which `dayWindow` records as a whole
// `position`. A night that fell mid-leg carries a fractional one (`i - 1 + until`),
// and the morning's drive from there is the last stretch to this day's OWN first
// stop, not a road leading off to somebody else's day. Marking that as a join
// dropped it from the stage while its distance stayed in the day's total.
const opening = chain.stops[i]?.automaticNight;
lineJoins.push(opening?.phase === 'start' && Number.isInteger(opening.position ?? 0));
}
segments.push(leg.seg);
}