#!/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 (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())