gatelock: split test_recovery.py 477 -> 208 lines by test layer

The file's own docstring described three layers; they are now three files:

  test_recovery_invariants.py  (52)  — the static AST check that _recovery.py
                                       contains no weakening call at all
  test_recovery_reassertion.py (218) — what a tick re-asserts: grab, focus, VT
  test_recovery.py             (208) — the 2026-07-25 monotonic-sequence walk,
                                       tick mechanics, and scheduling

Splitting the classes made pylint's R0801 fire: both halves built a
RecoveryLoop with the same RecoveryCollaborators block. Rather than suppress
it (banned) the shared construction became conftest.build_loop(root, config),
which the `loop` fixture now uses too — so the tests that need a non-default
config ask for one instead of restating the wiring. The output scans both
halves drive moved to conftest alongside it.

389 passed, 100% branch coverage — same tests, same count, three files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
This commit is contained in:
Krzysztof Rudnicki 2026-08-22 14:37:47 +02:00
parent 48f6c7420f
commit 408c7a2585
4 changed files with 305 additions and 275 deletions

View File

@ -16,7 +16,9 @@ from unittest.mock import MagicMock, patch
import pytest
from gatelock._outputs import Output, OutputRect
from gatelock._outputs import Output, OutputRect, OutputScan
from gatelock._recovery import RecoveryCollaborators, RecoveryLoop
from gatelock._surfaces import SurfaceSet
from gatelock._window import LockConfig, LockWindow
if TYPE_CHECKING:
@ -110,3 +112,51 @@ def make_window(
hooks = hooks if hooks is not None else MagicMock()
window = LockWindow(root, config or LockConfig(), hooks)
return window, hooks
DP0 = Output("DP-0", connected=True, rect=OutputRect(0, 0, 3840, 2160), primary=True)
HDMI = Output("HDMI-0", connected=True, rect=OutputRect(3840, 0, 2560, 1440))
DP0_DARK = Output("DP-0", connected=True, rect=None, primary=True)
HDMI_DARK = Output("HDMI-0", connected=True, rect=None)
BOTH = OutputScan((DP0, HDMI), "randr", ok=True)
DEAD_PRIMARY = OutputScan((DP0_DARK, HDMI), "randr", ok=True)
ALL_DARK = OutputScan((DP0_DARK, HDMI_DARK), "randr", ok=True)
FAILED = OutputScan((), "none", ok=False)
def build_loop(
root: MagicMock, config: LockConfig
) -> tuple[RecoveryLoop, MagicMock, SurfaceSet]:
"""A recovery loop over mocked surroundings, for an arbitrary config.
Args:
root: The mocked Tk root the loop drives.
config: The lock configuration under test.
Returns:
The loop, its mocked enumerator, and the surface set it manages.
"""
hooks = MagicMock()
surfaces = SurfaceSet(root, config, hooks)
enumerator = MagicMock()
return (
RecoveryLoop(
root,
RecoveryCollaborators(
config=config,
surfaces=surfaces,
enumerator=enumerator,
detector=MagicMock(),
hooks=hooks,
),
),
enumerator,
surfaces,
)
@pytest.fixture
def loop(mock_root: MagicMock) -> tuple[RecoveryLoop, MagicMock, SurfaceSet]:
"""A recovery loop in hard mode, the configuration most tests want."""
return build_loop(mock_root, LockConfig(mode="hard"))

View File

@ -7,92 +7,18 @@ lock never weakens, and per-branch coverage.
from __future__ import annotations
import ast
import logging
from pathlib import Path
import tkinter as tk
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
from gatelock.tests.conftest import ALL_DARK, BOTH, DEAD_PRIMARY, FAILED
from gatelock._outputs import Output, OutputRect, OutputScan
from gatelock._recovery import RecoveryCollaborators, RecoveryLoop
from gatelock._surfaces import SurfaceSet
from gatelock._window import LockConfig
if TYPE_CHECKING:
import pytest
DP0 = Output("DP-0", connected=True, rect=OutputRect(0, 0, 3840, 2160), primary=True)
HDMI = Output("HDMI-0", connected=True, rect=OutputRect(3840, 0, 2560, 1440))
DP0_DARK = Output("DP-0", connected=True, rect=None, primary=True)
HDMI_DARK = Output("HDMI-0", connected=True, rect=None)
BOTH = OutputScan((DP0, HDMI), "randr", ok=True)
DEAD_PRIMARY = OutputScan((DP0_DARK, HDMI), "randr", ok=True)
ALL_DARK = OutputScan((DP0_DARK, HDMI_DARK), "randr", ok=True)
FAILED = OutputScan((), "none", ok=False)
WEAKENING_CALLS = frozenset(
{"grab_release", "restore_vt_switching", "close", "destroy", "quit", "withdraw"}
)
SOURCE_ROOT = Path(__file__).resolve().parent.parent
def symbols(module: str) -> set[str]:
"""Every attribute and name referenced in a module's source."""
tree = ast.parse((SOURCE_ROOT / module).read_text(encoding="utf-8"))
return {n.attr for n in ast.walk(tree) if isinstance(n, ast.Attribute)} | {
n.id for n in ast.walk(tree) if isinstance(n, ast.Name)
}
@pytest.fixture
def loop(mock_root: MagicMock) -> tuple[RecoveryLoop, MagicMock, SurfaceSet]:
"""A recovery loop over mocked surroundings."""
config = LockConfig(mode="hard")
hooks = MagicMock()
surfaces = SurfaceSet(mock_root, config, hooks)
enumerator = MagicMock()
detector = MagicMock()
return (
RecoveryLoop(
mock_root,
RecoveryCollaborators(
config=config,
surfaces=surfaces,
enumerator=enumerator,
detector=detector,
hooks=hooks,
),
),
enumerator,
surfaces,
)
class TestStaticInvariant:
"""The separation that makes monotonicity mechanical rather than hoped-for."""
def test_recovery_contains_no_weakening_call(self) -> None:
"""_recovery.py must not be able to release, restore or destroy."""
assert symbols("_recovery.py") & WEAKENING_CALLS == set()
def test_recovery_may_still_strengthen(self) -> None:
"""The asymmetry is the point: strengthening calls ARE present."""
found = symbols("_recovery.py")
assert "disable_vt_switching" in found
assert "grab_set_global" in found
def test_surfaces_never_touches_grab_or_vt(self) -> None:
"""Window ownership and lock strength stay separate concerns."""
banned = {"grab_release", "grab_set", "grab_set_global", "restore_vt_switching"}
assert symbols("_surfaces.py") & banned == set()
def test_detect_thread_never_touches_tk(self) -> None:
"""Tk is not thread-safe; the RandR thread may only queue."""
source = (SOURCE_ROOT / "_detect.py").read_text(encoding="utf-8")
loop_body = source.split("def _loop(")[1].split("def ")[0]
assert "self._sink.put" in loop_body
for forbidden in ("geometry(", "deiconify(", "lift(", "grab_"):
assert forbidden not in loop_body
from gatelock._recovery import RecoveryLoop
from gatelock._surfaces import SurfaceSet
class TestMonotonicSequence:
@ -178,200 +104,6 @@ class TestTick:
assert recovery.ticks == 2
class TestGrabReassertion:
"""Re-taking a lost grab, never releasing one."""
def test_reasserts_when_lost(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""Something stole the grab; we take it back."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.return_value = None
report = recovery.tick()
assert report.grab_reasserted is True
mock_root.grab_set_global.assert_called()
def test_noop_when_still_held(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""Holding the grab already means no work."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.return_value = mock_root
assert recovery.tick().grab_reasserted is False
def test_noop_when_our_own_child_holds_the_grab(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""A posted menu owns the grab; we must not steal it back.
Regression: the old ``grab_current() is root`` test read a Tk menu's
own grab as "lost" and re-grabbed a second later, which made the
screen-locker sport selector unusable while locked.
"""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.return_value = MagicMock(name="posted menu")
assert recovery.tick().grab_reasserted is False
mock_root.grab_set_global.assert_not_called()
def test_reasserts_when_the_holder_is_not_one_of_our_windows(
self,
loop: tuple[RecoveryLoop, MagicMock, SurfaceSet],
mock_root: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Fail closed: a name we cannot resolve is not provably ours.
A ``ttk.Combobox`` popdown is a Tcl-created toplevel absent from
Tkinter's widget map, so ``nametowidget`` raises ``KeyError``. Keeping
the lock beats keeping a banned widget's popdown.
"""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.side_effect = KeyError("!popdown")
with caplog.at_level(logging.WARNING):
assert recovery.tick().grab_reasserted is True
mock_root.grab_set_global.assert_called()
assert "not one of our windows" in caplog.text
def test_a_persistent_lost_grab_warns_once_not_once_per_tick(
self,
loop: tuple[RecoveryLoop, MagicMock, SurfaceSet],
mock_root: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A state that lasts the whole lock must not stream to the journal."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.side_effect = tk.TclError("gone")
with caplog.at_level(logging.WARNING):
for _ in range(5):
recovery.tick()
lost = [r for r in caplog.records if "current grab" in r.message]
assert len(lost) == 1, f"warned {len(lost)} times over 5 ticks"
def test_tclerror_while_regrabbing_is_swallowed(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""A failed re-grab is retried next tick, not raised."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.return_value = None
mock_root.grab_set_global.side_effect = tk.TclError("held")
assert recovery.tick().grab_reasserted is False
def test_grab_query_tclerror(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""A TclError querying the grab counts as not holding it."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.side_effect = tk.TclError("gone")
assert recovery.tick().grab_reasserted is True
def test_no_reassert_when_grab_not_global(self, mock_root: MagicMock) -> None:
"""Soft mode takes no grab, so there is nothing to re-assert."""
config = LockConfig(mode="soft")
surfaces = SurfaceSet(mock_root, config, MagicMock())
enumerator = MagicMock()
enumerator.scan.return_value = BOTH
recovery = RecoveryLoop(
mock_root,
RecoveryCollaborators(
config=config,
surfaces=surfaces,
enumerator=enumerator,
detector=MagicMock(),
hooks=MagicMock(),
),
)
assert recovery.tick().grab_reasserted is False
class TestFocusReassertion:
"""Re-focusing a surface that was (re)built after the first tick.
Regression coverage for the 2026-08 bug: a monitor blip tears down and
rebuilds a surface's widgets mid-lock, but ``LockWindow`` only ever
focuses the entry once, on first grab acquisition. Without this, the new
widget renders correctly but is never the Tk focus target, and the held
global grab means nothing else can take focus either -- a silent,
permanent input dead end that looks identical to a working lock.
"""
def test_refocuses_when_a_surface_is_recreated(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet]
) -> None:
"""A surface coming back live re-triggers on_focus_ready."""
recovery, enumerator, surfaces = loop
hooks = surfaces._builder
enumerator.scan.return_value = BOTH
recovery.tick()
hooks.on_focus_ready.reset_mock()
enumerator.scan.return_value = DEAD_PRIMARY
recovery.tick()
enumerator.scan.return_value = BOTH
recovery.tick()
hooks.on_focus_ready.assert_called_once()
def test_no_refocus_when_nothing_was_created(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet]
) -> None:
"""A tick with no new surfaces must not fight the user mid-typing."""
recovery, enumerator, surfaces = loop
hooks = surfaces._builder
enumerator.scan.return_value = BOTH
recovery.tick()
hooks.on_focus_ready.reset_mock()
recovery.tick()
hooks.on_focus_ready.assert_not_called()
def test_tclerror_during_refocus_is_swallowed(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""A focus call on a half-torn-down widget must not crash the tick."""
recovery, enumerator, surfaces = loop
hooks = surfaces._builder
hooks.on_focus_ready.side_effect = tk.TclError("gone")
enumerator.scan.return_value = BOTH
recovery.tick()
class TestVtReassertion:
"""Periodically re-disabling VT switching, never re-enabling it."""
def test_reasserts_on_the_thirtieth_tick(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet]
) -> None:
"""VT is re-disabled every 30 ticks."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
with patch("gatelock._recovery.disable_vt_switching") as vt:
reports = [recovery.tick() for _ in range(30)]
assert [i for i, r in enumerate(reports, 1) if r.vt_reasserted] == [30]
vt.assert_called_once_with()
def test_never_when_vt_not_managed(self, mock_root: MagicMock) -> None:
"""An app that does not disable VT never re-asserts it."""
config = LockConfig(mode="soft")
surfaces = SurfaceSet(mock_root, config, MagicMock())
enumerator = MagicMock()
enumerator.scan.return_value = BOTH
recovery = RecoveryLoop(
mock_root,
RecoveryCollaborators(
config=config,
surfaces=surfaces,
enumerator=enumerator,
detector=MagicMock(),
hooks=MagicMock(),
),
)
assert all(not recovery.tick().vt_reasserted for _ in range(31))
class TestScheduling:
"""The two cadences."""

View File

@ -0,0 +1,52 @@
"""Static checks that the recovery loop *cannot* weaken the lock.
Split from ``test_recovery.py`` (250-line cap). These read the source with
``ast`` rather than running anything: monotonicity is enforced by which calls
a module is allowed to contain at all, so the check belongs at the source
level where no amount of mocking can fake it.
"""
from __future__ import annotations
import ast
from pathlib import Path
WEAKENING_CALLS = frozenset(
{"grab_release", "restore_vt_switching", "close", "destroy", "quit", "withdraw"}
)
SOURCE_ROOT = Path(__file__).resolve().parent.parent
def symbols(module: str) -> set[str]:
"""Every attribute and name referenced in a module's source."""
tree = ast.parse((SOURCE_ROOT / module).read_text(encoding="utf-8"))
return {n.attr for n in ast.walk(tree) if isinstance(n, ast.Attribute)} | {
n.id for n in ast.walk(tree) if isinstance(n, ast.Name)
}
class TestStaticInvariant:
"""The separation that makes monotonicity mechanical rather than hoped-for."""
def test_recovery_contains_no_weakening_call(self) -> None:
"""_recovery.py must not be able to release, restore or destroy."""
assert symbols("_recovery.py") & WEAKENING_CALLS == set()
def test_recovery_may_still_strengthen(self) -> None:
"""The asymmetry is the point: strengthening calls ARE present."""
found = symbols("_recovery.py")
assert "disable_vt_switching" in found
assert "grab_set_global" in found
def test_surfaces_never_touches_grab_or_vt(self) -> None:
"""Window ownership and lock strength stay separate concerns."""
banned = {"grab_release", "grab_set", "grab_set_global", "restore_vt_switching"}
assert symbols("_surfaces.py") & banned == set()
def test_detect_thread_never_touches_tk(self) -> None:
"""Tk is not thread-safe; the RandR thread may only queue."""
source = (SOURCE_ROOT / "_detect.py").read_text(encoding="utf-8")
loop_body = source.split("def _loop(")[1].split("def ")[0]
assert "self._sink.put" in loop_body
for forbidden in ("geometry(", "deiconify(", "lift(", "grab_"):
assert forbidden not in loop_body

View File

@ -0,0 +1,196 @@
"""Tests for what the recovery loop re-asserts on each tick.
Split from ``test_recovery.py`` (250-line cap). That file keeps the static
invariant, the 2026-07-25 monotonic-sequence walk, the tick mechanics and
scheduling; this one covers the three things a tick re-asserts -- the input
grab, focus, and VT switching -- and the branches within each.
The ``loop`` fixture and the output scans both halves drive come from
``conftest.py``.
"""
from __future__ import annotations
import logging
import tkinter as tk
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from gatelock._window import LockConfig
from gatelock.tests.conftest import BOTH, DEAD_PRIMARY, build_loop
if TYPE_CHECKING:
import pytest
from gatelock._recovery import RecoveryLoop
from gatelock._surfaces import SurfaceSet
class TestGrabReassertion:
"""Re-taking a lost grab, never releasing one."""
def test_reasserts_when_lost(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""Something stole the grab; we take it back."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.return_value = None
report = recovery.tick()
assert report.grab_reasserted is True
mock_root.grab_set_global.assert_called()
def test_noop_when_still_held(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""Holding the grab already means no work."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.return_value = mock_root
assert recovery.tick().grab_reasserted is False
def test_noop_when_our_own_child_holds_the_grab(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""A posted menu owns the grab; we must not steal it back.
Regression: the old ``grab_current() is root`` test read a Tk menu's
own grab as "lost" and re-grabbed a second later, which made the
screen-locker sport selector unusable while locked.
"""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.return_value = MagicMock(name="posted menu")
assert recovery.tick().grab_reasserted is False
mock_root.grab_set_global.assert_not_called()
def test_reasserts_when_the_holder_is_not_one_of_our_windows(
self,
loop: tuple[RecoveryLoop, MagicMock, SurfaceSet],
mock_root: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Fail closed: a name we cannot resolve is not provably ours.
A ``ttk.Combobox`` popdown is a Tcl-created toplevel absent from
Tkinter's widget map, so ``nametowidget`` raises ``KeyError``. Keeping
the lock beats keeping a banned widget's popdown.
"""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.side_effect = KeyError("!popdown")
with caplog.at_level(logging.WARNING):
assert recovery.tick().grab_reasserted is True
mock_root.grab_set_global.assert_called()
assert "not one of our windows" in caplog.text
def test_a_persistent_lost_grab_warns_once_not_once_per_tick(
self,
loop: tuple[RecoveryLoop, MagicMock, SurfaceSet],
mock_root: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A state that lasts the whole lock must not stream to the journal."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.side_effect = tk.TclError("gone")
with caplog.at_level(logging.WARNING):
for _ in range(5):
recovery.tick()
lost = [r for r in caplog.records if "current grab" in r.message]
assert len(lost) == 1, f"warned {len(lost)} times over 5 ticks"
def test_tclerror_while_regrabbing_is_swallowed(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""A failed re-grab is retried next tick, not raised."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.return_value = None
mock_root.grab_set_global.side_effect = tk.TclError("held")
assert recovery.tick().grab_reasserted is False
def test_grab_query_tclerror(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""A TclError querying the grab counts as not holding it."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
mock_root.grab_current.side_effect = tk.TclError("gone")
assert recovery.tick().grab_reasserted is True
def test_no_reassert_when_grab_not_global(self, mock_root: MagicMock) -> None:
"""Soft mode takes no grab, so there is nothing to re-assert."""
recovery, enumerator, _surfaces = build_loop(mock_root, LockConfig(mode="soft"))
enumerator.scan.return_value = BOTH
assert recovery.tick().grab_reasserted is False
class TestFocusReassertion:
"""Re-focusing a surface that was (re)built after the first tick.
Regression coverage for the 2026-08 bug: a monitor blip tears down and
rebuilds a surface's widgets mid-lock, but ``LockWindow`` only ever
focuses the entry once, on first grab acquisition. Without this, the new
widget renders correctly but is never the Tk focus target, and the held
global grab means nothing else can take focus either -- a silent,
permanent input dead end that looks identical to a working lock.
"""
def test_refocuses_when_a_surface_is_recreated(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet]
) -> None:
"""A surface coming back live re-triggers on_focus_ready."""
recovery, enumerator, surfaces = loop
hooks = surfaces._builder
enumerator.scan.return_value = BOTH
recovery.tick()
hooks.on_focus_ready.reset_mock()
enumerator.scan.return_value = DEAD_PRIMARY
recovery.tick()
enumerator.scan.return_value = BOTH
recovery.tick()
hooks.on_focus_ready.assert_called_once()
def test_no_refocus_when_nothing_was_created(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet]
) -> None:
"""A tick with no new surfaces must not fight the user mid-typing."""
recovery, enumerator, surfaces = loop
hooks = surfaces._builder
enumerator.scan.return_value = BOTH
recovery.tick()
hooks.on_focus_ready.reset_mock()
recovery.tick()
hooks.on_focus_ready.assert_not_called()
def test_tclerror_during_refocus_is_swallowed(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet], mock_root: MagicMock
) -> None:
"""A focus call on a half-torn-down widget must not crash the tick."""
recovery, enumerator, surfaces = loop
hooks = surfaces._builder
hooks.on_focus_ready.side_effect = tk.TclError("gone")
enumerator.scan.return_value = BOTH
recovery.tick()
class TestVtReassertion:
"""Periodically re-disabling VT switching, never re-enabling it."""
def test_reasserts_on_the_thirtieth_tick(
self, loop: tuple[RecoveryLoop, MagicMock, SurfaceSet]
) -> None:
"""VT is re-disabled every 30 ticks."""
recovery, enumerator, _surfaces = loop
enumerator.scan.return_value = BOTH
with patch("gatelock._recovery.disable_vt_switching") as vt:
reports = [recovery.tick() for _ in range(30)]
assert [i for i, r in enumerate(reports, 1) if r.vt_reasserted] == [30]
vt.assert_called_once_with()
def test_never_when_vt_not_managed(self, mock_root: MagicMock) -> None:
"""An app that does not disable VT never re-asserts it."""
recovery, enumerator, _surfaces = build_loop(mock_root, LockConfig(mode="soft"))
enumerator.scan.return_value = BOTH
assert all(not recovery.tick().vt_reasserted for _ in range(31))