Files
herdr/scripts/windows_input/report.py
T
JJ Liebig 68dd6edaa2 test(windows): qualify remote clipboard image paste (#4324)
* 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
2026-09-18 15:18:23 +02:00

475 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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())