screen-locker/scripts/poke_selftest.py
2026-10-09 19:05:51 +02:00

138 lines
4.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Drive the running workout poke listener from this PC, signed with the real key.
Each case POSTs to ``http://127.0.0.1:8773/v1/workout`` exactly as the phone
would (``docs/DOCS-workout-poke-contract.md``) and prints the status and reply:
* ``sandbox`` -- a signed sandbox request: 200, ok, credited=false;
* ``badsig`` -- a signature for different bytes: 401;
* ``replay`` -- the same nonce twice: 200 (sandbox), then 409;
* ``stale`` -- ``sent_at_ms`` 10 minutes old: 401;
* ``duplicate`` -- a NON-sandbox poke of today's already-logged session
(record ``2026-10-09T15:10:40.953``): duplicate=true, credited=false;
* ``latency`` -- N sandbox round trips, p50/p95 in ms.
Usage: ``python3 scripts/poke_selftest.py CASE [--count N] [--port P]``.
"""
from __future__ import annotations
import argparse
import hashlib
import hmac
import http.client
import json
from pathlib import Path
import statistics
import sys
import time
import uuid
KEY_FILE = Path.home() / ".config" / "workout_poke" / "key"
STALE_MS = 600_000
DUPLICATE_RECORD_ID = "2026-10-09T15:10:40.953"
# The session as the phone syncs it (same shape as the shared fixture).
DUPLICATE_PAYLOAD: dict[str, object] = {
"date": "2026-10-09",
"start_time": DUPLICATE_RECORD_ID,
"workout_type": "A",
"duration_seconds": 7020.5,
"succeeded": True,
"exercises": [{"name": "Squat", "weight_kg": 100.0, "sets": [5, 5, 5, 5, 5]}],
}
def _body(
*, sandbox: bool, sent_at_ms: int | None = None, nonce: str | None = None
) -> bytes:
"""One request body; compact separators like the phone's encoder."""
doc = {
"v": 1,
"sent_at_ms": sent_at_ms
if sent_at_ms is not None
else time.time_ns() // 1_000_000,
"nonce": nonce or str(uuid.uuid4()),
"sandbox": sandbox,
"record_id": DUPLICATE_RECORD_ID,
"payload": DUPLICATE_PAYLOAD,
}
return json.dumps(doc, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _post(
port: int, key: bytes, body: bytes, *, sign_over: bytes | None = None
) -> tuple[int, dict[str, object], float]:
"""POST ``body``, signed over ``sign_over`` (default: itself).
Returns (status, reply, round-trip ms).
"""
signature = hmac.new(
key, sign_over if sign_over is not None else body, hashlib.sha256
).hexdigest()
started = time.perf_counter()
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try:
conn.request(
"POST",
"/v1/workout",
body=body,
headers={"Content-Type": "application/json", "X-Poke-Signature": signature},
)
response = conn.getresponse()
reply = json.loads(response.read())
finally:
conn.close()
return response.status, reply, (time.perf_counter() - started) * 1000
def _show(label: str, result: tuple[int, dict[str, object], float]) -> int:
status, reply, ms = result
shown = json.dumps(reply, ensure_ascii=False)
sys.stdout.write(f"{label}: HTTP {status} rtt={ms:.1f}ms {shown}\n")
return status
def _latency(port: int, key: bytes, count: int) -> None:
rtts = sorted(_post(port, key, _body(sandbox=True))[2] for _ in range(count))
p95 = rtts[max(0, round(0.95 * count) - 1)]
p50 = statistics.median(rtts)
sys.stdout.write(
f"latency over {count} sandbox pokes: p50={p50:.1f}ms "
f"p95={p95:.1f}ms max={rtts[-1]:.1f}ms\n"
)
def main() -> int:
"""Run one case against the local listener."""
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument(
"case", choices=["sandbox", "badsig", "replay", "stale", "duplicate", "latency"]
)
parser.add_argument("--count", type=int, default=20)
parser.add_argument("--port", type=int, default=8773)
args = parser.parse_args()
key = bytes.fromhex(KEY_FILE.read_text(encoding="ascii").strip())
if args.case == "sandbox":
_show("sandbox", _post(args.port, key, _body(sandbox=True)))
elif args.case == "badsig":
_show(
"badsig",
_post(args.port, key, _body(sandbox=True), sign_over=b"something else"),
)
elif args.case == "replay":
body = _body(sandbox=True)
_show("replay first", _post(args.port, key, body))
_show("replay again", _post(args.port, key, body))
elif args.case == "stale":
stale = time.time_ns() // 1_000_000 - STALE_MS
_show("stale", _post(args.port, key, _body(sandbox=True, sent_at_ms=stale)))
elif args.case == "duplicate":
_show("duplicate (NON-sandbox)", _post(args.port, key, _body(sandbox=False)))
else:
_latency(args.port, key, args.count)
return 0
if __name__ == "__main__":
sys.exit(main())