Pin gatelock 0.2.1, and stop the popup check passing when it cannot run
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to 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) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-07-26 15:18:04 +02:00
parent c0aae7d05b
commit c11e27f123
4 changed files with 26 additions and 4 deletions

View File

@ -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

View File

@ -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",
]

View File

@ -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

View File

@ -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):")