feat(dates): photo date filters on the true instant (#152)
date_from / date_to on /photos, /photos/exif and unified search ran on the legacy mixed-convention date_taken, so a photo could display inside a range yet be filtered out of it by up to its UTC offset (or by years, for rows the filename class re-dated). The filter lives in the shared ExifDao::query_by_exif, so it's fixed once there: - query_by_exif takes Option<date_view::InstantRange> (bounds + display and server zones) instead of raw date_from / date_to. - SQL pre-filter: converted rows on date_utc exactly; unconverted rows (drain pending, permanent floor) on date_taken widened by LEGACY_INSTANT_BAND_SECS (±15 h, now shared with /memories). Converted rows are deliberately not bounded by date_taken: a re-dated row's legacy value can sit years from its instant. - Exact pass in Rust via InstantRange::contains → row_true_instant, so every returned row's true instant is inside the range. Calendar days now mean the owner's days: nl_query::iso_to_unix builds day bounds in the display zone (it used UTC midnights, which only lined up with the legacy wall-as-UTC column), and unified search's "today" is the owner's today rather than UTC's. The midnight-with-DST-gap fallback moves from memories_anchor into date_view::day_start_instant. Compatibility: clients already sending real instants (Apollo's /photos/exif windows) get correct edges with no change. Clients sending UTC-midnight day bounds see ranges shift by up to the UTC offset until they send display-zone bounds. No index on date_utc; the OR clause scans, which is milliseconds at personal-library scale (same as /memories today). dev/e2e check gains /photos/exif wall_clock + true_instant assertions and range-edge checks ([t, t] in, [.., t-1] and [t+1, ..] out). Not run here: the harness needs the WSL + Windows toolchain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e780774d9
commit
19fbf43d40
12 files changed
+478
-90
No files matched your search
+1
-1
@@ -7,7 +7,7 @@ end-to-end and agent-driven UI testing of the SynologyFileViewer app.
|
||||
dev/e2e/e2e.sh up # build + fixtures + reset + start; blocks until ingest is done
|
||||
dev/e2e/e2e.sh reset # back to the pristine baseline (after tests mutate things)
|
||||
dev/e2e/e2e.sh stop
|
||||
dev/e2e/e2e.sh check # every fixture's displayed capture time (wall_clock) vs the manifest
|
||||
dev/e2e/e2e.sh check # every fixture's wall_clock + true_instant vs the manifest, and /photos/exif range edges
|
||||
```
|
||||
|
||||
Log in from the app with **server** `http://10.0.2.2:8090` (Android
|
||||
|
||||
Binary file not shown.
@@ -3,10 +3,13 @@
|
||||
|
||||
For every indexed fixture, the manifest's expected wall clock must equal the
|
||||
`wall_clock` the server reports on each endpoint that includes the file:
|
||||
`/image/metadata`, `/memories` (today's day span) and `/photos/gps-summary`;
|
||||
and `/image/metadata`'s `true_instant` must equal the expected instant.
|
||||
One disagreement anywhere is exactly the class of bug this field exists to
|
||||
end, so any mismatch fails the run.
|
||||
`/image/metadata`, `/memories` (today's day span), `/photos/gps-summary` and
|
||||
`/photos/exif`; and `/image/metadata` / `/photos/exif`'s `true_instant` must
|
||||
equal the expected instant. The `/photos/exif` date filter must also treat
|
||||
that instant as a range edge (#152): `[t, t]` includes the file, and a range
|
||||
ending at `t - 1` or starting at `t + 1` excludes it. One disagreement
|
||||
anywhere is exactly the class of bug these fields exist to end, so any
|
||||
mismatch fails the run.
|
||||
|
||||
Usage: check_wall_clock.py <manifest.json> (run via `e2e.sh check`)
|
||||
"""
|
||||
@@ -32,6 +35,14 @@ def main() -> int:
|
||||
|
||||
memories = {m["path"]: m.get("wall_clock") for m in api("memories?span=day")["items"]}
|
||||
gps = {g["path"]: g.get("wall_clock") for g in api("photos/gps-summary?path=")["photos"]}
|
||||
exif = {
|
||||
e["file_path"]: e
|
||||
for e in api(f"photos/exif?date_from=0&date_to={2**62}")["photos"]
|
||||
}
|
||||
|
||||
def in_exif_range(path: str, lo: int, hi: int) -> bool:
|
||||
rows = api(f"photos/exif?date_from={lo}&date_to={hi}")["photos"]
|
||||
return any(r["file_path"] == path for r in rows)
|
||||
|
||||
failures = 0
|
||||
for item in items:
|
||||
@@ -42,16 +53,29 @@ def main() -> int:
|
||||
got["memories"] = memories[path]
|
||||
if path in gps:
|
||||
got["gps-summary"] = gps[path]
|
||||
if path in exif:
|
||||
got["photos/exif"] = exif[path].get("wall_clock")
|
||||
bad = {k: v for k, v in got.items() if v != want}
|
||||
# The matching anchor (SMS / location windows) must be the true instant.
|
||||
if meta.get("true_instant") != item["true_instant"]:
|
||||
bad["metadata true_instant"] = (meta.get("true_instant"), "expected", item["true_instant"])
|
||||
t = item["true_instant"]
|
||||
if meta.get("true_instant") != t:
|
||||
bad["metadata true_instant"] = (meta.get("true_instant"), "expected", t)
|
||||
if path in exif:
|
||||
if exif[path].get("true_instant") != t:
|
||||
bad["photos/exif true_instant"] = (exif[path].get("true_instant"), "expected", t)
|
||||
# Range edges on the true instant (#152).
|
||||
if not in_exif_range(path, t, t):
|
||||
bad["photos/exif [t, t]"] = "excluded"
|
||||
if in_exif_range(path, t - 86_400, t - 1):
|
||||
bad["photos/exif [.., t-1]"] = "included"
|
||||
if in_exif_range(path, t + 1, t + 86_400):
|
||||
bad["photos/exif [t+1, ..]"] = "included"
|
||||
if bad:
|
||||
failures += 1
|
||||
print(f"MISMATCH {path}: expected {want}, got {bad}")
|
||||
|
||||
print(f"{len(items) - failures}/{len(items)} fixtures report the expected wall clock + instant "
|
||||
f"({len(memories)} in memories, {len(gps)} in gps-summary)")
|
||||
f"({len(memories)} in memories, {len(gps)} in gps-summary, {len(exif)} in photos/exif)")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -232,7 +232,8 @@ Usage: dev/e2e/e2e.sh <command>
|
||||
reset stop, wipe db/thumbs/hls, re-copy media, recreate user
|
||||
start | stop | restart | status
|
||||
check verify every fixture's displayed capture time (wall_clock)
|
||||
on metadata, memories and gps-summary against the manifest
|
||||
on metadata, memories, gps-summary and photos/exif against
|
||||
the manifest, plus photos/exif date-range edges (#152)
|
||||
token print a fresh JWT for the e2e user
|
||||
api METHOD PATH [JSON]
|
||||
authenticated request, e.g. api GET 'photos?path=&recursive=true'
|
||||
|
||||
Reference in new issue
Block a user