Files
ImageApi/dev/e2e/check_wall_clock.py
T
Cameron CordesandClaude Opus 5.5 9269b5c9fd Dates: true_instant on metadata responses for instant matching
The mobile viewer anchored its "messages around this photo" window on
the legacy date_taken (a wall clock stored as UTC for EXIF rows), while
SMS-API dates are true instants: the window was off by the photo's UTC
offset (4-5 h for photos taken at home; ImageApi #113 user story 3).
Verified SMS-API side: importers store backup epochs as UTC, and 40/40
sampled rows equal their source epochs exactly.

`/image/metadata` (+ GPS / date mutation responses) now carries
`true_instant` from `date_view::file_true_instant`: `row_true_instant`,
else the filename-class instant, else the earliest fs time. Server-side
for the same reason as `wall_clock`: the drain's permanent floor rows
have no `date_utc`. `set_capture_dates` fills both fields at all four
response sites.

dev/e2e: the manifest records each fixture's expected instant (the wall
read in its GPS zone, else America/New_York) and `e2e.sh check` asserts
it (97/97).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:55:29 -04:00

61 lines
2.3 KiB
Python

#!/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) 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.
Usage: check_wall_clock.py <manifest.json> (run via `e2e.sh check`)
"""
import json
import os
import subprocess
import sys
import urllib.parse
from pathlib import Path
E2E = Path(__file__).with_name("e2e.sh")
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"]}
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]
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"])
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)")
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())