fix(dates): assumed_server fallback uses the offset at the photo's wall, not now's

A container whose /etc/localtime is a copy rather than a symlink makes
server_zone() return None; the assumed_server rung then fell back to
Local::now()'s offset, which is only correct for captures from the
current season. Historical winter walls on a DST host skewed date_utc
by an hour. Resolve the process-local zone at the wall instead
(earliest reading for ambiguous walls).

Surfaced by test-server validation of #119.
This commit is contained in:
Cameron Cordes committed 2026-09-26 23:38:50 -04:00
1 parent af87765bbb
commit 615941a511
1 file changed
+44 -1
+44 -1
View File
@@ -170,7 +170,16 @@ pub fn resolve_camera_offset(
}
let minutes = match server_zone {
Some(zone) => offset_minutes_for_zone(zone, wall),
None => Local::now().offset().fix().local_minus_utc() / 60,
// Zone name unresolvable on this host: assume the process-local
// zone, but at the PHOTO's wall (a DST host's current-season
// offset would skew historical captures by an hour). Ambiguous
// walls take the earliest (pre-transition) reading; truly
// nonexistent local times fall back to the current offset.
None => match Local.from_local_datetime(&wall) {
LocalResult::Single(dt) => dt.offset().fix().local_minus_utc() / 60,
LocalResult::Ambiguous(dt, _) => dt.offset().fix().local_minus_utc() / 60,
LocalResult::None => Local::now().offset().fix().local_minus_utc() / 60,
},
};
OffsetResolution {
minutes,
@@ -338,6 +347,40 @@ mod tests {
assert_eq!(res.source, TzSource::AssumedServer);
}
/// The offset the process-local zone assigns to a wall clock —
/// independent of the current season.
fn local_offset_at(wall: NaiveDateTime) -> i32 {
Local
.from_local_datetime(&wall)
.earliest()
.map(|dt| dt.offset().fix().local_minus_utc() / 60)
.unwrap_or_else(|| Local::now().offset().fix().local_minus_utc() / 60)
}
#[test]
fn assumed_server_fallback_is_dst_aware_at_the_wall_when_zone_unresolvable() {
// `server_zone() = None` (e.g. a container whose /etc/localtime is a
// copy, not a symlink): the fallback assumes the process-local zone
// AT THE PHOTO'S WALL, not the current season's offset. On a DST
// host this test catches the old behavior whenever the host is in
// either season (one of the two walls disagrees with `now`);
// on a fixed-offset host both assertions hold trivially.
let evidence = OffsetEvidence::default();
let winter = resolve_camera_offset(wall_january(), &evidence, None, &StubLookup(None));
let summer = resolve_camera_offset(wall_july(), &evidence, None, &StubLookup(None));
assert_eq!(
winter.minutes,
local_offset_at(wall_january()),
"winter wall must carry the winter offset"
);
assert_eq!(
summer.minutes,
local_offset_at(wall_july()),
"summer wall must carry the summer offset"
);
assert_eq!(winter.source, TzSource::AssumedServer);
}
#[test]
fn gps_miss_falls_through_to_server_zone() {
let evidence = OffsetEvidence {