testsAndMisc/python_pkg/android_ui/tests/test_cli_display.py
Krzysztof kuhy Rudnicki aa37852296 phone_guard: lease or block raw adb; android_ui drives a session's own virtual display
A raw `adb shell input tap` took no lease, so one session's tap landed in
another session's app (2026-09-25). python_pkg.phone_guard classifies every
adb call in a Bash command and, as the Claude Code PreToolUse hook, takes
the matching phone_lease (one app / real screen / whole phone) or exits 2:
key/text input without -d, input on another session's virtual display, or
a lease still held elsewhere after 40 s.

android_ui gains --display self|<id>: it drives only this session's
phone_vd display under that app's lease, and every input carries -d.
uiautomator sees display 0 only, so virtual-display trees come from a tiny
instrumentation APK (A11yDump.java) built from source without Gradle.

Prunes the oldest evidence artifact to stay under the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpA6wW2DKiovQADTdjWJoH
2026-09-25 16:09:45 +02:00

84 lines
2.9 KiB
Python

"""Tests for ``android_ui --display``: which display a call may drive.
The real screen is shared, so it takes the screen lease; a session's own
virtual display takes only its app's lease; anyone else's display is refused
before the driver or a lease is touched.
"""
from __future__ import annotations
from unittest.mock import patch
import pytest
from python_pkg.android_ui import cli
from python_pkg.phone_guard.vd_state import SessionDisplay
MOD = "python_pkg.android_ui.cli"
SCREEN_SCOPE = "pkg:__screen__"
pytestmark = pytest.mark.usefixtures("phone")
def _mine(display: int = 9, package: str = "dev.kuhy.todo") -> SessionDisplay:
return SessionDisplay("claude_1", display, "sf", package)
class TestDisplay:
def test_self_drives_this_sessions_display_under_its_app_lease(self) -> None:
with (
patch(f"{MOD}.own_display", return_value=_mine()),
patch(f"{MOD}.AndroidUi") as factory,
patch(f"{MOD}.acquire") as acquire,
):
assert cli.main(["--display", "self", "focus"]) == 0
factory.assert_called_once_with(serial="SER1", display=9)
assert acquire.call_args.kwargs["scope"] == "pkg:dev.kuhy.todo"
def test_an_explicit_id_of_ones_own_display_works(self) -> None:
with (
patch(f"{MOD}.own_display", return_value=_mine()),
patch(f"{MOD}.AndroidUi") as factory,
patch(f"{MOD}.acquire"),
):
assert cli.main(["--display", "9", "focus"]) == 0
assert factory.call_args.kwargs["display"] == 9
def test_display_0_is_the_real_screen(self) -> None:
with patch(f"{MOD}.AndroidUi") as factory, patch(f"{MOD}.acquire") as acquire:
assert cli.main(["--display", "0", "focus"]) == 0
assert factory.call_args.kwargs["display"] is None
assert acquire.call_args.kwargs["scope"] == SCREEN_SCOPE
@pytest.mark.parametrize(
("requested", "mine", "other", "needle"),
[
("self", None, None, "phone_vd.sh start"),
("abc", None, None, "display id or 'self'"),
("7", None, None, "not this session's"),
(
"7",
_mine(9),
SessionDisplay("claude_2", 7, "sf", "x"),
"owned by claude_2",
),
],
)
def test_refuses_what_is_not_this_sessions(
self,
capsys: pytest.CaptureFixture[str],
requested: str,
mine: SessionDisplay | None,
other: SessionDisplay | None,
needle: str,
) -> None:
with (
patch(f"{MOD}.own_display", return_value=mine),
patch(f"{MOD}.display_owner", return_value=other),
patch(f"{MOD}.AndroidUi") as factory,
patch(f"{MOD}.acquire") as acquire,
):
assert cli.main(["--display", requested, "focus"]) == 5
factory.assert_not_called()
acquire.assert_not_called()
assert needle in capsys.readouterr().err