From c11e27f1234d51694a1851c76b1912d441f09dc9 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 26 Jul 2026 15:18:04 +0200 Subject: [PATCH] Pin gatelock 0.2.1, and stop the popup check passing when it cannot run The lock-popup-safety job failed on its first CI run for two reasons, and only one of them was the obvious one. The obvious one: the script imports screen_locker directly rather than through pytest, which would have put the rootdir on sys.path for it. CI now installs the package. The one worth having found: CI installs gatelock from the pinned tag, which predated `holds_grab`, so every hostile tick died on an AttributeError -- inside a Tk `after` callback, where exceptions are swallowed and logged rather than raised. The harness sailed past, printed OK and exited 0 while doing no grab re-assertion whatsoever. A verification script that passes when it is broken is worse than not having it. So it now fails closed twice over: an explicit check that the installed gatelock has the predicate at all, with a message naming the fix, and a count of ticks that actually completed -- incremented after the work, so a raising tick does not count. Fewer completed ticks than asked for is reported as "the harness itself is broken, so this result proves nothing", not as a pass. The pin moves to gatelock-v0.2.1, which carries both of yesterday's fixes: the grab predicate that no longer steals the grab off our own posted menus, and the recovery loop that no longer stops forever when a tick raises. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/python-tests.yml | 3 +++ pyproject.toml | 2 +- requirements.txt | 2 +- scripts/verify_lock_popup_safety.py | 23 +++++++++++++++++++++-- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 8869410..5bcd0fe 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -45,6 +45,9 @@ jobs: sudo apt-get update sudo apt-get install -y xvfb python3-tk pip install -r requirements.txt + # The script imports screen_locker directly rather than through + # pytest, which would put the rootdir on sys.path for it. + pip install -e . --no-deps - name: Sport selector survives gatelock's recovery tick run: python3 scripts/verify_lock_popup_safety.py diff --git a/pyproject.toml b/pyproject.toml index 9cf66b5..cfd95ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.10" # crdt-sync: shared CRDT merge scheme + GitHub-Contents-API sync transport, # also used by diet_guard; here for phone-workout GitHub sync (Workstream C) dependencies = [ - "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.2.0#subdirectory=gatelock", + "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.2.1#subdirectory=gatelock", "crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync", ] diff --git a/requirements.txt b/requirements.txt index 2351a96..2e7f9bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ bandit>=1.7.0 codespell>=2.2.0 coverage>=7.4.0 crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync -gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.2.0#subdirectory=gatelock +gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.2.1#subdirectory=gatelock mcp>=1.9 mypy>=1.8.0 pre-commit>=3.6.0 diff --git a/scripts/verify_lock_popup_safety.py b/scripts/verify_lock_popup_safety.py index e83e1ed..2a22e46 100755 --- a/scripts/verify_lock_popup_safety.py +++ b/scripts/verify_lock_popup_safety.py @@ -97,10 +97,13 @@ class HostileLock: def _tick(self) -> None: """Lift the surface and re-take the grab, exactly as gatelock does.""" - self.ticks += 1 self.root.lift() if not self._loop.holds_grab(): self.root.grab_set_global() + # Counted only after the work, so a raising tick does not count. Tk + # swallows exceptions from `after` callbacks, and this harness once + # reported OK while every tick was dying on an AttributeError. + self.ticks += 1 self.root.after(TICK_MS, self._tick) @@ -120,13 +123,21 @@ def run_ticks( nonlocal verdict verdict = judge() - HostileLock(root) + lock = HostileLock(root) if setup is not None: root.after(200, setup) root.after(TICK_MS * TICKS + 400, decide) root.after(TICK_MS * TICKS + 600, root.quit) root.mainloop() root.destroy() + if lock.ticks < TICKS: + _logger.error( + " only %d of %d hostile ticks completed -- the harness itself is " + "broken, so this result proves nothing", + lock.ticks, + TICKS, + ) + return False return verdict @@ -269,6 +280,14 @@ def main() -> int: if os.environ.get(INNER_ENV) != "1": return reexec_under_xvfb() + if not hasattr(RecoveryLoop, "holds_grab"): + _logger.error( + "The installed gatelock has no RecoveryLoop.holds_grab, so this " + "check cannot replay the real grab predicate. Bump the gatelock " + "pin in requirements.txt to a version that has it." + ) + return 1 + _logger.info("The mechanism (a popup on a lock surface):") mechanism_reproduced = check_optionmenu_still_fails() _logger.info("The fix (radio buttons in the production form):")