150 lines
4.7 KiB
Python
150 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
import threading
|
|
from datetime import UTC, datetime
|
|
|
|
from host_agent.mcp_lock import McpBusyTracker
|
|
|
|
|
|
def _tracker_with_now() -> tuple[McpBusyTracker, list[datetime]]:
|
|
times: list[datetime] = []
|
|
|
|
def now() -> datetime:
|
|
return times[-1] if times else datetime(2026, 1, 1, tzinfo=UTC)
|
|
|
|
tracker = McpBusyTracker(ttl_seconds=60.0, now=now)
|
|
return tracker, times
|
|
|
|
|
|
def test_acquire_succeeds_on_empty() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
assert tracker.acquire("phone-1", "sess-a") is True
|
|
assert "phone-1" in tracker.busy_device_ids()
|
|
|
|
|
|
def test_acquire_fails_when_held_by_other_session() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
assert tracker.acquire("phone-1", "sess-a") is True
|
|
assert tracker.acquire("phone-1", "sess-b") is False
|
|
|
|
|
|
def test_acquire_is_idempotent_for_same_session() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
assert tracker.acquire("phone-1", "sess-a") is True
|
|
# Same session re-acquiring is allowed (acts as renew).
|
|
assert tracker.acquire("phone-1", "sess-a") is True
|
|
|
|
|
|
def test_renew_refreshes_last_seen() -> None:
|
|
tracker, times = _tracker_with_now()
|
|
times.append(datetime(2026, 1, 1, 12, 0, tzinfo=UTC))
|
|
tracker.acquire("phone-1", "sess-a")
|
|
initial = tracker.snapshot()[0]
|
|
times.append(datetime(2026, 1, 1, 12, 0, 30, tzinfo=UTC))
|
|
assert tracker.renew("phone-1", "sess-a") is True
|
|
refreshed = tracker.snapshot()[0]
|
|
assert refreshed.last_seen_at > initial.last_seen_at
|
|
|
|
|
|
def test_renew_fails_when_held_by_other() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
tracker.acquire("phone-1", "sess-a")
|
|
assert tracker.renew("phone-1", "sess-b") is False
|
|
|
|
|
|
def test_release_returns_freed_device_ids() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
tracker.acquire("phone-1", "sess-a")
|
|
tracker.acquire("phone-2", "sess-a")
|
|
freed = tracker.release("sess-a")
|
|
assert sorted(freed) == ["phone-1", "phone-2"]
|
|
assert tracker.busy_device_ids() == []
|
|
|
|
|
|
def test_release_only_frees_caller_session() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
tracker.acquire("phone-1", "sess-a")
|
|
tracker.acquire("phone-1", "sess-b") # fails
|
|
freed = tracker.release("sess-b")
|
|
assert freed == []
|
|
assert "phone-1" in tracker.busy_device_ids()
|
|
|
|
|
|
def test_ttl_sweeps_expired_leases() -> None:
|
|
tracker, times = _tracker_with_now()
|
|
times.append(datetime(2026, 1, 1, 12, 0, tzinfo=UTC))
|
|
tracker.acquire("phone-1", "sess-a")
|
|
# Advance past TTL without renew.
|
|
times.append(datetime(2026, 1, 1, 12, 1, 1, tzinfo=UTC)) # 61s later
|
|
assert tracker.busy_device_ids() == []
|
|
|
|
|
|
def test_renew_after_ttl_tolerates_same_session() -> None:
|
|
"""Scene 10: lease expired but session_id matches -> re-acquire."""
|
|
tracker, times = _tracker_with_now()
|
|
times.append(datetime(2026, 1, 1, 12, 0, tzinfo=UTC))
|
|
tracker.acquire("phone-1", "sess-a")
|
|
times.append(datetime(2026, 1, 1, 12, 1, 1, tzinfo=UTC)) # expired
|
|
# renew from the same session should succeed (re-acquire).
|
|
assert tracker.renew("phone-1", "sess-a") is True
|
|
assert "phone-1" in tracker.busy_device_ids()
|
|
|
|
|
|
def test_snapshot_matches_busy_device_ids() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
tracker.acquire("phone-1", "sess-a")
|
|
tracker.acquire("phone-2", "sess-a")
|
|
snap = tracker.snapshot()
|
|
assert {lease.device_id for lease in snap} == set(tracker.busy_device_ids())
|
|
|
|
|
|
def test_wait_until_usable_succeeds_when_free() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
ok = tracker.wait_until_usable(
|
|
"phone-1", "sess-a", timeout=1.0, poll_interval=0.01
|
|
)
|
|
assert ok is True
|
|
assert "phone-1" in tracker.busy_device_ids()
|
|
|
|
|
|
def test_wait_until_usable_returns_false_on_timeout() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
tracker.acquire("phone-1", "sess-a")
|
|
ok = tracker.wait_until_usable(
|
|
"phone-1", "sess-b", timeout=0.1, poll_interval=0.02
|
|
)
|
|
assert ok is False
|
|
|
|
|
|
def test_wait_until_usable_blocks_then_succeeds_when_released() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
tracker.acquire("phone-1", "sess-a")
|
|
|
|
def releaser() -> None:
|
|
import time
|
|
|
|
time.sleep(0.05)
|
|
tracker.release("sess-a")
|
|
|
|
t = threading.Thread(target=releaser)
|
|
t.start()
|
|
try:
|
|
ok = tracker.wait_until_usable(
|
|
"phone-1", "sess-b", timeout=2.0, poll_interval=0.02
|
|
)
|
|
assert ok is True
|
|
finally:
|
|
t.join()
|
|
|
|
|
|
def test_wait_until_usable_blocks_then_fails_when_cloud_remains_busy() -> None:
|
|
tracker, _ = _tracker_with_now()
|
|
ok = tracker.wait_until_usable(
|
|
"phone-1",
|
|
"sess-a",
|
|
timeout=0.1,
|
|
poll_interval=0.02,
|
|
cloud_busy_check=lambda: True,
|
|
)
|
|
assert ok is False
|
|
assert tracker.busy_device_ids() == [] |