mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-21 16:01:04 +00:00
* test(windows): qualify remote clipboard image paste refs #4314 * test(windows): verify staged clipboard image contents refs #4314 * test(windows): safely clean failed clipboard leases refs #4314
475 lines
30 KiB
Python
475 lines
30 KiB
Python
"""Portable case catalogue and strict evidence verdicts for the local Windows gauntlet.
|
||
|
||
This is not a host simulator. A report cannot turn absent Windows evidence into a pass.
|
||
"""
|
||
import argparse
|
||
import json
|
||
import re
|
||
from pathlib import Path
|
||
|
||
WIDTHS = [80, 119, 120, 121, 132, 160, 240]
|
||
HEIGHTS = [24, 50]
|
||
MODES = ["native", "legacy", "mok2", "kitty"]
|
||
|
||
|
||
def hex_of(text):
|
||
return text.encode("utf-8").hex()
|
||
|
||
|
||
def catalogue():
|
||
cases = []
|
||
|
||
def key(name, vk, text, modifiers=(), enhanced=None, legacy=None):
|
||
mask = (1 if 16 in modifiers else 0) | (2 if 18 in modifiers else 0) | (4 if 17 in modifiers else 0)
|
||
unicode = (vk + 32 if 65 <= vk <= 90 else vk if vk in (8, 9, 13, 27) else 0)
|
||
if 65 <= vk <= 90 and 16 in modifiers:
|
||
unicode = vk
|
||
if 17 in modifiers:
|
||
unicode = vk - 64 if 65 <= vk <= 90 else 10 if vk == 13 else 127 if vk == 8 else 0
|
||
expected = {"native": {"vk": vk, "modifiers": mask, "unicode": unicode, "modifier_keys": list(modifiers)}}
|
||
if text is not None:
|
||
expected.update({mode: {"hex": [hex_of(text)]} for mode in MODES[1:]})
|
||
if legacy is not None:
|
||
expected["legacy"] = {"hex": [hex_of(legacy)]}
|
||
if enhanced is not None:
|
||
expected["mok2"] = {"hex": [hex_of(f"\x1b[27;{mask + 1};{enhanced}~")]}
|
||
expected["kitty"] = {"hex": [hex_of(f"\x1b[{enhanced};{mask + 1}u")]}
|
||
cases.append(dict(id=name, kind="key", chords=[[*modifiers, vk]], expected=expected))
|
||
|
||
key("letter-a", 65, "a")
|
||
key("shift-letter", 65, "A", (16,))
|
||
key("enter", 13, "\r")
|
||
key("shift-enter", 13, None, (16,), 13)
|
||
cases[-1]["expected"]["legacy"] = {"loss": "enter", "hex": ["0d"]}
|
||
key("ctrl-enter", 13, None, (17,), 13)
|
||
cases[-1]["expected"]["legacy"] = {"loss": "modifier", "hex": ["0a"]}
|
||
key("ctrl-shift-enter", 13, None, (17, 16), 13)
|
||
cases[-1]["expected"]["legacy"] = {"loss": "modifier", "hex": ["0a"]}
|
||
key("tab", 9, "\t")
|
||
key("shift-tab", 9, "\x1b[Z", (16,))
|
||
cases[-1]["expected"]["mok2"] = {"hex": [hex_of("\x1b[27;2;9~")]}
|
||
cases[-1]["expected"]["kitty"]["hex"].append(hex_of("\x1b[9;2u"))
|
||
key("backspace", 8, "\x7f")
|
||
key("ctrl-backspace", 8, None, (17,), 127, "\x08")
|
||
key("alt-backspace", 8, None, (18,), 127, "\x1b\x7f")
|
||
key("escape", 27, "\x1b")
|
||
# Kitty's disambiguation flag encodes Escape distinctly.
|
||
cases[-1]["expected"]["kitty"] = {"hex": [hex_of("\x1b[27u")]}
|
||
for name, vk, seq, kitty in [("up", 38, "A", 57419), ("down", 40, "B", 57420), ("right", 39, "C", 57418),
|
||
("left", 37, "D", 57417), ("home", 36, "H", 57423), ("end", 35, "F", 57424)]:
|
||
for prefix, modifiers, modifier in [("", (), 1), ("shift-", (16,), 2), ("ctrl-", (17,), 5), ("ctrl-shift-", (17, 16), 6)]:
|
||
if prefix == "ctrl-shift-" and name in ("up", "down", "home", "end"):
|
||
continue
|
||
key(prefix + name, vk, "\x1b[" + ("" if modifier == 1 else f"1;{modifier}") + seq, modifiers)
|
||
cases[-1]["expected"]["kitty"]["hex"].append(hex_of(f"\x1b[{kitty}{'' if modifier == 1 else f';{modifier}'}u"))
|
||
for name, vk, code, kitty in [("insert", 45, 2, 57425), ("delete", 46, 3, 57426),
|
||
("page-up", 33, 5, 57421), ("page-down", 34, 6, 57422)]:
|
||
key(name, vk, f"\x1b[{code}~")
|
||
cases[-1]["expected"]["kitty"]["hex"].append(hex_of(f"\x1b[{kitty}u"))
|
||
for letter in "acdj lmqsu z".replace(" ", ""):
|
||
key("ctrl-" + letter, ord(letter.upper()), None, (17,), ord(letter), chr(ord(letter) - 96))
|
||
key("alt-v", 86, None, (18,), ord("v"), "\x1bv")
|
||
for name, text in [
|
||
("paste-lf", "line 1\nline 2"),
|
||
("paste-crlf", "line 1\r\nline 2\r\n"),
|
||
("paste-cr", "line 1\rline 2"),
|
||
("paste-whitespace", " one\t\n\n two \n"),
|
||
("paste-unicode", "é e\u0301 日本語\nnext"),
|
||
("paste-escape-looking", "literal [200~ and \\x1b[31m\nend"),
|
||
]:
|
||
cases.append(dict(id=name, kind="paste", text=text, expected={mode: {"paste": text} for mode in MODES[1:]}))
|
||
cases.append(dict(id="clipboard-image", kind="clipboard-image",
|
||
expected={mode: {"clipboard_image": True,
|
||
"sha256": "4BA8D4FD5AB42544FEEFF22D50E84412502433CC95952FD1DF9A5293588DDBEA"}
|
||
for mode in MODES[1:]}))
|
||
cases.append(dict(id="clipboard-mixed", kind="clipboard-mixed", text="clipboard text wins",
|
||
expected={mode: {"paste": "clipboard text wins"} for mode in MODES[1:]}))
|
||
cases.append(dict(id="mouse-interleave", kind="mouse-interleave", text="mouse\npaste",
|
||
expected={mode: {"mouse_interleave": True} for mode in MODES[1:]}))
|
||
cases.append(dict(id="mouse-focus-refresh", kind="mouse-focus-refresh",
|
||
expected={mode: {"mouse_focus_refresh": True} for mode in MODES[1:]}))
|
||
transitions = "a\r" + "b\x1b[27;2;13~" + "c\x1b[13;2u" + "d\x1b[27;2;13~" + "e\r" + "f"
|
||
cases.append(dict(id="mode-transitions", kind="mode-transitions",
|
||
expected={mode: {"hex": [hex_of(transitions)]} for mode in MODES[1:]}))
|
||
cases.append(dict(id="dead-acute", kind="layout-key", dead="´", base_vk=69,
|
||
expected={mode: {"hex": [hex_of("é")]} for mode in MODES[1:]}))
|
||
for name, prompt, text in [
|
||
("dead-grave", "With US-International active, type grave then e, once; no Enter.", "è"),
|
||
("dead-circumflex", "With US-International active, type circumflex then e, once; no Enter.", "ê"),
|
||
("dead-tilde", "With US-International active, type tilde then n, once; no Enter.", "ñ"),
|
||
("dead-diaeresis", "With US-International active, type diaeresis then u, once; no Enter.", "ü"),
|
||
("dead-space", "With US-International active, type acute then Space, once; no Enter.", "'"),
|
||
("altgr-euro", "With your declared euro-producing AltGr layout, type € using its AltGr chord, once; no Enter.", "€"),
|
||
("ime-commit", "Using your declared IME, compose and commit 日本語 (not paste); do not submit the prompt.", "日本語"),
|
||
]:
|
||
cases.append(dict(id=name, kind="manual", prompt=prompt, expected={mode: {"hex": [hex_of(text)]} for mode in MODES[1:]}))
|
||
# Explicit qualification tasks, never auto-passed by key/byte checks.
|
||
for name, prompt in [
|
||
("dead-cancel-repeat", "Qualify dead-key cancellation, repeated accent and non-composing next character against the direct-host baseline."),
|
||
("ime-cancel", "Qualify IME cancel and partial composition without committed/duplicate text."),
|
||
("native-repeat-release", "Qualify held-key repeat, release identity, and modifier interleaving using native records."),
|
||
("locks-keypad", "Qualify CapsLock/NumLock, keypad Enter/operators/decimal and restore lock states."),
|
||
("mouse-right-edge", "Click/drag/wheel at a marked pane column above 120; compare actual report coordinates and visual target."),
|
||
("wrap-rendering", "Verify the displayed ruler and wrapped multiline text at both window heights and every observed width."),
|
||
("capture-refresh", "Toggle mouse capture/config reload, refocus, detach/reattach; repeat paste and Shift+Enter."),
|
||
("setup-recovery", "Inject a recoverable setup failure and late VT activation; verify mode restoration and the same input sentinels."),
|
||
("ctrl-v", "Qualify raw Ctrl+V only with the Windows Terminal paste binding explicitly removed."),
|
||
("alt-enter", "Qualify Alt+Enter with the Windows Terminal fullscreen binding explicitly controlled."),
|
||
("ctrl-shift-up", "Qualify Ctrl+Shift+Up with the Windows Terminal scroll binding explicitly controlled."),
|
||
("ctrl-shift-down", "Qualify Ctrl+Shift+Down with the Windows Terminal scroll binding explicitly controlled."),
|
||
("ctrl-shift-home", "Qualify Ctrl+Shift+Home with the Windows Terminal scroll binding explicitly controlled."),
|
||
("ctrl-shift-end", "Qualify Ctrl+Shift+End with the Windows Terminal scroll binding explicitly controlled."),
|
||
("paste-supplementary", "Qualify supplementary-plane clipboard text, including emoji, against the direct-host baseline."),
|
||
("paste-burst", "Qualify a 200-line clipboard burst with the host multiline-paste warning configured or handled explicitly."),
|
||
]:
|
||
cases.append(dict(id=name, kind="qualification", prompt=prompt, expected={}))
|
||
return dict(schema=1, widths=WIDTHS, heights=HEIGHTS, modes=MODES, cases=cases)
|
||
|
||
|
||
def verdict(case, mode, evidence):
|
||
"""The full capture, not a matching prefix, is the primary assertion."""
|
||
if evidence.get("status") in {"not_run", "unsupported", "inconclusive"}:
|
||
return evidence["status"], evidence.get("reason", "No qualifying observation")
|
||
expected = case["expected"].get(mode)
|
||
if expected is None:
|
||
return "not_run", "No automatic oracle for this case/profile; requires qualification"
|
||
if not evidence.get("ready") or not evidence.get("focus_verified") or not evidence.get("complete"):
|
||
return "inconclusive", "Missing readiness, focus, or complete capture"
|
||
if evidence.get("error"):
|
||
return "inconclusive", evidence["error"]
|
||
if case["kind"] == "clipboard-image" and evidence.get("path") == "herdr":
|
||
return "not_run", "Clipboard image bridging is active only for remote clients"
|
||
if evidence.get("path") == "herdr" and case["id"] in ("page-up", "page-down") and "vk" not in expected:
|
||
expected = {"hex": [""]} # Plain page keys intentionally control Herdr's host scrollback.
|
||
def geometry(name):
|
||
value = evidence.get(name)
|
||
return value[:2] if isinstance(value, list) and len(value) >= 2 and all(type(v) is int for v in value[:2]) else None
|
||
outer, pane = geometry("outer_geometry"), geometry("pane_geometry")
|
||
final_pane, final_outer = geometry("final_pane_geometry"), geometry("final_outer_geometry")
|
||
if outer != [evidence.get("width"), evidence.get("height")]:
|
||
return "inconclusive", "Requested geometry was not observed"
|
||
if pane is None or min(pane) <= 0:
|
||
return "inconclusive", "Missing actual pane dimensions"
|
||
if final_pane != pane:
|
||
return "inconclusive", "Pane geometry changed during capture"
|
||
if final_outer != outer:
|
||
return "inconclusive", "Outer geometry changed during capture"
|
||
if evidence.get("path") == "herdr" and case["id"] in ("page-up", "page-down") and "vk" in expected:
|
||
records = evidence.get("records")
|
||
if not isinstance(records, list) or any(not isinstance(record, list) or len(record) != 7
|
||
or not all(type(field) is int for field in record) for record in records):
|
||
return "inconclusive", "Malformed native record"
|
||
return (("inconclusive", "No positive host-scrollback evidence for the consumed page key")
|
||
if all(record[0] in (4, 16) for record in records)
|
||
else ("fail", "Plain page key unexpectedly reached the pane"))
|
||
if (evidence.get("path") == "herdr" and case["id"] in ("page-up", "page-down")
|
||
and evidence.get("hex") == ""):
|
||
return "inconclusive", "No positive host-scrollback evidence for the consumed page key"
|
||
if "vk" in expected:
|
||
records = evidence.get("records")
|
||
scans = evidence.get("scans", [])
|
||
chord = case["chords"][0]
|
||
if not isinstance(records, list):
|
||
return "inconclusive", "Malformed native record"
|
||
if not isinstance(scans, list) or len(scans) != len(chord) or not all(type(scan) is int for scan in scans):
|
||
return "inconclusive", "Missing injected scan-code evidence"
|
||
expected_scans = dict(zip(chord, scans))
|
||
aliases = {160: 16, 161: 16, 162: 17, 163: 17, 164: 18, 165: 18}
|
||
held = set()
|
||
keys = []
|
||
for record in records:
|
||
if not isinstance(record, list) or len(record) != 7 or not all(type(field) is int for field in record):
|
||
return "inconclusive", "Malformed native record"
|
||
if record[0] in (4, 16): # Resize/focus notifications carry no typed text.
|
||
continue
|
||
if record[0] != 1:
|
||
return "fail", "Unexpected non-key native input"
|
||
vk = aliases.get(record[3], record[3])
|
||
if vk not in (16, 17, 18):
|
||
keys.append(record)
|
||
continue
|
||
if vk not in expected["modifier_keys"] or record[5] != 0 or record[2] != 1 or record[4] != expected_scans[vk]:
|
||
return "fail", "Unexpected/corrupted modifier record"
|
||
if record[1] == 1 and vk not in held:
|
||
held.add(vk)
|
||
elif record[1] == 0 and vk in held:
|
||
held.remove(vk)
|
||
else:
|
||
return "fail", "Unbalanced modifier sequence"
|
||
if held:
|
||
return "fail", "Modifier release missing"
|
||
if len(keys) != 2 or [r[1] for r in keys] != [1, 0]:
|
||
return "fail", "Expected exactly one non-modifier down/up pair"
|
||
for record in keys:
|
||
control = record[6]
|
||
modifiers = bool(control & 16) + 2 * bool(control & 3) + 4 * bool(control & 12)
|
||
if (record[3] != expected["vk"] or modifiers != expected["modifiers"] or record[2] != 1
|
||
or record[5] != expected["unicode"] or record[4] != expected_scans[expected["vk"]]):
|
||
return "fail", "Native identity/modifiers/repeat differ"
|
||
return "pass", "Native down/up pair matches"
|
||
try:
|
||
raw = bytes.fromhex(evidence["hex"])
|
||
except (KeyError, ValueError, TypeError):
|
||
return "inconclusive", "Missing or malformed raw bytes"
|
||
if expected.get("clipboard_image"):
|
||
if evidence.get("path") == "direct":
|
||
return (("pass", "Terminal emitted an empty bracketed paste for image-only clipboard")
|
||
if raw == b"\x1b[200~\x1b[201~" else
|
||
("fail", "Terminal did not emit an empty bracketed paste for image-only clipboard"))
|
||
if evidence.get("path") != "herdr-remote":
|
||
return "not_run", "Clipboard image bridge requires the remote-client gauntlet path"
|
||
if not raw.startswith(b"\x1b[200~") or not raw.endswith(b"\x1b[201~"):
|
||
return "fail", "Remote clipboard image did not reach the pane as one paste"
|
||
try:
|
||
path = raw[6:-6].decode("utf-8")
|
||
except UnicodeDecodeError:
|
||
return "fail", "Staged clipboard image path is not UTF-8"
|
||
valid_path = re.fullmatch(r"[A-Za-z]:\\.*\\herdr-clipboard-images-[^\\]+\\[^\\]+\.png", path)
|
||
if not valid_path:
|
||
return "fail", "Pane did not receive a staged clipboard PNG path"
|
||
return (("pass", "Exact clipboard PNG was staged and its path reached the pane")
|
||
if evidence.get("staged_image_sha256") == expected["sha256"] else
|
||
("fail", "Staged clipboard image contents differ from the fixture"))
|
||
if expected.get("mouse_interleave"):
|
||
motion = rb"(?:\x1b\[<35;\d+;\d+M)+"
|
||
newline = rb"(?:\r\n|\r|\n)"
|
||
pattern = b"a" + motion + rb"\x1b\[200~mouse" + newline + rb"paste\x1b\[201~" + motion + b"b"
|
||
return ("pass", "Typing, mouse motion and paste remained ordered") if re.fullmatch(pattern, raw) else ("fail", "Mouse interleave order or payload differs")
|
||
if expected.get("mouse_focus_refresh"):
|
||
coords = rb"\d+;\d+"
|
||
gesture = (rb"(?:\x1b\[<35;" + coords + rb"M)*"
|
||
+ rb"\x1b\[<0;" + coords + rb"M"
|
||
+ rb"\x1b\[<0;" + coords + rb"m"
|
||
+ rb"\x1b\[<64;" + coords + rb"M")
|
||
pattern = b"a" + gesture + b"b" + b"c" + gesture + b"d"
|
||
return ("pass", "Click/wheel reporting survived focus loss and regain") if re.fullmatch(pattern, raw) else ("fail", "Mouse reporting failed before or after focus regain")
|
||
if case["id"] == "mode-transitions" and evidence.get("path") == "direct":
|
||
legacy_only = hex_of("a\rb\rc\rd\re\rf")
|
||
kitty_only = hex_of("a\rb\rc\x1b[13;2u" + "d\re\rf")
|
||
if raw.hex() in {legacy_only, kitty_only}:
|
||
return "unsupported", "Direct Windows Terminal ignored modifyOtherKeys during the transition chain"
|
||
return "fail", "Unexpected direct-host runtime transition bytes"
|
||
if "loss" in expected:
|
||
return ("unsupported", "Plain VT cannot preserve modified Enter") if raw.hex() in expected["hex"] else ("fail", "Unexpected plain-VT modified Enter result")
|
||
if "hex" in expected:
|
||
return ("pass", "Exact bytes match") if raw.hex() in expected["hex"] else ("fail", "Bytes differ (including any duplicates/trailing input)")
|
||
if not raw.startswith(b"\x1b[200~") or not raw.endswith(b"\x1b[201~"):
|
||
return "fail", "Missing bracketed-paste envelope; newlines may be key events"
|
||
try:
|
||
actual = raw[6:-6].decode("utf-8")
|
||
except UnicodeDecodeError:
|
||
return "fail", "Paste is not intact UTF-8"
|
||
# Line-ending conversion is expected on Windows; all other payload bytes matter.
|
||
normalize = lambda text: text.replace("\r\n", "\n").replace("\r", "\n")
|
||
return ("pass", "One complete paste matches") if normalize(actual) == normalize(expected["paste"]) else ("fail", "Paste payload differs")
|
||
|
||
|
||
def known_host_gap(observation, case, terminal_version):
|
||
"""A narrowly observed host limitation, never a blanket version exemption."""
|
||
legacy = case["expected"].get("legacy", {}).get("hex", [])
|
||
mok2 = case["expected"].get("mok2", {}).get("hex", [])
|
||
return (observation.get("path") == "direct"
|
||
and observation.get("mode") == "mok2"
|
||
and case["id"] in {"shift-enter", "ctrl-enter", "ctrl-shift-enter", "shift-tab"}
|
||
and re.match(r"^1\.(?:24|25)\.", str(terminal_version)) and set(legacy) != set(mok2)
|
||
and observation.get("hex") in legacy)
|
||
|
||
|
||
def channel_identity_errors(hosts):
|
||
"""Compare both launcher identities and the processes actually activated."""
|
||
identities = {}
|
||
for host in hosts:
|
||
values = identities.setdefault(host.get("channel"), set())
|
||
for field, kind in (("launcher_identity", "file"), ("installation_identity", "installation")):
|
||
if host.get(field):
|
||
values.add((kind, host[field]))
|
||
for run in host.get("runs", []):
|
||
for field, kind in (("image_identity", "file"), ("installation_identity", "installation"), ("process_identity", "process")):
|
||
if run.get(field):
|
||
values.add((kind, run[field]))
|
||
return ["Stable and Preview share a Terminal executable, installation, or process identity"] if identities.get("stable", set()) & identities.get("preview", set()) else []
|
||
|
||
|
||
def summarize(document):
|
||
matrix = catalogue()
|
||
cases = {case["id"]: case for case in matrix["cases"]}
|
||
rows = []
|
||
seen = set()
|
||
captures = set()
|
||
for observation in document.get("observations", []):
|
||
identity = tuple(observation.get(k) for k in ("host", "path", "mode", "phase", "width", "height", "case"))
|
||
if identity in seen:
|
||
raise ValueError(f"Duplicate observation identity: {identity}")
|
||
seen.add(identity)
|
||
case = cases[observation["case"]]
|
||
status, reason = verdict(case, observation["mode"], observation)
|
||
scope = "direct_host" if observation.get("path") == "direct" else "through_herdr_not_yet_attributed"
|
||
if status in ("pass", "fail"):
|
||
bound = [run for host in document.get("hosts", []) if host.get("channel") == observation.get("host")
|
||
for run in host.get("runs", [])
|
||
if run.get("nonce") == observation.get("nonce") and run.get("path") == observation.get("path")
|
||
and run.get("mode") == observation.get("mode") and run.get("pid", 0) > 0 and run.get("hwnd", 0) != 0
|
||
and run.get("elevated") is False and run.get("image_identity") and run.get("installation_identity")]
|
||
capture = observation.get("capture_id")
|
||
if len(bound) != 1 or document.get("controller_elevated") is not False or not capture or capture in captures:
|
||
status, reason = "inconclusive", "Missing non-elevated owned-run binding or fresh capture identity"
|
||
else:
|
||
captures.add(capture)
|
||
if status == "fail" and known_host_gap(observation, case, bound[0].get("terminal_version")):
|
||
status, reason = "unsupported", "Direct host ignored mOK and emitted the case's legacy bytes"
|
||
rows.append({**observation, "status": status, "reason": reason, "failure_scope": scope})
|
||
counts = {status: sum(r["status"] == status for r in rows) for status in ("pass", "fail", "not_run", "unsupported", "inconclusive")}
|
||
# No run may claim all-green just because it produced zero/missing observations.
|
||
planned = set()
|
||
geometries = [(120, 30, True)] + [(w, h, False) for h in document.get("heights", HEIGHTS) for w in document.get("widths", WIDTHS)] + [(80, 30, False)]
|
||
selected_cases = document.get("cases") or list(cases)
|
||
for host in document.get("channels") or ("stable", "preview"):
|
||
for path in document.get("paths") or ("direct", "herdr"):
|
||
for mode in document.get("modes", MODES):
|
||
for phase, (width, height, full) in enumerate(geometries, 1):
|
||
for case_id in selected_cases if full else (case_id for case_id in ("letter-a", "shift-enter", "paste-lf", "mouse-focus-refresh") if case_id in selected_cases):
|
||
planned.add((host, path, mode, phase, width, height, case_id))
|
||
missing = planned - seen
|
||
if seen - planned:
|
||
raise ValueError("Observations outside the declared run matrix")
|
||
hosts = document.get("hosts", [])
|
||
errors = list(document.get("errors", [])) + channel_identity_errors(hosts)
|
||
expected_hosts = set(document.get("channels") or ("stable", "preview"))
|
||
complete = (bool(rows) and not missing and {h.get("channel") for h in hosts} == expected_hosts
|
||
and all(h.get("runs") for h in hosts)
|
||
and not errors and not document.get("cleanup_errors")
|
||
and all(r["status"] == "pass" for r in rows))
|
||
return {**document, "errors": errors, "observations": rows, "counts": counts, "coverage_missing": len(missing), "observed_checks_passed": complete,
|
||
"native_qualification": "Required; this report is not a full Windows support certificate"}
|
||
|
||
|
||
def qualification_matrix(result):
|
||
"""Collapse real observations into the user-facing capability summary."""
|
||
rows = result.get("observations", [])
|
||
required_channels = set(result.get("channels") or ("stable", "preview"))
|
||
manual_cases = {case["id"] for case in catalogue()["cases"] if case["kind"] == "manual"}
|
||
groups = [
|
||
("Printable keys", {"letter-a", "shift-letter"}, None),
|
||
("Shift+Enter", {"shift-enter"}, None),
|
||
("Ctrl+Enter", {"ctrl-enter"}, None),
|
||
("Ctrl+Shift+Enter", {"ctrl-shift-enter"}, None),
|
||
("Navigation/editing", {"up", "down", "left", "right", "home", "end", "insert", "delete", "tab", "shift-tab", "page-up", "page-down"}, None),
|
||
("Multiline paste", {"paste-lf"}, None),
|
||
("CR/LF/CRLF paste", {"paste-lf", "paste-crlf", "paste-cr"}, None),
|
||
("Unicode/whitespace paste", {"paste-unicode", "paste-whitespace"}, None),
|
||
("Paste framing/ordering", {case["id"] for case in catalogue()["cases"] if case["kind"] == "paste"}, None),
|
||
("Remote clipboard image", {"clipboard-image", "clipboard-mixed"}, None),
|
||
("Resize 120 -> 80", {"letter-a", "shift-enter", "paste-lf"}, 80),
|
||
("Mouse while typing/pasting", {"mouse-interleave"}, None),
|
||
("Mouse after focus regain", {"mouse-focus-refresh"}, None),
|
||
("Mouse after resize", {"mouse-focus-refresh"}, 80),
|
||
("Dead-key composition", {"dead-acute"}, None),
|
||
("AltGr", {"altgr-euro"}, None),
|
||
("IME composition", {"ime-commit"}, None),
|
||
("Runtime mode transitions", {"mode-transitions"}, None),
|
||
]
|
||
|
||
def cell(case_ids, width, path, modes):
|
||
matched = [row for row in rows if row.get("case") in case_ids and row.get("path") == path
|
||
and row.get("mode") in modes and (width is None or row.get("width") == width)]
|
||
statuses = {row.get("status") for row in matched}
|
||
if "fail" in statuses:
|
||
return "FAIL"
|
||
if case_ids <= manual_cases and not any(status == "pass" for status in statuses):
|
||
return "MANUAL"
|
||
if {row.get("case") for row in matched} != case_ids:
|
||
return "PARTIAL" if "pass" in statuses else "INCONCLUSIVE" if "inconclusive" in statuses else "NOT TESTED"
|
||
if required_channels and {(row.get("host"), row.get("case")) for row in matched} != {
|
||
(channel, case_id) for channel in required_channels for case_id in case_ids}:
|
||
return "PARTIAL"
|
||
if case_ids == {"mode-transitions"} and path == "direct" and statuses <= {"unsupported", "inconclusive"}:
|
||
return "X - mOK ignored"
|
||
if path == "direct" and modes == {"legacy"} and statuses == {"unsupported"}:
|
||
return "X - becomes Enter" if case_ids == {"shift-enter"} else "X - loses modifier"
|
||
def case_passed(case_id):
|
||
statuses_for_case = {row.get("status") for row in matched if row.get("case") == case_id}
|
||
return "pass" in statuses_for_case or (width == 80 and path == "direct" and modes == {"legacy"}
|
||
and case_id == "shift-enter" and statuses_for_case == {"unsupported"})
|
||
per_case_passed = all(case_passed(case_id) for case_id in case_ids)
|
||
allowed = {"pass", "inconclusive"}
|
||
if width == 80 and path == "direct" and modes == {"legacy"}:
|
||
allowed.add("unsupported")
|
||
if per_case_passed and statuses <= allowed:
|
||
return "PASS**" if "inconclusive" in statuses else "PASS"
|
||
if "inconclusive" in statuses:
|
||
return "INCONCLUSIVE"
|
||
if "unsupported" in statuses:
|
||
return "UNSUPPORTED"
|
||
return "NOT TESTED"
|
||
|
||
table = []
|
||
for name, case_ids, width in groups:
|
||
herdr_modes = {"legacy"} if name in {"Mouse after resize", "Runtime mode transitions"} else {"mok2"} if "Enter" in name or name in {"Resize 120 -> 80", "Dead-key composition", "AltGr", "IME composition"} else {"legacy"}
|
||
herdr_path = "herdr-remote" if name == "Remote clipboard image" else "herdr"
|
||
table.append((name, cell(case_ids, width, herdr_path, herdr_modes),
|
||
cell(case_ids, width, "direct", {"legacy"}), cell(case_ids, width, "direct", {"kitty"})))
|
||
return table
|
||
|
||
|
||
def herdr_protocol_label(result):
|
||
runs = {(host.get("channel"), run.get("path"), run.get("mode"), run.get("nonce"))
|
||
for host in result.get("hosts", []) for run in host.get("runs", [])
|
||
if run.get("path") in {"herdr", "herdr-remote"} and run.get("nonce")}
|
||
proven = {(row.get("host"), row.get("path"), row.get("mode"), row.get("nonce"))
|
||
for row in result.get("observations", [])
|
||
if row.get("path") in {"herdr", "herdr-remote"} and row.get("input_reader") == "windows-console"
|
||
and row.get("input_transport") == "win32-serialized" and row.get("nonce")}
|
||
return "Win32 (Herdr)*" if runs and runs <= proven else "Herdr default (UNKNOWN)*"
|
||
|
||
|
||
def print_qualification_matrix(result):
|
||
table = [("Thing", herdr_protocol_label(result), "Plain VT", "Kitty"), *qualification_matrix(result)]
|
||
widths = [max(len(str(row[column])) for row in table) for column in range(4)]
|
||
line = lambda row: " | ".join(str(value).ljust(widths[index]) for index, value in enumerate(row))
|
||
print("\nWindows input qualification results")
|
||
print(line(table[0]))
|
||
print("-+-".join("-" * width for width in widths))
|
||
for row in table[1:]:
|
||
print(line(row))
|
||
print("* Herdr protocol label comes from runtime evidence; UNKNOWN is never treated as Win32.")
|
||
print("** At least one capable host passed; host capability gaps remain visible in report.json.")
|
||
print("MANUAL requires an operator-assisted -Manual run; no automated result is claimed.")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument("command", choices=["matrix", "report"])
|
||
parser.add_argument("--input", type=Path)
|
||
parser.add_argument("--output", type=Path, required=True)
|
||
args = parser.parse_args()
|
||
result = catalogue() if args.command == "matrix" else summarize(json.loads(args.input.read_text(encoding="utf-8-sig")))
|
||
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
if args.command == "report":
|
||
counts = result["counts"]
|
||
through_failures = sum(row["status"] == "fail" and row.get("path") != "direct" for row in result["observations"])
|
||
direct_failures = sum(row["status"] == "fail" and row.get("path") == "direct" for row in result["observations"])
|
||
print(f"Observed: {counts['pass']} pass, {counts['fail']} fail, {counts['unsupported']} unsupported, "
|
||
f"{counts['inconclusive']} inconclusive, {counts['not_run']} not run; "
|
||
f"{result['coverage_missing']} planned rows missing")
|
||
if through_failures:
|
||
print(f"Assessment: {through_failures} through-Herdr failures need attribution; inspect report.json before treating them as product regressions")
|
||
elif result.get("errors") or result.get("cleanup_errors"):
|
||
print("Assessment: harness or cleanup failed; this run does not qualify input behavior")
|
||
elif direct_failures:
|
||
print(f"Assessment: {direct_failures} direct-host differences observed; these are not automatically Herdr bugs")
|
||
elif counts["inconclusive"] or counts["not_run"] or counts["unsupported"] or result["coverage_missing"]:
|
||
print("Assessment: observed assertions passed, but host limitations or qualification gaps remain")
|
||
else:
|
||
print("Assessment: all planned observed assertions passed")
|
||
if result.get("errors"):
|
||
print("Run errors: " + " | ".join(result["errors"]))
|
||
if result.get("cleanup_errors"):
|
||
print("Cleanup errors: " + " | ".join(result["cleanup_errors"]))
|
||
print_qualification_matrix(result)
|
||
return 1 if result["counts"]["fail"] or result.get("errors") or result.get("cleanup_errors") else 2 if not result["observed_checks_passed"] else 0
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|