191 lines
6.4 KiB
Python
191 lines
6.4 KiB
Python
"""Unit tests for the goose-native approval mirror's pane parser."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
import omnigent.goose_native_permissions as gp
|
|
from omnigent.goose_native_permissions import (
|
|
goose_permission_elicitation_id,
|
|
parse_goose_approval_prompt,
|
|
)
|
|
|
|
# cliclack radio with "Always Allow" → Deny is the 3rd item (2 downs from Allow).
|
|
_THREE_ITEM = (
|
|
"│ developer__shell\n"
|
|
"│ command: rm -rf /tmp/x\n"
|
|
"◆ Goose would like to call the above tool, do you allow?\n"
|
|
"│ ● Allow Allow the tool call once\n"
|
|
"│ ○ Always Allow Always allow the tool call\n"
|
|
"│ ○ Deny Deny the tool call\n"
|
|
"│ ○ Cancel Cancel the AI response and tool call\n"
|
|
)
|
|
|
|
# Security-prompt variant: no "Always Allow" → Deny is the 2nd item (1 down).
|
|
_TWO_ITEM = (
|
|
"⚠ this command writes files\n"
|
|
"◆ Do you allow this tool call?\n"
|
|
"│ ● Allow Allow the tool call once\n"
|
|
"│ ○ Deny Deny the tool call\n"
|
|
"│ ○ Cancel Cancel the AI response and tool call\n"
|
|
)
|
|
|
|
|
|
def test_parses_three_item_prompt_and_deny_index() -> None:
|
|
prompt = parse_goose_approval_prompt(_THREE_ITEM)
|
|
assert prompt is not None
|
|
# Allow(0) → Always Allow(1) → Deny(2): two Down presses.
|
|
assert prompt.deny_down_count == 2
|
|
# Subject is scraped from the tool-request lines above the question.
|
|
assert "developer__shell" in prompt.subject
|
|
assert prompt.block_hash
|
|
|
|
|
|
def test_parses_two_item_prompt_and_deny_index() -> None:
|
|
prompt = parse_goose_approval_prompt(_TWO_ITEM)
|
|
assert prompt is not None
|
|
# Allow(0) → Deny(1): one Down press.
|
|
assert prompt.deny_down_count == 1
|
|
|
|
|
|
def test_requires_question_and_both_items() -> None:
|
|
# Question but no Deny item → not a confirmation block.
|
|
assert parse_goose_approval_prompt("◆ do you allow?\n│ ● Allow\n") is None
|
|
# Items but no question → not live.
|
|
assert parse_goose_approval_prompt("│ ● Allow\n│ ○ Deny\n") is None
|
|
assert parse_goose_approval_prompt("") is None
|
|
|
|
|
|
def test_block_hash_differs_per_tool_and_id_is_deterministic() -> None:
|
|
a = parse_goose_approval_prompt(_THREE_ITEM)
|
|
other = _THREE_ITEM.replace("rm -rf /tmp/x", "cat /etc/passwd")
|
|
b = parse_goose_approval_prompt(other)
|
|
assert a is not None and b is not None
|
|
assert a.block_hash != b.block_hash
|
|
eid = goose_permission_elicitation_id("conv_9", a.block_hash)
|
|
assert eid == goose_permission_elicitation_id("conv_9", a.block_hash)
|
|
assert eid.startswith("elicit_goose_conv_9_")
|
|
|
|
|
|
# --- mirror plumbing (web verdict → cliclack keystrokes; TUI answer → release) -
|
|
|
|
|
|
class _Resp:
|
|
def __init__(self, status: int = 200, content: bytes = b'{"action":"accept"}', payload=None):
|
|
self.status_code = status
|
|
self.content = content
|
|
self._payload = payload if payload is not None else {"action": "accept"}
|
|
self.text = content.decode() if isinstance(content, bytes) else str(content)
|
|
|
|
def json(self):
|
|
return self._payload
|
|
|
|
|
|
class _FakeClient:
|
|
def __init__(self, resp: _Resp) -> None:
|
|
self._resp = resp
|
|
self.posts: list[tuple[str, dict]] = []
|
|
|
|
async def post(self, url, json=None, **_kwargs):
|
|
self.posts.append((url, json or {}))
|
|
return self._resp
|
|
|
|
|
|
def _gprompt(deny_downs: int = 2) -> gp.GooseApprovalPrompt:
|
|
return gp.GooseApprovalPrompt(
|
|
subject="developer__shell rm -rf x",
|
|
message="m",
|
|
preview="developer__shell",
|
|
deny_down_count=deny_downs,
|
|
block_hash="h",
|
|
)
|
|
|
|
|
|
async def test_run_one_approval_accept_presses_enter(tmp_path, monkeypatch) -> None:
|
|
sent: list[tuple] = []
|
|
monkeypatch.setattr(gp, "send_goose_pane_keys", lambda _bd, *keys: sent.append(keys))
|
|
await gp._run_one_approval(
|
|
_FakeClient(_Resp(payload={"action": "accept"})),
|
|
session_id="c",
|
|
bridge_dir=tmp_path,
|
|
prompt=_gprompt(),
|
|
elicitation_id="e1",
|
|
)
|
|
assert sent == [("Enter",)] # Allow is the default-highlighted item
|
|
|
|
|
|
async def test_run_one_approval_decline_walks_to_deny(tmp_path, monkeypatch) -> None:
|
|
sent: list[tuple] = []
|
|
monkeypatch.setattr(gp, "send_goose_pane_keys", lambda _bd, *keys: sent.append(keys))
|
|
await gp._run_one_approval(
|
|
_FakeClient(_Resp(payload={"action": "decline"})),
|
|
session_id="c",
|
|
bridge_dir=tmp_path,
|
|
prompt=_gprompt(deny_downs=2),
|
|
elicitation_id="e1",
|
|
)
|
|
assert sent == [("Down", "Down", "Enter")] # Allow → Always Allow → Deny
|
|
|
|
|
|
async def test_run_one_approval_empty_and_error_send_nothing(tmp_path, monkeypatch) -> None:
|
|
sent: list[tuple] = []
|
|
monkeypatch.setattr(gp, "send_goose_pane_keys", lambda _bd, *keys: sent.append(keys))
|
|
await gp._run_one_approval(
|
|
_FakeClient(_Resp(content=b"")),
|
|
session_id="c",
|
|
bridge_dir=tmp_path,
|
|
prompt=_gprompt(),
|
|
elicitation_id="e1",
|
|
)
|
|
await gp._run_one_approval(
|
|
_FakeClient(_Resp(status=503, content=b"down")),
|
|
session_id="c",
|
|
bridge_dir=tmp_path,
|
|
prompt=_gprompt(),
|
|
elicitation_id="e1",
|
|
)
|
|
assert sent == []
|
|
|
|
|
|
async def test_post_external_elicitation_resolved_targets_events(tmp_path) -> None:
|
|
client = _FakeClient(_Resp(status=200, content=b""))
|
|
await gp._post_external_elicitation_resolved(client, "conv_g", "e3")
|
|
url, body = client.posts[0]
|
|
assert url == "/v1/sessions/conv_g/events"
|
|
assert body["type"] == "external_elicitation_resolved"
|
|
|
|
|
|
async def test_supervise_raises_one_card_per_episode(tmp_path, monkeypatch) -> None:
|
|
panes = [_THREE_ITEM, _THREE_ITEM, None]
|
|
seq = {"i": 0}
|
|
|
|
def _cap(_bd):
|
|
i = seq["i"]
|
|
seq["i"] += 1
|
|
return panes[i] if i < len(panes) else None
|
|
|
|
monkeypatch.setattr(gp, "capture_goose_pane", _cap)
|
|
created: list[str] = []
|
|
|
|
async def _fake_run_one(_client, *, session_id, bridge_dir, prompt, elicitation_id):
|
|
created.append(elicitation_id)
|
|
|
|
monkeypatch.setattr(gp, "_run_one_approval", _fake_run_one)
|
|
|
|
sleeps = {"n": 0}
|
|
|
|
async def _sleep(_s):
|
|
sleeps["n"] += 1
|
|
if sleeps["n"] >= 3:
|
|
raise asyncio.CancelledError
|
|
|
|
monkeypatch.setattr(gp.asyncio, "sleep", _sleep)
|
|
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await gp.supervise_goose_approval_mirror(
|
|
base_url="http://x", headers={}, session_id="c", bridge_dir=tmp_path
|
|
)
|
|
assert len(created) == 1
|