mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
test: cover answers from the notification
Three seams: the offer is known when the wait starts (a confirm and a question, with and without the platform capability), a button pressed after the notification appears answers the tool confirm it belongs to — a pty stands in for the terminal so the read has to consult both surfaces — and `confirm()` takes an answer passed in and one offered by the wait hook, without asking the backend.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import sys
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -253,6 +254,60 @@ def test_tool_confirm_denied_by_the_notification() -> None:
|
|||||||
assert prompt_confirm_tool("run_argv", {}, "runs a command") is False
|
assert prompt_confirm_tool("run_argv", {}, "runs a command") is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.platform == "win32", reason="a pty is POSIX-only")
|
||||||
|
def test_a_button_replaces_the_terminal_read(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""A button pressed in the notification answers the waiting prompt.
|
||||||
|
|
||||||
|
The read consults stdin *and* the notification while it blocks: the button
|
||||||
|
only exists once the notification is shown, which is after the read began.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import pty
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from plyngent.cli.notify import ALLOW, DesktopRequest, WaitNotifier
|
||||||
|
from plyngent.config.models import NotifyConfig
|
||||||
|
from plyngent.prompting import set_prompt_wait_hook
|
||||||
|
|
||||||
|
class PressesTheButton:
|
||||||
|
"""A desktop surface that answers once the notification is up."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.requests: list[DesktopRequest] = []
|
||||||
|
|
||||||
|
def capabilities(self) -> frozenset[str]:
|
||||||
|
return frozenset({"buttons", "reply"})
|
||||||
|
|
||||||
|
def send(self, request: DesktopRequest) -> str | None:
|
||||||
|
self.requests.append(request)
|
||||||
|
answer = request.on_answer
|
||||||
|
if answer is not None:
|
||||||
|
threading.Timer(0.2, answer, args=(ALLOW,)).start()
|
||||||
|
return "id-1"
|
||||||
|
|
||||||
|
def clear(self, identifier: str | None) -> None:
|
||||||
|
del identifier
|
||||||
|
|
||||||
|
desktop = PressesTheButton()
|
||||||
|
notifier = WaitNotifier(
|
||||||
|
NotifyConfig(grace_seconds=0.0, min_interval_seconds=0.0),
|
||||||
|
desktop=desktop,
|
||||||
|
write=lambda _sequence: None,
|
||||||
|
)
|
||||||
|
master, slave = pty.openpty()
|
||||||
|
stream = os.fdopen(slave)
|
||||||
|
set_prompt_wait_hook(notifier)
|
||||||
|
monkeypatch.setattr(sys, "stdin", stream)
|
||||||
|
try:
|
||||||
|
with temporary_backend(ScriptedBackend([])):
|
||||||
|
assert prompt_confirm_tool("run_argv", {}, "runs a command") is True
|
||||||
|
finally:
|
||||||
|
set_prompt_wait_hook(None)
|
||||||
|
stream.close()
|
||||||
|
os.close(master)
|
||||||
|
assert desktop.requests
|
||||||
|
|
||||||
|
|
||||||
def test_batch_confirm_takes_a_notification_answer() -> None:
|
def test_batch_confirm_takes_a_notification_answer() -> None:
|
||||||
from plyngent.agent import ToolConfirmRequest
|
from plyngent.agent import ToolConfirmRequest
|
||||||
from plyngent.cli.limits import _prompt_confirm_batch_sync
|
from plyngent.cli.limits import _prompt_confirm_batch_sync
|
||||||
|
|||||||
@@ -257,6 +257,34 @@ def test_a_confirmation_offers_three_buttons() -> None:
|
|||||||
assert request.reply_field is False
|
assert request.reply_field is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_waiting_prompt_is_answerable_before_it_is_announced() -> None:
|
||||||
|
"""The offer is decided when the wait starts, not when the notification shows.
|
||||||
|
|
||||||
|
A notification only appears after the grace period, so a prompt that learned
|
||||||
|
about its answers from ``notify_desktop`` would already be blocked on stdin.
|
||||||
|
"""
|
||||||
|
desktop = _FakeDesktop()
|
||||||
|
notifier, _ = _notifier(desktop=desktop, grace_seconds=60.0)
|
||||||
|
with notifier(PromptWait("confirm", "Allow?")) as handle:
|
||||||
|
assert handle.supports_actions is True
|
||||||
|
assert desktop.requests == [] # not announced yet
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_question_is_answerable_before_it_is_announced() -> None:
|
||||||
|
desktop = _FakeDesktop()
|
||||||
|
notifier, _ = _notifier(desktop=desktop, grace_seconds=60.0)
|
||||||
|
with notifier(PromptWait("ask", "Which branch?")) as handle:
|
||||||
|
assert handle.supports_actions is True
|
||||||
|
assert desktop.requests == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_answer_is_expected_without_the_capability() -> None:
|
||||||
|
desktop = _FakeDesktop(capabilities=frozenset({"buttons"}))
|
||||||
|
notifier, _ = _notifier(desktop=desktop)
|
||||||
|
with notifier(PromptWait("ask", "Which branch?")) as handle:
|
||||||
|
assert handle.supports_actions is False
|
||||||
|
|
||||||
|
|
||||||
def test_an_ask_offers_a_reply_field_instead_of_buttons() -> None:
|
def test_an_ask_offers_a_reply_field_instead_of_buttons() -> None:
|
||||||
desktop = _FakeDesktop()
|
desktop = _FakeDesktop()
|
||||||
notifier, _ = _notifier(desktop=desktop)
|
notifier, _ = _notifier(desktop=desktop)
|
||||||
|
|||||||
@@ -267,6 +267,25 @@ def test_non_interactive_confirm_uses_default() -> None:
|
|||||||
assert confirm("ok?", default=True) is True
|
assert confirm("ok?", default=True) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_confirm_takes_a_host_answer() -> None:
|
||||||
|
"""``allow`` / ``deny`` from a notification answer a yes/no like ``y`` / ``n``."""
|
||||||
|
with temporary_backend(ScriptedBackend([])):
|
||||||
|
assert confirm("Go?", source=_AnswerSource("allow")) is True
|
||||||
|
assert confirm("Go?", source=_AnswerSource("deny")) is False
|
||||||
|
assert confirm("Go?", source=_AnswerSource("n")) is False
|
||||||
|
# Nothing arrived: the prompt falls back to its default.
|
||||||
|
assert confirm("Go?", default=True, source=_AnswerSource(None)) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_confirm_takes_an_answer_the_hook_offers() -> None:
|
||||||
|
backend = ScriptedBackend([], confirms=[False])
|
||||||
|
set_prompt_wait_hook(lambda _wait: contextlib.nullcontext(_AnswerSource("allow")))
|
||||||
|
with temporary_backend(backend):
|
||||||
|
assert confirm("Go?") is True
|
||||||
|
# The answer came from the surface, so the backend was never asked.
|
||||||
|
assert backend.confirms == [False]
|
||||||
|
|
||||||
|
|
||||||
def test_prompt_output_hook_runs_before_chrome(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_prompt_output_hook_runs_before_chrome(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
"""A registered hook runs before echo/secho write; clearing it removes it.
|
"""A registered hook runs before echo/secho write; clearing it removes it.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user