Add opt-in Tentacle protocol 1.1, explicit remote grants, lifecycle IPC and expiring core leases in both SDKs. Revoke access before update preparation, including failed updates. Add transient secret entries, shared Braille cell export and isolated Xorg recovery experiments with focused tests and documented limitations. Production consent, desktop services and live interoperability remain pending.
81 lines
4.2 KiB
Python
81 lines
4.2 KiB
Python
"""Private GTK update confirmation and keyboard workflow; no installed packages."""
|
|
import select
|
|
|
|
from native_x11 import wait_for
|
|
|
|
|
|
def update_snapshot():
|
|
repositoryUrl = "https://example.org/tentacles"
|
|
return {
|
|
"Entries": [{"Entry": {
|
|
"ID": "probe", "LocalID": "echo", "Name": "Update probe",
|
|
"Repository": repositoryUrl, "RepositoryID": "example.source",
|
|
"Version": "1.0", "Enabled": True, "Presentation": True,
|
|
"SessionEvents": True, "RemoteGrants": ["remote-present"],
|
|
}, "State": "active"}],
|
|
"Repositories": [{"URL": repositoryUrl, "ID": "example.source", "Name": "Example",
|
|
"Tentacles": [{"ID": "echo", "Name": "Update probe", "Version": "1.1",
|
|
"Capabilities": ["presentation", "interaction", "output-observe", "keyboard-inject"]}]}],
|
|
"Updates": [{"ID": "probe", "LocalID": "echo", "Name": "Update probe",
|
|
"InstalledVersion": "1.0", "AvailableVersion": "1.1",
|
|
"RepositoryURL": repositoryUrl, "RepositoryID": "example.source"}],
|
|
}
|
|
|
|
|
|
def check_page(pyatspi, process, window, named, walk, focus, key, request, send, initial):
|
|
tab = next(node for node in walk(window)
|
|
if node.name == "Tentacles" and node.getRole() == pyatspi.ROLE_PAGE_TAB)
|
|
assert tab.parent.querySelection().selectChild(tab.getIndexInParent()), "cannot select Tentacles tab"
|
|
table = next(node for node in walk(window)
|
|
if node.name == "Tentacles" and node.getRole() == pyatspi.ROLE_TABLE)
|
|
focus(table)
|
|
key("Home")
|
|
update = named("Update")
|
|
wait_for(lambda: update.getState().contains(pyatspi.STATE_SENSITIVE), "Update did not enable")
|
|
# Tab from the table skips disabled Install and reaches Update.
|
|
key("Tab")
|
|
wait_for(lambda: update.getState().contains(pyatspi.STATE_FOCUSED), "Tab did not reach Update")
|
|
key("Tab")
|
|
wait_for(lambda: named("Settings").getState().contains(pyatspi.STATE_FOCUSED), "Update trapped Tab")
|
|
key("shift+Tab")
|
|
wait_for(lambda: update.getState().contains(pyatspi.STATE_FOCUSED), "Shift+Tab did not reach Update")
|
|
|
|
def confirmation():
|
|
for app in pyatspi.Registry.getDesktop(0):
|
|
for node in app:
|
|
if node and node.getRole() in (pyatspi.ROLE_DIALOG, pyatspi.ROLE_ALERT):
|
|
if any("Current capabilities:" in child.name for child in walk(node)):
|
|
return node
|
|
return None
|
|
|
|
key("space")
|
|
dialog = wait_for(confirmation, "keyboard activation did not open confirmation")
|
|
message = " ".join(node.name for node in walk(dialog))
|
|
assert "version 1.0 to version 1.1" in message, message
|
|
assert "Current capabilities: presentation, remote-present." in message, message
|
|
assert "Repository declares: presentation, interaction, output-observe, keyboard-inject." in message, message
|
|
assert "accept keyboard input from the remote controller" in message, message
|
|
assert "Each session requires local approval" in message, message
|
|
cancel = next(node for node in walk(dialog) if node.name == "Cancel")
|
|
focus(cancel)
|
|
key("space")
|
|
wait_for(lambda: not confirmation(), "Cancel did not close confirmation")
|
|
assert not select.select([process.stdout], [], [], 0.2)[0], "Cancel sent a management request"
|
|
|
|
# The native button action uses the same path as keyboard activation.
|
|
update.queryAction().doAction(0)
|
|
dialog = wait_for(confirmation, "button action did not open confirmation")
|
|
accept = next(node for node in walk(dialog)
|
|
if node.name == "Update" and node.getRole() == pyatspi.ROLE_PUSH_BUTTON)
|
|
focus(accept)
|
|
key("Return")
|
|
value = request()
|
|
assert value["Tentacle"] == {"Action": "upgrade", "ID": "echo", "Entry": None,
|
|
"URL": "https://example.org/tentacles"}, value["Tentacle"]
|
|
send(dict(initial, Message="Update accepted."))
|
|
wait_for(lambda: update.getState().contains(pyatspi.STATE_SENSITIVE), "response left controls busy")
|
|
focus(named("Cancel"))
|
|
key("space")
|
|
assert process.wait(timeout=5) == 0
|
|
print("PASS: Update Tab/Shift+Tab, keyboard/button activation, versions/capabilities, Cancel, upgrade request")
|