aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-09-24 19:37:09 +0200
committerGitHub <noreply@github.com>2026-09-24 19:37:09 +0200
commit11a7ab4ae21ab9427bcb44b3be01f62a4ab58c47 (patch)
treea56b304c2951c0bfb6581b17ee7b0d747dd32d22
parentd11c0c34cf8f9c2dc0d6fb45c58c307aa5ce8a6b (diff)
test: qualify Win32 and Kitty input paths by default (#4587)
-rw-r--r--scripts/test_windows_input.ps127
-rw-r--r--scripts/test_windows_input.py71
-rw-r--r--scripts/windows_input/README.md50
-rw-r--r--scripts/windows_input/report.py106
4 files changed, 204 insertions, 50 deletions
diff --git a/scripts/test_windows_input.ps1 b/scripts/test_windows_input.ps1
index 42d23bd7..4fdeb8ef 100644
--- a/scripts/test_windows_input.ps1
+++ b/scripts/test_windows_input.ps1
@@ -19,6 +19,7 @@ param(
[switch] $AllowInputInjection,
[switch] $ClearClipboard,
[switch] $Manual,
+ [switch] $Full,
[switch] $MatrixOnly
)
. "$PSScriptRoot/windows_input/Common.ps1"
@@ -35,6 +36,19 @@ $Cases = @($Cases | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
$knownCases = @($matrix.cases | ForEach-Object id)
if (($caseSelectionProvided -and -not @($Cases).Count) -or @($Cases | Where-Object { $_ -notin $knownCases }).Count) { throw 'Unknown case selection' }
$selectedCases = if (@($Cases).Count) { @($matrix.cases | Where-Object id -in $Cases) } else { @($matrix.cases) }
+$customSelection = @($PSBoundParameters.Keys | Where-Object { $_ -in @('Profile', 'Modes', 'Channels', 'Paths', 'Cases', 'Widths', 'Heights', 'Manual') }).Count -gt 0
+$release = -not $Full -and -not $customSelection
+if ($release) {
+ $runSpecs = @($matrix.release_plan)
+ $releaseCaseIds = @($runSpecs | ForEach-Object { $_.cases } | Sort-Object -Unique)
+ $selectedCases = @($matrix.cases | Where-Object id -in $releaseCaseIds)
+ $Channels = @('stable', 'preview'); $Paths = @('herdr'); $Modes = @('legacy', 'mok2', 'kitty')
+ $Widths = @(80); $Heights = @(24)
+} else {
+ $runSpecs = @(foreach ($channel in $Channels) { foreach ($path in $Paths) { foreach ($mode in $Modes) {
+ @{ channel = $channel; path = $path; mode = $mode; cases = @($selectedCases | ForEach-Object id) }
+ } } })
+}
if (-not $IsWindows) { throw 'Real-host qualification requires Windows and an interactive desktop; no tests passed' }
if (-not $AllowInputInjection) { throw 'Read scripts/windows_input/README.md, then explicitly pass -AllowInputInjection on an isolated desktop' }
if (-not [Environment]::UserInteractive) { throw 'Interactive desktop unavailable' }
@@ -90,13 +104,14 @@ $exeHash = (Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash
if ($sourceCommit) { Write-Host "Source: $sourceCommit$(if ($sourceDirty) { ' + working tree changes' })" }
Write-Host "Herdr under test: $exe"
Write-Host "SHA-256: $exeHash"
-Write-Host "Modes: $($Modes -join ', '); widths: $($Widths -join ', '); heights: $($Heights -join ', ')"
-Write-Host "Channels: $($Channels -join ', '); paths: $($Paths -join ', '); cases: $(if (@($Cases).Count) { $Cases -join ', ' } else { 'all' })"
+Write-Host "Pane observer modes: $($Modes -join ', '); widths: $($Widths -join ', '); heights: $($Heights -join ', ')"
+Write-Host "Campaign: $(if ($release) { 'release matrix' } else { 'diagnostic' }); channels: $($Channels -join ', '); paths: $($Paths -join ', ')"
$document = @{ schema = 1; run = [IO.Path]::GetFileName($root); started = [DateTime]::UtcNow.ToString('O');
source_commit = $sourceCommit; source_dirty = $sourceDirty; exe = $exe; exe_sha256 = $exeHash;
powershell = $PSVersionTable.PSVersion.ToString(); os = [Environment]::OSVersion.VersionString;
profile = $Profile; controller_elevated = $false; observations = @(); hosts = @(); errors = @(); cleanup_errors = @();
clipboard_clear_requested = [bool]$ClearClipboard; clipboard_formats_at_start = $clipboardFormatsAtStart; clipboard_formats_cleared = $clipboardFormatsCleared;
+ campaign = if ($release) { 'release' } else { 'diagnostic' }; run_specs = $runSpecs;
widths = $Widths; heights = $Heights; modes = $Modes; channels = $Channels; paths = $Paths;
cases = @($selectedCases | ForEach-Object id); note = 'Desktop input evidence; native qualification still required' }
$rawReport = Join-Path $root 'observations.json'
@@ -183,7 +198,9 @@ try {
$hostRecord = @{ channel = $hostName; launcher = $terminal.path; launcher_version = (Get-Item $terminal.path).VersionInfo.FileVersion;
launcher_identity = $launcherIdentities[$hostName]; installation_identity = $installationIdentities[$hostName]; runs = @() }
$document.hosts += $hostRecord
- foreach ($path in $Paths) { foreach ($mode in $Modes) {
+ foreach ($spec in @($runSpecs | Where-Object channel -eq $hostName)) {
+ $path = [string]$spec.path; $mode = [string]$spec.mode
+ $runCases = @($selectedCases | Where-Object id -in $spec.cases)
$nonce = 'herdr-gauntlet-' + [guid]::NewGuid().ToString('N')
$work = [IO.Directory]::CreateDirectory((Join-Path $root $nonce)).FullName
$configHome = [IO.Directory]::CreateDirectory((Join-Path $work 'config')).FullName
@@ -264,7 +281,7 @@ try {
$phase = 0
foreach ($geometry in $geometries) {
$phase++
- $selected = if ($geometry.full) { $selectedCases } else { @($selectedCases | Where-Object { $_.id -in @('letter-a', 'shift-enter', 'paste-lf', 'mouse-focus-refresh') }) }
+ $selected = if ($geometry.full) { $runCases } else { @($runCases | Where-Object { $_.id -in @('letter-a', 'shift-enter', 'paste-lf', 'mouse-focus-refresh') }) }
$outer = Set-ObservedGeometry $plan $window $windowPid $geometry.width $geometry.height
foreach ($case in $selected) {
$row = New-Observation $hostName $plan $geometry.width $geometry.height $case
@@ -449,7 +466,7 @@ try {
if ($null -ne $launcher) { $launcher.Dispose() }
Save-Report
}
- } }
+ }
}
} catch {
if ($document.errors.Count -eq 0) { $document.errors += $_.Exception.Message }
diff --git a/scripts/test_windows_input.py b/scripts/test_windows_input.py
index 63329134..09b60e2c 100644
--- a/scripts/test_windows_input.py
+++ b/scripts/test_windows_input.py
@@ -24,6 +24,11 @@ class WindowsInputGauntletTests(unittest.TestCase):
self.assertTrue(bytes.fromhex(value))
self.assertEqual(self.cases["dead-acute"]["kind"], "layout-key")
self.assertEqual(self.cases["altgr-euro"]["kind"], "manual")
+ for spec in self.matrix["release_plan"]:
+ for case_id in spec["cases"]:
+ self.assertIn(spec["mode"], self.cases[case_id]["expected"])
+ self.assertNotIn(self.cases[case_id]["kind"], {"manual", "qualification"})
+ self.assertFalse({"page-up", "page-down"} & {case_id for spec in self.matrix["release_plan"] for case_id in spec["cases"]})
def test_default_catalogue_does_not_inject_terminal_host_actions(self):
for case_id in ["ctrl-v", "alt-enter", "ctrl-shift-up", "ctrl-shift-down", "ctrl-shift-home", "ctrl-shift-end"]:
@@ -200,6 +205,53 @@ class WindowsInputGauntletTests(unittest.TestCase):
rows = {row[0]: row[1:] for row in qualification_matrix(result)}
self.assertEqual(rows["Shift+Enter"][0], "PARTIAL")
+ def test_release_matrix_uses_its_paired_hosts(self):
+ observations = [{"host": spec["channel"], "path": spec["path"], "mode": spec["mode"],
+ "case": case, "status": "pass", "width": width}
+ for spec in self.matrix["release_plan"] for width in (120, 80) for case in spec["cases"]]
+ nominal_only = [row for row in observations if row["width"] == 120]
+ rows = {row[0]: row[1:] for row in qualification_matrix({"campaign": "release", "observations": nominal_only})}
+ self.assertEqual(rows["Resize 120 -> 80"], ("PARTIAL", "PARTIAL"))
+ rows = {row[0]: row[1:] for row in qualification_matrix({"campaign": "release", "observations": observations})}
+ for name, cells in rows.items():
+ self.assertEqual(cells, ("MANUAL", "MANUAL") if name in {"PageUp/PageDown scroll", "AltGr", "IME composition"}
+ else ("PASS", "PASS"), name)
+ failure = next(row for row in observations if row["host"] == "stable" and row["mode"] == "mok2"
+ and row["case"] == "letter-a" and row["width"] == 120)
+ failure["status"] = "fail"
+ rows = {row[0]: row[1:] for row in qualification_matrix({"campaign": "release", "observations": observations})}
+ self.assertEqual(rows["Printable keys"][0], "FAIL")
+ self.assertEqual(rows["Resize 120 -> 80"][0], "FAIL")
+ self.assertEqual(rows["PageUp/PageDown scroll"], ("MANUAL", "MANUAL"))
+ failure["status"] = "pass"
+ missing_mode = [row for row in observations if not (row["host"] == "stable" and row["mode"] == "mok2"
+ and row["case"] == "paste-lf")]
+ rows = {row[0]: row[1:] for row in qualification_matrix({"campaign": "release", "observations": missing_mode})}
+ self.assertEqual(rows["Multiline paste"][0], "PARTIAL")
+ stable_partial = next(row for row in observations if row["host"] == "stable" and row["mode"] == "mok2"
+ and row["case"] == "paste-lf" and row["width"] == 120)
+ for status, expected in (("inconclusive", "INCONCLUSIVE"), ("unsupported", "UNSUPPORTED"),
+ ("not_run", "PARTIAL")):
+ stable_partial["status"] = status
+ rows = {row[0]: row[1:] for row in qualification_matrix({"campaign": "release", "observations": observations})}
+ self.assertEqual(rows["Multiline paste"][0], expected)
+ stable_partial["status"] = "pass"
+ partial = next(row for row in observations if row["host"] == "preview" and row["mode"] == "kitty"
+ and row["case"] == "shift-enter" and row["width"] == 80)
+ partial["status"] = "inconclusive"
+ rows = {row[0]: row[1:] for row in qualification_matrix({"campaign": "release", "observations": observations})}
+ self.assertEqual(rows["Shift+Enter"][1], "INCONCLUSIVE")
+
+ def test_run_spec_planning_does_not_require_other_combinations(self):
+ document = {"channels": ["stable"], "widths": [80], "heights": [24],
+ "run_specs": [{"channel": "stable", "path": "herdr", "mode": "legacy", "cases": ["letter-a"]}],
+ "observations": [{"host": "stable", "path": "herdr", "mode": "legacy", "phase": 1,
+ "width": 120, "height": 30, "case": "letter-a", "status": "not_run"}]}
+ self.assertEqual(summarize(document)["coverage_missing"], 2)
+ document["observations"][0]["mode"] = "kitty"
+ with self.assertRaisesRegex(ValueError, "outside the declared run matrix"):
+ summarize(document)
+
def test_direct_legacy_limit_requires_unsupported_from_every_channel(self):
observations = [{"host": host, "case": "shift-enter", "path": "direct", "mode": "legacy", "status": status}
for host, status in (("stable", "unsupported"), ("preview", "not_run"))]
@@ -241,6 +293,25 @@ class WindowsInputGauntletTests(unittest.TestCase):
result["hosts"].append({"channel": "preview", "runs": [{"nonce": "one", "path": "herdr", "mode": "native"}]})
self.assertEqual(herdr_protocol_label(result), "Herdr default (UNKNOWN)*")
+ def test_release_success_requires_stable_win32_runtime_evidence(self):
+ observations = [{**self.evidence, "case": "letter-a", "host": "stable", "path": "herdr", "mode": "legacy",
+ "hex": "61", "phase": phase, "width": width, "height": height,
+ "outer_geometry": [width, height], "final_outer_geometry": [width, height],
+ "nonce": "owned", "capture_id": str(phase)}
+ for phase, (width, height) in enumerate(((120, 30), (80, 24), (80, 30)), 1)]
+ run = {"nonce": "owned", "path": "herdr", "mode": "legacy", "pid": 123, "hwnd": 456,
+ "elevated": False, "image_identity": "image-s", "installation_identity": "install-s"}
+ document = {"campaign": "release", "channels": ["stable"], "widths": [80], "heights": [24],
+ "run_specs": [{"channel": "stable", "path": "herdr", "mode": "legacy", "cases": ["letter-a"]}],
+ "observations": observations, "hosts": [{"channel": "stable", "runs": [run]}],
+ "controller_elevated": False}
+ incomplete = summarize(document)
+ self.assertEqual(incomplete["counts"]["pass"], 3)
+ self.assertEqual(incomplete["coverage_missing"], 0)
+ self.assertFalse(incomplete["observed_checks_passed"])
+ observations[0].update(input_reader="windows-console", input_transport="win32-serialized")
+ self.assertTrue(summarize(document)["observed_checks_passed"])
+
def test_empty_partial_and_duplicate_reports_never_become_green(self):
self.assertFalse(summarize({})["observed_checks_passed"])
row = {**self.evidence, "case": "letter-a", "host": "stable", "path": "herdr", "mode": "legacy", "hex": "61", "phase": 1,
diff --git a/scripts/windows_input/README.md b/scripts/windows_input/README.md
index 61b76bd2..94b74299 100644
--- a/scripts/windows_input/README.md
+++ b/scripts/windows_input/README.md
@@ -16,8 +16,8 @@ An elevation-query failure is also a refusal, not permission to proceed. Start
PowerShell and Terminal **without Run as administrator**, on a dedicated desktop. There is no unattended CI job or runner permission
change in this implementation.
-Required: Windows, PowerShell **7** (`pwsh`), Python 3, Rust/Cargo, and at least
-one Windows Terminal channel. Stable and Preview are discovered independently
+Required: Windows, PowerShell **7** (`pwsh`), Python 3, Rust/Cargo, and both
+Windows Terminal channels for the default run. Stable and Preview are discovered independently
through their installed packages. Explicit paths are available when discovery
does not work. No software is installed or updated. The actual Terminal process path/version is recorded,
not inferred from the requested channel or bundled OpenConsole version. Stable
@@ -50,19 +50,22 @@ From the repository in PowerShell 7:
just test-windows-input
```
-The full catalogue normally exits `2` because operator-assisted and explicitly
-unimplemented qualification cases remain. That is incomplete coverage, not an
-automated test failure; inspect the printed matrix and retained `report.json`.
+The default runs the original input matrix as three targeted observer runs:
+Stable WT through Herdr with legacy and modifyOtherKeys observers, and Preview
+WT through Herdr with Kitty. Herdr's outer reader stays on its default Win32
+policy. It tests the full selected cases at 120×30, then the existing input,
+paste, and mouse sentinels at 80×24 and 80×30. PageUp/PageDown scrolling,
+AltGr, and IME remain explicit manual checks. The printed matrix has one column
+per host path; it does not combine Stable and Preview into one verdict.
### Expected outcome
-A healthy run builds and identifies the current checkout, reaches the result
-matrix, and reports no `FAIL` rows or cleanup errors. The default Herdr column
-should identify itself as `Win32 (Herdr)*` from captured runtime evidence. A full
-run may still exit `2` and show `MANUAL`, `NOT TESTED`, or a documented host
-capability limitation when a terminal channel, physical gesture, or oracle is
-unavailable. A focused campaign exits `0` when every selected observation passes;
-exit `1` means an assertion, harness, or cleanup failure and needs investigation.
+A healthy default run prints PASS for every automated cell, MANUAL for the
+physical checks, and no cleanup errors. The Stable column identifies itself as
+Win32 only from captured runtime evidence. A missing host, missing Win32 reader
+evidence, unreachable required size, or unavailable assertion exits `2`; an
+assertion or cleanup failure exits `1`. Inspect the retained `report.json` for
+exact observations.
The recipe itself is the explicit opt-in to foreground input injection. It builds
the current checkout in release mode, stages that exact binary with the pinned
@@ -78,11 +81,11 @@ pwsh -NoProfile -File scripts/test_windows_input.ps1 `
-PreviewPath 'C:\TerminalPreview\WindowsTerminal.exe'
```
-The default tests current Herdr's **default** input policy. Diagnostic runs may
-use `-Profile win32` or `-Profile vt`; they do not replace the default run.
-`-Modes native,legacy,mok2,kitty`, `-Channels`, `-Paths`, `-Cases`, `-Widths`, and
-`-Heights` select a focused campaign. In a direct PowerShell invocation, supply
-arrays normally:
+The default tests Herdr's **default** input policy. Pass `-Full` for the full
+channel × path × mode × geometry diagnostic catalogue. Any explicit `-Profile`,
+`-Modes`, `-Channels`, `-Paths`, `-Cases`, `-Widths`, `-Heights`, or `-Manual`
+selection also runs a diagnostic campaign. In a direct PowerShell invocation,
+supply arrays normally:
```powershell
.\scripts\test_windows_input.ps1 -ExePath 'C:\test-app\herdr.exe' `
@@ -140,7 +143,7 @@ Every run needs a **new** output directory. By default it is
passes when the origin is a positively observed `empty-paste`: a paste the
terminal issued for clipboard text fails, and a missing or inconclusive trace is
`inconclusive` rather than a qualification (#4314).
-- A full case pass at an observed 120×30 host size; keyboard/paste sentinels at
+- A full case pass at an observed 120×30 host size; the full diagnostic campaign also runs keyboard/paste sentinels at
**80, 119, 120, 121, 132, 160, 240 columns**, at 24 and 50 rows; then return to
80 columns. This exercises narrow→wide→narrow resizing of the actual outer
window. Both outer and pane dimensions are captured. Herdr chrome means pane
@@ -182,8 +185,10 @@ The retained directory contains:
modes, and bootstrap/probe error records.
The console ends with a capability matrix derived only from that run's captured
-observations. Filtered, unavailable, or operator-assisted cases remain
-`NOT TESTED` or `MANUAL`; the full evidence and reasons remain in `report.json`.
+observations. The default pairs Stable/Win32 with Preview/Kitty; diagnostic
+campaigns retain the direct-host Plain VT column. Filtered, unavailable, or
+operator-assisted cases remain `NOT TESTED` or `MANUAL`; the full evidence and
+reasons remain in `report.json`.
The Herdr column is labelled Win32 only after the current binary's selected
reader decodes a real nonce-owned Win32 serialized record. Configuration defaults
alone do not select the label; missing runtime evidence prints `UNKNOWN`.
@@ -212,8 +217,9 @@ Exit codes:
- `0`: all observed assertions passed and no recorded coverage gaps/errors;
- `1`: an assertion, harness, or cleanup failed;
-- `2`: coverage is incomplete (including deliberately unimplemented catalogue
- rows, missing hosts, or unavailable geometry).
+- `2`: selected coverage is incomplete (for example, a missing host or
+ unavailable geometry). The full diagnostic campaign also selects unimplemented
+ catalogue rows.
An empty or missing report never means success. None of these statuses certifies
an entire Windows host.
diff --git a/scripts/windows_input/report.py b/scripts/windows_input/report.py
index 74ce92cd..f63e700e 100644
--- a/scripts/windows_input/report.py
+++ b/scripts/windows_input/report.py
@@ -123,7 +123,14 @@ def catalogue():
("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)
+ legacy = ["letter-a", "shift-letter", "up", "down", "left", "right", "home", "end", "insert", "delete",
+ "tab", "shift-tab", "paste-lf", "paste-crlf", "paste-cr", "paste-whitespace", "paste-unicode",
+ "paste-escape-looking", "mouse-interleave", "mouse-focus-refresh", "mode-transitions"]
+ mok2 = ["letter-a", "shift-enter", "ctrl-enter", "ctrl-shift-enter", "paste-lf", "dead-acute"]
+ release_plan = [dict(channel="stable", path="herdr", mode="legacy", cases=legacy),
+ dict(channel="stable", path="herdr", mode="mok2", cases=mok2),
+ dict(channel="preview", path="herdr", mode="kitty", cases=sorted(set(legacy + mok2)))]
+ return dict(schema=1, widths=WIDTHS, heights=HEIGHTS, modes=MODES, cases=cases, release_plan=release_plan)
def classification_of_client_events(trace_lines):
@@ -362,13 +369,14 @@ def summarize(document):
# 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))
+ run_specs = document.get("run_specs") or [dict(channel=host, path=path, mode=mode, 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 spec in run_specs:
+ for phase, (width, height, full) in enumerate(geometries, 1):
+ for case_id in spec["cases"] if full else (case_id for case_id in ("letter-a", "shift-enter", "paste-lf", "mouse-focus-refresh") if case_id in spec["cases"]):
+ planned.add((spec["channel"], spec["path"], spec["mode"], phase, width, height, case_id))
missing = planned - seen
if seen - planned:
raise ValueError("Observations outside the declared run matrix")
@@ -378,7 +386,9 @@ def summarize(document):
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))
+ and all(r["status"] == "pass" for r in rows)
+ and (document.get("campaign") != "release" or
+ herdr_protocol_proven({"hosts": hosts, "observations": rows}, {"stable"})))
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"}
@@ -386,6 +396,7 @@ def summarize(document):
def qualification_matrix(result):
"""Collapse real observations into the user-facing capability summary."""
rows = result.get("observations", [])
+ release = result.get("campaign") == "release"
required_channels = set(result.get("channels") or ("stable", "preview"))
manual_cases = {case["id"] for case in catalogue()["cases"] if case["kind"] == "manual"}
groups = [
@@ -409,18 +420,44 @@ def qualification_matrix(result):
("Runtime mode transitions", {"mode-transitions"}, None),
]
- def cell(case_ids, width, path, modes):
+ if release:
+ groups = [(name, case_ids - {"page-up", "page-down"} if name == "Navigation/editing" else case_ids,
+ None if name == "Resize 120 -> 80" else width)
+ for name, case_ids, width in groups if name != "Remote clipboard image"]
+ groups.insert(5, ("PageUp/PageDown scroll", set(), None))
+ release_specs = (result.get("run_specs") or catalogue()["release_plan"]) if release else []
+
+ def cell(case_ids, width, path, modes, channels=required_channels, required_widths=None, failure_modes=None):
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)]
+ and row.get("mode") in modes and row.get("host") in channels
+ and (width is None or row.get("width") == width)]
statuses = {row.get("status") for row in matched}
+ if failure_modes:
+ selected_rows = [row for row in rows if row.get("case") in case_ids and row.get("path") == path
+ and row.get("mode") in failure_modes and row.get("host") in channels
+ and (width is None or row.get("width") == width)]
+ statuses.update(row.get("status") for row in selected_rows if row.get("status") != "pass")
if "fail" in statuses:
return "FAIL"
if case_ids <= manual_cases and not any(status == "pass" for status in statuses):
return "MANUAL"
+ if release and statuses - {"pass"}:
+ return "INCONCLUSIVE" if "inconclusive" in statuses else "UNSUPPORTED" if "unsupported" in statuses else "PARTIAL" if "pass" in statuses else "NOT TESTED"
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"
- expected_pairs = {(channel, case_id) for channel in required_channels for case_id in case_ids}
- if required_channels and {(row.get("host"), row.get("case")) for row in matched} != expected_pairs:
+ expected_pairs = {(channel, case_id) for channel in channels for case_id in case_ids}
+ if channels and {(row.get("host"), row.get("case")) for row in matched} != expected_pairs:
+ return "PARTIAL"
+ if failure_modes:
+ selected_pairs = {(spec["mode"], case_id) for spec in release_specs if spec["channel"] in channels
+ and spec["path"] == path and spec["mode"] in failure_modes for case_id in case_ids
+ if case_id in spec["cases"]}
+ if selected_pairs - {(row.get("mode"), row.get("case")) for row in selected_rows}:
+ return "PARTIAL"
+ if required_widths and not {
+ (channel, case_id, required_width) for channel in channels for case_id in case_ids
+ for required_width in required_widths} <= {
+ (row.get("host"), row.get("case"), row.get("width")) for row in matched}:
return "PARTIAL"
if case_ids == {"mode-transitions"} and path == "direct" and statuses <= {"unsupported", "inconclusive"}:
return "X - mOK ignored"
@@ -434,7 +471,7 @@ def qualification_matrix(result):
return "pass" in statuses_for_case or (width == 80 and path == "direct" and modes == {"legacy"}
and case_id == "shift-enter" and statuses_for_case - {"not_run"} == {"unsupported"})
per_case_passed = all(case_passed(case_id) for case_id in case_ids)
- if "not_run" in statuses and all(case_passed(case_id, channel) for channel in required_channels for case_id in case_ids):
+ if "not_run" in statuses and all(case_passed(case_id, channel) for channel in channels for case_id in case_ids):
return "PARTIAL"
allowed = {"pass", "inconclusive"}
if width == 80 and path == "direct" and modes == {"legacy"}:
@@ -449,27 +486,42 @@ def qualification_matrix(result):
table = []
for name, case_ids, width in groups:
+ if release and name == "PageUp/PageDown scroll":
+ table.append((name, "MANUAL", "MANUAL"))
+ continue
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"})))
+ if release:
+ required_widths = {120, 80} if name == "Resize 120 -> 80" else None
+ table.append((name, cell(case_ids, width, "herdr", herdr_modes, {"stable"}, required_widths, {"legacy", "mok2"}),
+ cell(case_ids, width, "herdr", {"kitty"}, {"preview"}, required_widths)))
+ else:
+ 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):
+def herdr_protocol_proven(result, channels=None):
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")}
+ if run.get("path") in {"herdr", "herdr-remote"} and run.get("nonce")
+ and (channels is None or host.get("channel") in channels)}
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)*"
+ return bool(runs) and runs <= proven
+
+
+def herdr_protocol_label(result, channels=None):
+ return "Win32 (Herdr)*" if herdr_protocol_proven(result, channels) 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)]
+ table = ([("Thing", "Stable WT / " + herdr_protocol_label(result, {"stable"}), "Preview WT / Kitty")]
+ if result.get("campaign") == "release" else
+ [("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(len(table[0]))]
line = lambda row: " | ".join(str(value).ljust(widths[index]) for index, value in enumerate(row))
print("\nWindows input qualification results")
print(line(table[0]))
@@ -477,8 +529,11 @@ def print_qualification_matrix(result):
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.")
+ if result.get("campaign") == "release":
+ print("MANUAL: physically verify PageUp/PageDown scrolling, AltGr, and IME before release.")
+ else:
+ 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():
@@ -500,6 +555,8 @@ def main():
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 result.get("campaign") == "release" and not herdr_protocol_proven(result, {"stable"}):
+ print("Assessment: Stable Win32 runtime evidence missing; the release matrix is incomplete")
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"]:
@@ -510,6 +567,9 @@ def main():
print("Run errors: " + " | ".join(result["errors"]))
if result.get("cleanup_errors"):
print("Cleanup errors: " + " | ".join(result["cleanup_errors"]))
+ failures = [row for row in result["observations"] if row["status"] == "fail"]
+ for row in failures:
+ print(f"FAIL: {row['host']}/{row['path']}/{row['mode']} {row['case']} at {row['width']}x{row['height']}: {row['reason']}")
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