|
1 | 1 | # What's New — AutoControl |
2 | 2 |
|
| 3 | +## What's new (2026-08-20) |
| 4 | + |
| 5 | +### The Platforms This Project Claims, Now Measured |
| 6 | + |
| 7 | +The suite ran on `windows-2022` alone for its whole life, plus one Linux |
| 8 | +container run. macOS got two commands and nothing else. Wayland had five jobs |
| 9 | +reading input back off a real peer; X11 — the older and more widely deployed |
| 10 | +of the two Linux paths — had none, and every X11 assertion in the suite was |
| 11 | +made against a mock of `python-Xlib`. |
| 12 | + |
| 13 | +**The suite now runs where the project says it runs.** `pytest-headless` |
| 14 | +became an OS matrix: Windows keeps all five Pythons, Linux and macOS carry the |
| 15 | +two ends of the range. Linux runs under a real Xvfb rather than Qt's offscreen |
| 16 | +platform, because the X11 backend opens a display at import time and offscreen |
| 17 | +would hide exactly the breakage this exists to find. It found two real macOS |
| 18 | +defects on the first run: |
| 19 | + |
| 20 | +- `write("\b")` had no key route on macOS, so it fell through to the space |
| 21 | + fallback and typed a space where a backspace was asked for. X11 and Wayland |
| 22 | + both carry the raw character; macOS was the one that did not. |
| 23 | +- `system_profiler` reports a *symbolic* vendor id for Apple's own devices — |
| 24 | + `apple_vendor_id`, not a number — and that went straight into a field |
| 25 | + documented as four hex digits. Its leading `a` is a valid hex digit, so a |
| 26 | + lenient parse turns it into `000a`. |
| 27 | + |
| 28 | +**X11 input is read back out of a real client.** A new `x11-verification` job |
| 29 | +runs against a real Xvfb server with a real window manager, taking ground |
| 30 | +truth from other codebases than the subject: `xev`, a real X client that |
| 31 | +prints every event delivered to its window; ImageMagick's `import` against a |
| 32 | +root painted two asymmetric colours; `xdotool` and `xdpyinfo`. The assertion |
| 33 | +worth naming is `synthetic NO` — `XSendEvent` traffic arrives with `YES` and |
| 34 | +is discarded by most toolkits, so a backend that quietly stopped driving real |
| 35 | +input would still pass any check that only counted events. |
| 36 | + |
| 37 | +**macOS turned out to be fully testable in CI, contrary to the usual |
| 38 | +assumption.** A `macos-14` runner grants *both* Screen Recording and |
| 39 | +Accessibility: capture returns real pixels rather than the black rectangle a |
| 40 | +refusal produces, `CGEventPost` moves the cursor and the move reads back |
| 41 | +exactly, and the AX walk returns real elements. That was measured first and |
| 42 | +asserted second, and the probe still refuses to pass while its expectations |
| 43 | +table is empty. |
| 44 | + |
| 45 | +### Window Management Is No Longer Windows-Only |
| 46 | + |
| 47 | +It was: the facade branched on `sys.platform` and raised everywhere else, |
| 48 | +leaving 23 `AC_*` commands and their MCP tools dead on macOS and Linux. It now |
| 49 | +goes through a backend seam — Win32, EWMH over `python-Xlib` on X11, Quartz |
| 50 | +plus the accessibility API on macOS, and a null fallback that lists nothing |
| 51 | +and refuses actions with a reason. |
| 52 | + |
| 53 | +Two things only a real window manager could show up were wrong first time: |
| 54 | + |
| 55 | +- **The rectangle is the frame, not the client.** Win32's `GetWindowRect` |
| 56 | + returns the frame, and every caller is written against that, so reporting |
| 57 | + the client area was off by the decorations on X11 alone — silently, and by |
| 58 | + a different amount per window manager. |
| 59 | +- **A move has to go through `_NET_MOVERESIZE_WINDOW`.** Under a reparenting |
| 60 | + window manager a client's own x/y are relative to its frame, so a direct |
| 61 | + `ConfigureWindow` asks in the wrong coordinate space. Asking openbox for |
| 62 | + (300, 220) that way landed the window at (302, 260). |
| 63 | + |
| 64 | +Refusals now raise a class that is both an `AutoControlException` and a |
| 65 | +`NotImplementedError`. The GUI tabs and the REST handler already catch the |
| 66 | +latter to say "not on this platform"; the executor catches the former, and a |
| 67 | +bare `NotImplementedError` slipped past every containment boundary — aborting |
| 68 | +a whole script where one action should have been reported as failed. |
| 69 | + |
| 70 | +### Linux Has an Accessibility Backend |
| 71 | + |
| 72 | +It had none — the selector fell through to the null one while the capability |
| 73 | +matrix claimed "backend tests" for Linux X11. The new backend speaks |
| 74 | +**AT-SPI2**, which is a D-Bus protocol rather than a library, and that is what |
| 75 | +makes it reachable without a new dependency: `pyatspi` and |
| 76 | +`gi.repository.Atspi` are distribution packages built against the system |
| 77 | +introspection data and cannot be installed into a virtual environment. |
| 78 | + |
| 79 | +The D-Bus client written for the portal handshake moved from `linux_wayland/` |
| 80 | +to `utils/dbus_client/` to make that possible, and verifying the backend |
| 81 | +against a real bus and a real GTK application immediately found a gap in it: |
| 82 | +**it could not demarshal signed integers.** The portal never needed one, and |
| 83 | +AT-SPI reports a component's extents as four *signed* values, because a window |
| 84 | +on a monitor left of or above the primary one is at a negative coordinate — so |
| 85 | +the backend could read a tree but not where anything in it was. |
| 86 | + |
| 87 | +Because AT-SPI is a bus rather than a display protocol, this is the one |
| 88 | +capability where Wayland is not the restricted case: the same bus serves both |
| 89 | +Linux sessions. |
| 90 | + |
| 91 | +### The BSDs, and arm64 |
| 92 | + |
| 93 | +`platform_wrapper` refused to start on anything that was not |
| 94 | +win32/cygwin/msys, darwin or linux/linux2, and each of the seven X11 backend |
| 95 | +modules carried its own copy of the same Linux-only guard — so a FreeBSD, |
| 96 | +OpenBSD or NetBSD desktop, which runs the same X server and the same |
| 97 | +`python-Xlib`, could not import the package at all. `sys.platform` was being |
| 98 | +compared against literal lists in over a hundred places, so the fix is one |
| 99 | +place that decides: `utils/platform_id`, whose `is_x11_unix()` asks the |
| 100 | +question those guards were always trying to ask. |
| 101 | + |
| 102 | +A `freebsd` job boots a real FreeBSD 14 VM inside the runner, imports the X11 |
| 103 | +modules under a real X server, and moves the pointer and reads it back. It |
| 104 | +covers the platform layer rather than the whole package, because opencv has no |
| 105 | +FreeBSD wheel — a limit stated in the job rather than left to be discovered. |
| 106 | +`ubuntu-22.04-arm` and `windows-11-arm` join the smoke matrix; `macos-14` was |
| 107 | +already arm64. |
| 108 | + |
| 109 | + |
3 | 110 | ## What's new (2026-08-19) |
4 | 111 |
|
5 | 112 | ### Two Wayland Judgement Calls, Settled |
|
0 commit comments