145 lines
5.0 KiB
Python
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
|