Files
agent-desktop/tests/unit/plugins/computer_use/test_capability_recovery.py
T

145 lines
5.0 KiB
Python

# -*- coding: utf-8 -*-
"""Recovering after the native helper goes away.
The capability names a pipe or socket the helper is listening on. It used to be
cached for the life of the process with no way to retire it, so a helper that
crashed left the client reconnecting to a name that answered nothing -- for
every request thereafter, until the backend restarted. The desktop host notices
the dead child and will issue a new capability, but only if asked.
"""
from __future__ import annotations
import pytest
from pineagents.app.computer_use import runtime as runtime_module
from pineagents.app.computer_use.runtime import (
HostRuntimeProvider,
RuntimeCapability,
)
def _capability(pipe: str) -> RuntimeCapability:
return RuntimeCapability(
_pipe_name=pipe,
_secret="secret",
protocol_version=1,
)
@pytest.fixture(autouse=True)
def _clean_provider(monkeypatch: pytest.MonkeyPatch):
"""Start from a provider that holds nothing and sees no environment."""
monkeypatch.setattr(HostRuntimeProvider, "_capability", None)
monkeypatch.setattr(HostRuntimeProvider, "_environment_spent", False)
for name in (
"QWENPAW_COMPUTER_USE_PIPE",
"QWENPAW_COMPUTER_USE_CAPABILITY",
"QWENPAW_COMPUTER_USE_PROTOCOL",
):
monkeypatch.delenv(name, raising=False)
yield
monkeypatch.setattr(HostRuntimeProvider, "_capability", None)
monkeypatch.setattr(HostRuntimeProvider, "_environment_spent", False)
def test_a_dead_endpoint_is_replaced_by_asking_the_host_again(
monkeypatch: pytest.MonkeyPatch,
) -> None:
issued = iter([_capability("pipe-first"), _capability("pipe-second")])
monkeypatch.setattr(
runtime_module,
"_control_endpoint",
object,
)
monkeypatch.setattr(
runtime_module,
"_request_capability",
lambda _control: next(issued),
)
first = HostRuntimeProvider.acquire_capability()
assert first is not None
assert first.names_same_endpoint(_capability("pipe-first"))
# Cached, so the host is not troubled again while it works.
assert HostRuntimeProvider.acquire_capability() is first
HostRuntimeProvider.invalidate_capability(first)
second = HostRuntimeProvider.acquire_capability()
assert second is not None
assert second.names_same_endpoint(_capability("pipe-second"))
def test_invalidating_an_endpoint_already_replaced_keeps_the_new_one(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Two clients can fail over the same dead helper at once.
The second one must not discard the replacement the first already obtained,
or they would take turns invalidating each other's capability.
"""
monkeypatch.setattr(runtime_module, "_control_endpoint", object)
monkeypatch.setattr(
runtime_module,
"_request_capability",
lambda _control: _capability("pipe-new"),
)
HostRuntimeProvider.acquire_capability()
HostRuntimeProvider.invalidate_capability(_capability("pipe-old"))
held = HostRuntimeProvider.get_capability()
assert held is not None
assert held.names_same_endpoint(_capability("pipe-new"))
def test_a_spent_environment_capability_stops_being_returned(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The environment value is a bootstrap, not a permanent answer.
It is handed over when the backend is spawned. Once its endpoint is gone,
returning it again would loop forever, since clearing the cached capability
cannot clear an environment variable.
"""
monkeypatch.setenv("QWENPAW_COMPUTER_USE_PIPE", "pipe-from-env")
monkeypatch.setenv("QWENPAW_COMPUTER_USE_CAPABILITY", "secret")
monkeypatch.setenv("QWENPAW_COMPUTER_USE_PROTOCOL", "1")
injected = HostRuntimeProvider.get_capability()
assert injected is not None
assert injected.names_same_endpoint(_capability("pipe-from-env"))
HostRuntimeProvider.invalidate_capability(injected)
assert HostRuntimeProvider.get_capability() is None
# And the next acquire goes to the host rather than back to the
# environment.
monkeypatch.setattr(runtime_module, "_control_endpoint", object)
monkeypatch.setattr(
runtime_module,
"_request_capability",
lambda _control: _capability("pipe-fresh"),
)
replacement = HostRuntimeProvider.acquire_capability()
assert replacement is not None
assert replacement.names_same_endpoint(_capability("pipe-fresh"))
def test_an_incompatible_desktop_capability_is_not_exposed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("QWENPAW_COMPUTER_USE_PIPE", "pipe-old")
monkeypatch.setenv("QWENPAW_COMPUTER_USE_CAPABILITY", "secret")
monkeypatch.setenv("QWENPAW_COMPUTER_USE_PROTOCOL", "2")
assert HostRuntimeProvider.get_capability() is None
def test_no_host_and_no_environment_means_no_capability(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(runtime_module, "_control_endpoint", lambda: None)
assert HostRuntimeProvider.acquire_capability() is None