Files
2026-07-13 13:12:33 +08:00

144 lines
4.7 KiB
Python

from __future__ import annotations
import ast
from pathlib import Path
import pytest
from opensquilla.application.approval_queue import ApprovalQueue
from opensquilla.gateway import rpc_approvals
from opensquilla.gateway.rpc import RpcContext, get_dispatcher
@pytest.mark.asyncio
async def test_exec_approval_rpc_delegates_payload_to_application_boundary(
monkeypatch: pytest.MonkeyPatch,
) -> None:
queue = ApprovalQueue(db_path=":memory:")
monkeypatch.setattr(rpc_approvals, "get_approval_queue", lambda: queue)
try:
queue.set_settings("auto-deny")
result = await get_dispatcher().dispatch(
"r1",
"exec.approval.request",
{"toolName": "exec_command", "args": {}, "sessionKey": "agent:main:demo"},
RpcContext(conn_id="test"),
)
assert result.error is None, result.error
assert result.payload["mode"] == "auto-deny"
assert result.payload["approved"] is False
assert result.payload["resolved"] is True
assert result.payload["pending"] is False
finally:
queue.close()
@pytest.mark.asyncio
async def test_exec_approval_extend_rpc_pushes_deadline(
monkeypatch: pytest.MonkeyPatch,
) -> None:
queue = ApprovalQueue(db_path=":memory:", default_timeout=10.0)
monkeypatch.setattr(rpc_approvals, "get_approval_queue", lambda: queue)
try:
approval_id = queue.request(
"exec",
{"toolName": "exec_command", "command": "rm x", "sessionKey": "agent:main:demo"},
)
before = queue.get(approval_id).deadline
result = await get_dispatcher().dispatch(
"r1",
"exec.approval.extend",
{"id": approval_id, "seconds": 120},
RpcContext(conn_id="test"),
)
assert result.error is None, result.error
assert result.payload["pending"] is True
assert result.payload["deadline"] == before + 120
assert queue.get(approval_id).deadline == before + 120
finally:
queue.close()
@pytest.mark.asyncio
async def test_exec_approval_extend_rpc_rejects_non_positive_seconds(
monkeypatch: pytest.MonkeyPatch,
) -> None:
queue = ApprovalQueue(db_path=":memory:", default_timeout=10.0)
monkeypatch.setattr(rpc_approvals, "get_approval_queue", lambda: queue)
try:
approval_id = queue.request("exec", {"toolName": "exec_command", "command": "rm x"})
result = await get_dispatcher().dispatch(
"r1",
"exec.approval.extend",
{"id": approval_id, "seconds": 0},
RpcContext(conn_id="test"),
)
assert result.error is not None
finally:
queue.close()
def test_gateway_rpc_approvals_keeps_payload_logic_out_of_gateway_boundary() -> None:
source = Path(rpc_approvals.__file__).read_text(encoding="utf-8")
tree = ast.parse(source)
imports = [
node
for node in ast.walk(tree)
if isinstance(node, ast.ImportFrom) and node.module is not None
]
top_level_functions = {
node.name for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
}
helper_names = {
"approval_forget_rpc_payload",
"approval_snapshot_rpc_payload",
}
imported_helpers = {
alias.name
for node in imports
if node.module == "opensquilla.application.approval_rpc"
for alias in node.names
}
handlers = {
node.name: node
for node in tree.body
if isinstance(node, ast.AsyncFunctionDef)
and node.name in {"_handle_exec_approval_snapshot", "_handle_exec_approval_forget"}
}
handler_names = {
node.id
for handler in handlers.values()
for node in ast.walk(handler)
if isinstance(node, ast.Name)
}
direct_key_sets = {
tuple(key.value for key in node.keys if isinstance(key, ast.Constant))
for handler in handlers.values()
for node in ast.walk(handler)
if isinstance(node, ast.Dict)
}
private_attrs = {
node.attr
for handler in handlers.values()
for node in ast.walk(handler)
if isinstance(node, ast.Attribute)
}
assert "_settings_payload" not in top_level_functions
assert "_status_payload" not in top_level_functions
assert "_request_approval" not in top_level_functions
assert helper_names.issubset(imported_helpers)
assert helper_names.issubset(handler_names)
assert ("mode", "intent_cache_size", "intent_cache_entries") not in direct_key_sets
assert ("kind", "target", "scope") not in direct_key_sets
assert ("scope", "target") not in direct_key_sets
assert ("scope",) not in direct_key_sets
assert "_entries" not in private_attrs