diff options
| author | akbash <akbash@herdr.dev> | 2026-09-18 02:13:13 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-18 01:13:13 +0200 |
| commit | b88e8116e9e1f7e2f6e8b211536311cd17feca9a (patch) | |
| tree | 699535f31233ae1aee63a110bdbf2e989ba0878b /scripts/windows_input/report.py | |
| parent | 14351f121f6fc8c84b7c6c6eda78ad210e93cfcb (diff) | |
test: add local windows input gauntlet (#4298)
* test: add local windows input gauntlet
refs #4251
* test: add local windows input gauntlet
Harden elevation and channel identity checks, distinguish known host limitations, and surface incomplete cleanup.
refs #4251
* test(windows): harden input gauntlet execution
refs #4251
* test(windows): make input gauntlet self-contained
refs #4251
* test(windows): bound input gauntlet failure paths
refs #4251
* test(windows): extend input qualification gauntlet
refs #4251
* test(windows): complete input qualification matrix
refs #4251
---------
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: Jonathan Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'scripts/windows_input/report.py')
| -rw-r--r-- | scripts/windows_input/report.py | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/scripts/windows_input/report.py b/scripts/windows_input/report.py new file mode 100644 index 00000000..7c15c149 --- /dev/null +++ b/scripts/windows_input/report.py @@ -0,0 +1,446 @@ +"""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="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."), + ("clipboard-nontext", "Qualify supported image/file clipboard integrations separately; do not infer from text paste."), + ]: + 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 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("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), + ("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"} + table.append((name, cell(case_ids, width, "herdr", 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") == "herdr" 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") == "herdr" 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") == "herdr" 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()) |
