Files
ImageApi/dev/e2e/check_wall_clock.py
T
Cameron CordesandClaude Opus 5.5 bfc3c1c21a feat(dates): calendar-day date filters in the owner's display zone (#152)
/photos, /photos/exif and unified search take date_from_day /
date_to_day (YYYY-MM-DD): the server turns them into the first day's
start and the last day's end in the owner's display zone, the same days
/memories groups by. Each replaces its instant counterpart. Clients stop
computing midnights in the device's zone, which drifted by the travel
offset whenever the phone's zone differed from display_tz.

e2e check: every fixture is included by its display-zone day and
excluded by the days either side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 03:24:50 +00:00

104 lines
4.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Regression check for capture dates (ADR-0001 `wall_clock` / `true_instant`).
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), `/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; and filtering by the
file's calendar day in the owner's display zone (`date_from_day` /
`date_to_day`) includes it, while the day before or after 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`)
"""
import json
import os
import subprocess
import sys
import urllib.parse
from datetime import datetime, timedelta, timezone
from pathlib import Path
from zoneinfo import ZoneInfo
E2E = Path(__file__).with_name("e2e.sh")
# The owner's display zone in the e2e env (no prefs row → the app default).
DISPLAY_ZONE = ZoneInfo("America/New_York")
def api(path: str) -> dict:
return json.loads(subprocess.check_output([str(E2E), "api", "GET", path]))
def main() -> int:
manifest = json.load(open(sys.argv[1]))
excluded = tuple(d.strip("/") + "/" for d in manifest["excluded_dirs"])
items = [i for i in manifest["items"] if not i["path"].startswith(excluded)]
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)
def in_exif_days(path: str, first: str, last: str) -> bool:
rows = api(f"photos/exif?date_from_day={first}&date_to_day={last}")["photos"]
return any(r["file_path"] == path for r in rows)
failures = 0
for item in items:
path, want = item["path"], item["wall_clock"]
meta = api("image/metadata?path=" + urllib.parse.quote(path))
got = {"metadata": meta.get("wall_clock")}
if path in memories:
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.
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"
# Calendar days are the owner's display-zone days (#152).
day = datetime.fromtimestamp(t, timezone.utc).astimezone(DISPLAY_ZONE).date()
iso = lambda d: d.isoformat()
if not in_exif_days(path, iso(day), iso(day)):
bad[f"photos/exif day {day}"] = "excluded"
for other in (day - timedelta(days=1), day + timedelta(days=1)):
if in_exif_days(path, iso(other), iso(other)):
bad[f"photos/exif day {other}"] = "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, {len(exif)} in photos/exif)")
return 1 if failures else 0
if __name__ == "__main__":
os.environ.setdefault("E2E_TOKEN", subprocess.check_output([str(E2E), "token"], text=True).strip())
sys.exit(main())