#!/usr/bin/env python3 """Validate a restricted GuestOps desktop-parity acceptance record.""" from __future__ import annotations import argparse import datetime as dt import json from pathlib import Path import re from urllib.parse import urlparse DESKTOP_BASELINE = "18b983bf402ecdded6430fd40bc4d3320587595a" SCENARIOS = { "inbox-core-workflow", "pagination-beyond-500", "draft-conversation-guard", "draft-filter-search-guard", "draft-route-history-reload-guard", "timezone-and-dst", "role-and-control-parity", "desktop-layout", } def require(condition: bool, message: str) -> None: if not condition: raise ValueError(message) def timestamp(value: object, field: str) -> dt.datetime: require(isinstance(value, str) and value.endswith("Z"), f"{field} must be a UTC timestamp ending in Z.") try: parsed = dt.datetime.fromisoformat(value.removesuffix("Z") + "+00:00") except ValueError as error: raise ValueError(f"{field} is not a valid timestamp.") from error require(parsed.tzinfo == dt.timezone.utc, f"{field} must be UTC.") return parsed def safe_name(value: object, field: str) -> str: name = str(value or "").strip() require(2 <= len(name) <= 120 and "@" not in name, f"{field} requires a name without an email address.") return name def validate(record: object) -> None: require(isinstance(record, dict), "Acceptance record must be a JSON object.") require(record.get("schemaVersion") == 1, "Unsupported acceptance record schema.") require(record.get("system") == "guestops-desktop-parity", "Acceptance record system must be guestops-desktop-parity.") require(record.get("dataClassification") == "synthetic-only", "Desktop acceptance must use synthetic data only.") require(record.get("desktopBaselineCommit") == DESKTOP_BASELINE, "desktopBaselineCommit must identify the reviewed desktop baseline.") require(re.fullmatch(r"[0-9a-f]{40}", str(record.get("releaseCommit", ""))) is not None, "releaseCommit must be a full lowercase Git SHA.") require(re.fullmatch(r"[0-9a-f]{64}", str(record.get("releaseRecordSha256", ""))) is not None, "releaseRecordSha256 must be a SHA-256 digest.") origin = urlparse(str(record.get("environment", ""))) require(origin.scheme == "https" and origin.hostname and origin.path in ("", "/") and not origin.query and not origin.fragment and origin.username is None and origin.password is None, "environment must be an HTTPS origin without credentials, path, query or fragment.") browser = record.get("browser") require(isinstance(browser, dict) and set(browser) == {"name", "version", "operatingSystem"}, "browser must contain exactly name, version and operatingSystem.") for field in ("name", "version", "operatingSystem"): require(2 <= len(str(browser.get(field, "")).strip()) <= 120, f"browser.{field} is required.") viewport = record.get("viewport") require(isinstance(viewport, dict) and set(viewport) == {"width", "height", "deviceScaleFactor"}, "viewport must contain exactly width, height and deviceScaleFactor.") require(isinstance(viewport["width"], int) and not isinstance(viewport["width"], bool) and 1280 <= viewport["width"] <= 7680, "Desktop viewport width must be between 1280 and 7680 pixels.") require(isinstance(viewport["height"], int) and not isinstance(viewport["height"], bool) and 720 <= viewport["height"] <= 4320, "Desktop viewport height must be between 720 and 4320 pixels.") require(isinstance(viewport["deviceScaleFactor"], (int, float)) and not isinstance(viewport["deviceScaleFactor"], bool) and 0.5 <= viewport["deviceScaleFactor"] <= 4, "deviceScaleFactor must be between 0.5 and 4.") time_zone = str(record.get("hotelTimeZone", "")) require(time_zone == "UTC" or re.fullmatch(r"[A-Za-z_]+(?:/[A-Za-z0-9_+\-]+)+", time_zone) is not None, "hotelTimeZone must be UTC or an IANA timezone name.") operator = safe_name(record.get("operator"), "operator") reviewer = safe_name(record.get("reviewedBy"), "reviewedBy") require(operator.casefold() != reviewer.casefold(), "operator and reviewedBy must be different people.") started = timestamp(record.get("startedAt"), "startedAt") ended = timestamp(record.get("endedAt"), "endedAt") reviewed = timestamp(record.get("reviewedAt"), "reviewedAt") require(started <= ended <= reviewed, "Acceptance timestamps are out of order.") scenarios = record.get("scenarios") require(isinstance(scenarios, list), "scenarios must be a list.") ids = [item.get("id") for item in scenarios if isinstance(item, dict)] require(len(ids) == len(scenarios) and len(ids) == len(set(ids)) and set(ids) == SCENARIOS, "Acceptance record requires the exact desktop scenario set.") for item in scenarios: scenario_id = item["id"] require(item.get("status") == "pass", f"Scenario {scenario_id} has not passed.") evidence = item.get("evidence") require(isinstance(evidence, list) and 1 <= len(evidence) <= 10 and all( isinstance(value, str) and 3 <= len(value) <= 200 and "@" not in value for value in evidence ), f"Scenario {scenario_id} requires safe opaque evidence references without email addresses.") def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("record", type=Path) args = parser.parse_args() validate(json.loads(args.record.read_text(encoding="utf-8"))) print(f"Desktop-parity acceptance record is structurally complete: {len(SCENARIOS)} scenarios passed. This validates the record, not its restricted evidence.") if __name__ == "__main__": try: main() except (OSError, ValueError, json.JSONDecodeError) as error: print(f"Desktop-parity acceptance record rejected: {error}", file=__import__("sys").stderr) raise SystemExit(1)