mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
Two sessions driving the same phone interleave their taps: on 2026-09-20 a tap meant for the workout sandbox opened another session's manga reader, twice. The lease is a flock-guarded JSON file per serial under ~/.cache/phone-lease/, owned by the nearest ancestor `claude` process (one per session), refreshed on every android_ui call and expiring 180 s after the last one, so a quiet session frees the phone without anyone remembering to. A foreign fresh lease makes a call wait up to 60 s, then exit 3 naming the holder and pointing at the alternatives (the phone's virtual display, the headless emulator) instead of inviting a retry loop. phone_deploy.sh takes the same lease (exit 40). The oldest evidence artifact is pruned to stay under the 60-file cap; it remains in history. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YbBZqUaNWyhHYfzz6zGC7q
194 lines
6.5 KiB
Python
194 lines
6.5 KiB
Python
"""The lease: owner identity, atomic take/refresh, expiry, release, status."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import time
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from python_pkg.phone_lease import (
|
|
Clock,
|
|
Lease,
|
|
PhoneBusyError,
|
|
acquire,
|
|
owner_id,
|
|
release,
|
|
status,
|
|
)
|
|
from python_pkg.phone_lease import lease as lease_mod
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable, Iterator
|
|
|
|
|
|
class FakeClock:
|
|
"""A clock `acquire` can be handed instead of `time`."""
|
|
|
|
def __init__(self, now: float = 1000.0) -> None:
|
|
self.now = now
|
|
self.slept: list[float] = []
|
|
|
|
def time(self) -> float:
|
|
return self.now
|
|
|
|
def sleep(self, secs: float) -> None:
|
|
self.slept.append(secs)
|
|
self.now += secs
|
|
|
|
def as_clock(self) -> Clock:
|
|
return Clock(now=self.time, sleep=self.sleep)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def lease_dir(tmp_path: Path) -> Iterator[Path]:
|
|
"""Every test gets its own lease directory."""
|
|
with patch.object(lease_mod, "LEASE_DIR", tmp_path / "leases"):
|
|
yield tmp_path / "leases"
|
|
|
|
|
|
class TestOwnerId:
|
|
def test_explicit_env_wins(self) -> None:
|
|
assert owner_id(environ={"PHONE_LEASE_OWNER": "ci-run-7"}) == "ci-run-7"
|
|
|
|
def test_finds_the_claude_ancestor(self, tmp_path: Path) -> None:
|
|
proc = tmp_path / "proc"
|
|
for pid, comm, ppid in (
|
|
(300, "zsh", 200),
|
|
(200, "claude", 100),
|
|
(100, "systemd", 1),
|
|
):
|
|
d = proc / str(pid)
|
|
d.mkdir(parents=True)
|
|
(d / "comm").write_text(f"{comm}\n")
|
|
(d / "stat").write_text(f"{pid} ({comm}) S {ppid} 1 1 0 -1\n")
|
|
with patch.object(lease_mod.Path, "read_text", _proc_reader(proc)):
|
|
assert owner_id(pid=300, environ={}) == "claude:200"
|
|
|
|
def test_falls_back_to_own_pid_without_a_claude_ancestor(
|
|
self, tmp_path: Path
|
|
) -> None:
|
|
proc = tmp_path / "proc"
|
|
d = proc / "300"
|
|
d.mkdir(parents=True)
|
|
(d / "comm").write_text("zsh\n")
|
|
(d / "stat").write_text("300 (zsh) S 1 1 1 0 -1\n")
|
|
with patch.object(lease_mod.Path, "read_text", _proc_reader(proc)):
|
|
assert owner_id(pid=300, environ={}) == "pid:300"
|
|
|
|
def test_unreadable_proc_falls_back_to_own_pid(self) -> None:
|
|
assert owner_id(pid=2**22 + 12345, environ={}) == f"pid:{2**22 + 12345}"
|
|
|
|
def test_defaults_to_this_process(self) -> None:
|
|
assert owner_id(environ={}).split(":")[0] in {"claude", "pid"}
|
|
|
|
|
|
def _proc_reader(proc: Path) -> Callable[..., str]:
|
|
real = Path.read_text
|
|
|
|
def read_text(self: Path, *args: object, **kwargs: object) -> str:
|
|
if str(self).startswith("/proc/"):
|
|
return real(proc / str(self)[len("/proc/") :], *args, **kwargs)
|
|
return real(self, *args, **kwargs)
|
|
|
|
return read_text
|
|
|
|
|
|
class TestAcquire:
|
|
def test_takes_a_free_phone(self, lease_dir: Path) -> None:
|
|
clock = FakeClock()
|
|
got = acquire(
|
|
"PIXEL", "deploy", owner="claude:1", ttl=10, clock=clock.as_clock()
|
|
)
|
|
assert got == Lease(
|
|
serial="PIXEL", owner="claude:1", expires=1010.0, note="deploy"
|
|
)
|
|
assert (lease_dir / "PIXEL.json").exists()
|
|
|
|
def test_refreshes_the_same_owner(self) -> None:
|
|
clock = FakeClock()
|
|
acquire("PIXEL", "a", owner="claude:1", ttl=10, clock=clock.as_clock())
|
|
clock.now += 5
|
|
got = acquire("PIXEL", "b", owner="claude:1", ttl=10, clock=clock.as_clock())
|
|
assert got.expires == 1015.0
|
|
assert got.note == "b"
|
|
assert clock.slept == []
|
|
|
|
def test_waits_for_a_foreign_lease_to_expire(self) -> None:
|
|
clock = FakeClock()
|
|
acquire("PIXEL", "other", owner="claude:2", ttl=3, clock=clock.as_clock())
|
|
got = acquire(
|
|
"PIXEL", "mine", owner="claude:1", ttl=10, wait=60, clock=clock.as_clock()
|
|
)
|
|
assert got.owner == "claude:1"
|
|
assert clock.slept == [1.0, 1.0, 1.0]
|
|
|
|
def test_gives_up_naming_the_holder(self) -> None:
|
|
clock = FakeClock()
|
|
acquire("PIXEL", "manga", owner="claude:2", ttl=1000, clock=clock.as_clock())
|
|
with pytest.raises(
|
|
PhoneBusyError, match=r"held by claude:2 \(manga\).*waited 2s"
|
|
):
|
|
acquire("PIXEL", "mine", owner="claude:1", wait=2, clock=clock.as_clock())
|
|
|
|
def test_uses_the_real_clock_by_default(self) -> None:
|
|
got = acquire("PIXEL", owner="claude:1", ttl=5)
|
|
assert got.expires == pytest.approx(time.time() + 5, abs=2)
|
|
|
|
def test_uses_the_real_owner_by_default(self) -> None:
|
|
assert acquire("PIXEL").owner == owner_id()
|
|
|
|
def test_serial_with_slash_is_a_safe_filename(self, lease_dir: Path) -> None:
|
|
acquire("192.168.1.5:5555/x", owner="claude:1")
|
|
assert (lease_dir / "192.168.1.5:5555_x.json").exists()
|
|
|
|
|
|
class TestCorruptLeaseFile:
|
|
def test_garbage_is_treated_as_free(self, lease_dir: Path) -> None:
|
|
lease_dir.mkdir()
|
|
(lease_dir / "PIXEL.json").write_text("not json")
|
|
assert acquire("PIXEL", owner="claude:1").owner == "claude:1"
|
|
|
|
def test_wrong_shape_is_treated_as_free(self, lease_dir: Path) -> None:
|
|
lease_dir.mkdir()
|
|
(lease_dir / "PIXEL.json").write_text('{"unexpected": 1}')
|
|
assert status("PIXEL") is None
|
|
|
|
|
|
class TestRelease:
|
|
def test_drops_own_lease(self) -> None:
|
|
acquire("PIXEL", owner="claude:1")
|
|
assert release("PIXEL", owner="claude:1") is True
|
|
assert status("PIXEL") is None
|
|
|
|
def test_leaves_a_foreign_lease_alone(self) -> None:
|
|
acquire("PIXEL", owner="claude:2")
|
|
assert release("PIXEL", owner="claude:1") is False
|
|
assert status("PIXEL") is not None
|
|
|
|
def test_nothing_to_release(self) -> None:
|
|
assert release("PIXEL", owner="claude:1") is False
|
|
|
|
def test_defaults_to_this_owner(self) -> None:
|
|
acquire("PIXEL")
|
|
assert release("PIXEL") is True
|
|
|
|
|
|
class TestStatus:
|
|
def test_free(self) -> None:
|
|
assert status("PIXEL") is None
|
|
|
|
def test_expired_is_free(self) -> None:
|
|
acquire("PIXEL", owner="claude:1", ttl=1, clock=FakeClock(now=100.0).as_clock())
|
|
assert status("PIXEL", now=200.0) is None
|
|
|
|
def test_live_describes_the_holder(self) -> None:
|
|
acquire("PIXEL", "deploy", owner="claude:1", ttl=1000)
|
|
live = status("PIXEL")
|
|
assert live is not None
|
|
assert live.describe().startswith(
|
|
"phone PIXEL held by claude:1 (deploy) until "
|
|
)
|