Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a986527
Run the headless suite on the platforms it claims to support
JE-Chen Aug 19, 2026
fd4fba7
Fix two macOS defects the widened matrix found
JE-Chen Aug 19, 2026
b4055cc
Read X11 input back out of a real client
JE-Chen Aug 19, 2026
e2c43de
Measure what a real macOS runner permits, rather than guess
JE-Chen Aug 19, 2026
cb1fe40
Turn the macOS probe into a gate, now that it has been measured
JE-Chen Aug 19, 2026
c0ac50a
Give window management a backend, so it works off Windows
JE-Chen Aug 19, 2026
688dd2b
Give Linux an accessibility backend, over AT-SPI2
JE-Chen Aug 19, 2026
dfbce67
Gate the macOS window probe on the code, not on the runner's session
JE-Chen Aug 19, 2026
3afc453
Support the BSDs and arm64, and run something on both
JE-Chen Aug 19, 2026
f550eba
Record what shipped, and the one thing deliberately left
JE-Chen Aug 19, 2026
fb60702
Poll the macOS modifier state instead of racing it
JE-Chen Aug 19, 2026
794220c
Get FreeBSD's python-Xlib from pip, not from a flavoured port
JE-Chen Aug 19, 2026
775e23c
Stop losing an MCP tool reply when the transport shuts down
JE-Chen Aug 19, 2026
48368c6
Drop the Windows arm64 runner, because it measured a real blocker
JE-Chen Aug 19, 2026
a42d218
Keep the FreeBSD probe to the modules that need no Pillow
JE-Chen Aug 19, 2026
949684b
Give the FreeBSD VM the dependencies the facade imports
JE-Chen Aug 19, 2026
47257cd
Scope the FreeBSD job to the decision a BSD is needed to answer
JE-Chen Aug 19, 2026
19ea506
Give macOS the recorder it already had the code for
JE-Chen Aug 20, 2026
9cbc2bd
Drop the record-on-macOS assertion the gate no longer backs
JE-Chen Aug 20, 2026
2a49f30
Stop requiring OpenCV to move a mouse, and verify a BSD really does
JE-Chen Aug 20, 2026
2a70ccb
Bootstrap the FreeBSD VM's pip from ensurepip, not from pkg
JE-Chen Aug 20, 2026
f5347ea
Name six explicitly, since --no-deps is the point of the FreeBSD job
JE-Chen Aug 20, 2026
d46bfec
Stop needing a database to move a mouse, and let a BSD prove it
JE-Chen Aug 20, 2026
b07de3c
Mark the two import probes the way Codacy's Semgrep honours
JE-Chen Aug 20, 2026
ae65cba
Put the last five errors inside the family that gets caught
JE-Chen Aug 20, 2026
501f146
Pin that importing the package writes nothing to the user's home
JE-Chen Aug 20, 2026
7ee8eb9
Scope the import side-effect check to the directory we own
JE-Chen Aug 20, 2026
d8b6384
Name the second package that blocks Windows arm64
JE-Chen Aug 20, 2026
d7ff13f
Say where the destructive-tool prompt actually works
JE-Chen Aug 20, 2026
f52662c
Give the HTTP transport the session identity MCP specifies
JE-Chen Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,54 @@ jobs:
- name: Verify the portal handshake against liboeffis
run: docker run --rm autocontrol-portal:ci

x11-verification:
name: X11 backend against a real X server
needs: build-image
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637

- name: Build the X11 verification image
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
with:
context: .
file: docker/Dockerfile.x11
tags: autocontrol-x11:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max

# Wayland ended up with five jobs that read back what reached a real
# peer. X11 — the older and more widely deployed of the two Linux
# paths — had none: every X11 assertion in the suite is made against a
# mock of python-Xlib, so nothing had confirmed that an injected event
# reaches a client at all.
#
# Ground truth deliberately comes from other codebases than the one
# under test. xev is a real X client that prints every event delivered
# to its window, so a click is read back the way the ydotool job reads
# its events off /dev/input/eventN — including `synthetic NO`, which is
# what separates real server input from XSendEvent traffic that
# toolkits discard. ImageMagick's `import` is an independent grabber,
# in the role grim plays for Wayland, against a root window painted two
# asymmetric colours so a wrong rectangle cannot look right.
#
# It runs twice: one monitor over the whole screen, then two RANDR
# monitors side by side. There is deliberately no negative-origin pass
# — on X11 the root window is the union of every monitor and always
# starts at (0, 0), so the Wayland job's second layout has no analogue
# here. That is a protocol difference, not an untested case.
#
# The container exits with the number of failed checks.
- name: Verify the X11 backend against a real X server
run: docker run --rm autocontrol-x11:ci

seat-verification:
name: ydotool absolute move against a seat that consumes it
needs: build-image
Expand Down
141 changes: 140 additions & 1 deletion .github/workflows/platform-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,31 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [windows-2022, ubuntu-22.04, macos-14]
# arm64 is not a rounding error on the desktop any more, and the
# dependency set is where it shows. macos-14 is already arm64;
# ubuntu-22.04-arm adds Linux, and it passes.
#
# windows-11-arm is deliberately absent, and it was measured rather
# than assumed. Two dependencies have no win_arm64 wheel, and both
# have to go before the runner is worth adding back:
#
# opencv-python — no win_arm64 wheel in any version, so pip falls
# back to building from source and CMake cannot
# configure for ARM64. Twelve minutes, then failure.
# cryptography — wheels stop at 46.0.3; 46.0.4 onwards ship none.
# Our floor is >=48.0.1 and that is a security floor
# (GHSA-537c-gmf6-5ccf), so it cannot be lowered.
#
# Neither is a CI problem to work around — the package genuinely
# cannot be installed on Windows arm64 today. Re-check without a
# runner, in about ten seconds:
#
# pip install --dry-run --only-binary=:all: --platform win_arm64 \
# --python-version 3.12 --target /tmp/probe \
# 'opencv-python>=4.8,<6' 'cryptography>=48.0.1'
#
# Recorded in Progress.md; add the runner back when both resolve.
os: [windows-2022, ubuntu-22.04, macos-14, ubuntu-22.04-arm]
python-version: ["3.10", "3.14"]
runs-on: ${{ matrix.os }}
steps:
Expand Down Expand Up @@ -49,3 +73,118 @@ jobs:
name: platform-smoke-${{ matrix.os }}-${{ matrix.python-version }}
path: platform-smoke.zip
if-no-files-found: warn

freebsd:
name: The X11 backend driving input on a real FreeBSD
runs-on: ubuntu-22.04
timeout-minutes: 30

steps:
- uses: actions/checkout@v4

# The X11 backend was gated on sys.platform being linux/linux2, so it
# refused to load on a FreeBSD desktop that runs the same X server, the
# same python-Xlib and the same code. Relaxing that guard is only worth
# something if a BSD actually runs it, and no hosted runner is one — so
# this boots a real FreeBSD VM inside the runner.
#
# For a while it could only check the *decision*, because importing
# anything under je_auto_control ran the facade and the facade imported
# OpenCV and cryptography at module scope. Neither publishes a FreeBSD
# wheel and building them from ports had not finished after fifty
# minutes, so utils/platform_id was loaded by file path and the backend
# itself went untested.
#
# That was the wrong thing to work around. Moving a mouse needs neither
# package, and the facade no longer insists on them — they are imported
# by the functions that use them, which test_facade_import_is_light.py
# keeps true. What is left for this VM is python-Xlib and defusedxml,
# both pure Python, plus an X server. So the whole backend runs here now
# and the reads come back off the server itself: query_pointer for the
# cursor, its button mask for the buttons, query_keymap for the keys.
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
- uses: vmactions/freebsd-vm@v1 # NOSONAR githubactions:S7637
with:
release: "14.2"
usesh: true
prepare: |
pkg install -y python311 xorg-vfbserver
run: |
set -eu
echo "uname: $(uname -a)"

# pip comes from ensurepip, not from pkg: FreeBSD 14.2's repository
# has no py311-pip (the flavoured port names are not dependable
# here, while python311 itself is). These are the only dependencies
# the facade still needs, all pure Python, at the versions
# pyproject pins and by their PyPI names.
#
# --no-deps is the point of this job rather than a detail: it is
# what proves nothing heavy is being dragged in behind the
# verification. six is therefore named explicitly — python-Xlib
# 0.33 imports it from Xlib.display, and with --no-deps nothing
# else would install it.
#
# py311-sqlite3 is deliberately not installed either. FreeBSD
# packages sqlite3 apart from python311, this VM is the only
# machine in CI that does, and it is what caught ten subsystems
# importing it at module scope — which made `import
# je_auto_control` fail outright on a stock FreeBSD. Adding the
# package here would make that regression invisible again.
python3.11 -m ensurepip --upgrade
python3.11 -m pip install --no-deps \
python-xlib==0.33 six defusedxml==0.7.1

# The backend connects to a display at import time, so the server
# has to be up first. 1280x1024 because the verification drives the
# cursor to the far corner and reads it back.
Xvfb :99 -screen 0 1280x1024x24 &
xvfb_pid=$!
trap 'kill "$xvfb_pid" 2>/dev/null || true' EXIT
waited=0
while [ ! -e /tmp/.X11-unix/X99 ]; do
waited=$((waited + 1))
if [ "$waited" -gt 100 ]; then
echo "Xvfb never created /tmp/.X11-unix/X99" >&2
exit 1
fi
sleep 0.1
done

DISPLAY=:99 PYTHONPATH="$(pwd)" python3.11 test/verify/freebsd_verify.py
macos-capabilities:
name: What a real macOS runner permits
runs-on: macos-14

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- run: python -m pip install -e . # NOSONAR githubactions:S8544 # reason: installs the checked-out project itself, there is no upstream version to lock

# macOS is the one supported platform with no container to put it in,
# and every macOS row in docs/CAPABILITY_MATRIX.md said
# "implementation": the code was there and nothing had run it on a Mac.
#
# Two of these capabilities are gated by TCC — macOS asks a *user* to
# grant Screen Recording and Accessibility, and a CI runner has no user
# to ask. Which of them a runner grants is not something to guess at,
# and guessing is how the Wayland work twice recorded a desktop's
# refusal as a container's limitation.
#
# Measured first, in --measure mode, and the answer was a surprise: a
# macos-14 runner grants BOTH Screen Recording and Accessibility, so
# every capability works — capture returns real pixels rather than the
# black rectangle a refusal produces, CGEventPost moves the cursor and
# the move reads back exactly, and the AX walk returns real elements.
# The usual assumption that CI cannot exercise a TCC-gated macOS API is
# simply wrong for this runner.
#
# So the flag is off and this is a gate now: EXPECTED in the script
# holds what was measured, and a capability appearing or disappearing
# turns this red and names which one.
- name: Verify the macOS backend against a real window server
run: python test/verify/macos_verify.py
78 changes: 63 additions & 15 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,26 @@ jobs:
run: bandit -r je_auto_control/ -c pyproject.toml

pytest-headless:
runs-on: windows-2022
# The suite ran on Windows alone for its whole life, so every
# platform assumption it holds went unmeasured on the two operating
# systems the project also claims to support. Linux and macOS are
# here to measure them; they carry the two ends of the supported
# Python range rather than all five, because what differs between
# 3.10 and 3.14 is Python and what differs here is the OS.
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]
include:
- { os: windows-2022, python-version: "3.10" }
- { os: windows-2022, python-version: "3.11" }
- { os: windows-2022, python-version: "3.12" }
- { os: windows-2022, python-version: "3.13" }
- { os: windows-2022, python-version: "3.14" }
- { os: ubuntu-22.04, python-version: "3.10" }
- { os: ubuntu-22.04, python-version: "3.14" }
- { os: macos-14, python-version: "3.10" }
- { os: macos-14, python-version: "3.14" }
steps:
- uses: actions/checkout@v4

Expand All @@ -74,33 +89,66 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: "pip"

- name: Install dependencies
# Same set the container image installs, and for the same reasons:
# the X11 backend connects to a display at import time, opencv and
# PySide6 hard-require libGL/glib, and Qt's platform plugin needs
# the xcb libraries. Without these the suite fails at collection
# with a linker error rather than a test result.
- name: Install X11 and Qt runtime libraries (Linux)
if: runner.os == 'Linux'
run: |
python -m pip install --upgrade pip wheel
# Install the editable package FIRST so its source dir is the
# one Python sees on subsequent imports. We deliberately
# avoid `pip install -r dev_requirements.txt` here because
# that file pulls in `je_auto_control_dev` (a separate PyPI
# package), which ships its own snapshot of `je_auto_control/`
# straight into site-packages and masks the editable install
# for any sub-package the snapshot doesn't include
# (admin, usb, remote_desktop, vision, …).
pip install -e .
pip install --only-binary :all: ruff==0.15.22 bandit==1.9.4 pytest==9.1.1 pytest-timeout==2.4.0 pytest-rerunfailures==15.1 pytest-cov==7.0.0 PySide6==6.11.1
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb xauth x11-utils \
libgl1 libegl1 libglib2.0-0 \
libxkbcommon-x11-0 libdbus-1-3 \
libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \
libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-sync1 \
libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1

# One command per step, each on a single line: a `run: |` block scalar
# swallows NOSONAR markers, so a justification inside one is a comment
# that reads as handled and suppresses nothing.
- name: Upgrade the installer
shell: bash
run: python -m pip install --upgrade pip wheel # NOSONAR githubactions:S8544 # reason: pip and wheel are the installer; pinning them here would pin the tool that applies the pins below

# Install the editable package FIRST so its source dir is the one
# Python sees on subsequent imports. We deliberately avoid
# `pip install -r dev_requirements.txt` here because that file pulls in
# `je_auto_control_dev` (a separate PyPI package), which ships its own
# snapshot of `je_auto_control/` straight into site-packages and masks
# the editable install for any sub-package the snapshot doesn't include
# (admin, usb, remote_desktop, vision, …).
- name: Install the project itself
shell: bash
run: pip install -e . # NOSONAR githubactions:S8544 githubactions:S8541 # reason: installs the checked-out project itself, so there is no upstream version to lock and no third-party setup script to run

- name: Install the test tooling
shell: bash
# Quoted: `--only-binary :all:` puts a colon-space inside the
# scalar, which YAML reads as a mapping and refuses.
run: "pip install --only-binary :all: ruff==0.15.22 bandit==1.9.4 pytest==9.1.1 pytest-timeout==2.4.0 pytest-rerunfailures==15.1 pytest-cov==7.0.0 PySide6==6.11.1"

# Paths come from `testpaths` in pyproject.toml. Do NOT pass an explicit
# path here: an argument overrides testpaths, which previously meant the
# flow_control tests were configured to run but silently never did.
#
# Linux runs under a real Xvfb rather than Qt's offscreen platform:
# the X11 backend opens a display at import time, and offscreen
# would hide exactly the breakage this job exists to find.
- name: Run headless pytest suite
shell: bash
run: >-
${{ runner.os == 'Linux' && 'xvfb-run -a -s "-screen 0 1280x800x24"' || '' }}
pytest -v --tb=short --timeout=120
--cov=je_auto_control --cov-report=term-missing
--cov-report=xml --cov-fail-under=35

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.python-version }}
name: coverage-${{ matrix.os }}-${{ matrix.python-version }}
path: coverage.xml

typing-stable-api:
Expand Down
Loading
Loading