← Keyhac crftwr/keyhac on GitHub · craftware

Testing

How Keyhac2 is tested, the reusable harness patterns, and what has been verified live on each OS.

Test layers

Clean-room skill testing

A packaged skill is a snapshot with no repository behind it, and this repository is full of the answers. So “is the skill self-contained?” cannot be asked from a session that has read doc/, examples/actions/ or the source — it cannot unsee them. .claude/skills/keyhac-skill-cleanroom/ is the procedure: build the versioned bundle, unpack it outside the checkout, hand a fresh session the bundle and a task, and forbid everything else.

The distinction that makes it workable: the MCP endpoint is allowed, the checkout is not. Reading the screen through Keyhac’s own tools is what the skill tells an author to do — it is the product’s public interface. Reading the package’s source, or another action somebody wrote with the repository open, is inside information.

The output is QUESTIONS.md, not the action. Every point where the clean room had to guess is a line the skill should have carried; a run that produces a working action and no questions has measured nothing. Two rules follow from that and are easy to get wrong: the operator must not answer questions during the run (an answered question is a destroyed finding), and the case’s “must” list stays out of the room, because it is the scoring key.

Scoring is evals/check.py for the mechanical rules and the case’s “must” list for judgement, and evals/cases.md’s rule stands: a “must” missed is a skill defect, not a model defect.

Cross-cutting patterns

Windows harness patterns (worth keeping)

macOS testing lessons (encoded in tests/test_mac_window.py etc.)

Live verification record

Everything automatable has been run live on both OSes (Windows 11 Home 10.0.26200; macOS 15 on this machine). Highlights and the bugs the passes caught:

Two things the pass itself got wrong, both worth keeping in mind for any Windows UI harness:

Notepad 11 drops and reorders injected input, and this is the target’s doing, not ours. hello-keys arrived in its text box as helloke-ys; a Ctrl-V came through as a bare v; and an injected Ctrl-V is silently dropped outright often enough that the pass retries it. The same strings down the same code path — including with a real hook installed and through InputContext, which is what fill.py uses — land intact in a plain Win32 control 30/30 (tests/test_win_send_text.py and a scratch probe), so the reordering is WinUI’s, not SendInput’s and not the hook’s. Two consequences: keys is not trustworthy against XAML editors, which is an argument for the existing paste-first default; and every one of these arrived as a loud failure because set_text reads back what it wrote. That rule is what turned a silently corrupted document into a FillFailed naming the text it actually found.

The interactive pass before a release

The genuinely-interactive checks were tracked in issue #10 and are all through as of 2026-08-07, but they are a standing pass, not a burnt-down backlog: they describe what to repeat before a release, since nothing here is covered by the automated harnesses. The two that needed hardware this machine does not have — a JIS keyboard, and a Japanese input method — were passed by hand on 2026-08-07 and stay on the list for the next release rather than being struck off. What the IME entry checks inverted on 2026-08-26: the chooser no longer takes keyboard focus, so its filter field no longer composes, and that is now the thing to confirm rather than the thing to fix.

Where 2.2.1 left it, since “nothing outstanding” is a claim that goes stale one release at a time: on macOS the tray “Edit Config” click and mouse feel were both verified live (2026-08-07). On Windows, 2.2.1 re-passed the Japanese IME, the tray icon and menu, and the console’s log pane, hook checkbox and log-level dropdown by hand (2026-08-08), and ran the bundle pass 11/11 against a bundle built from the PyPI PuiKit wheel. The one check not repeated for 2.2.1 is JIS layout detection, for want of the keyboard — skipped and recorded, not assumed.