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

401 lines
12 KiB
Python

from __future__ import annotations
import inspect
from types import SimpleNamespace
from typing import Any, cast
import pytest
from opensquilla.cli import chat_cmd
from opensquilla.cli.repl.stream import TurnResult
from opensquilla.cli.tui.contracts import TuiOutputHandle
from opensquilla.engine.commands import Surface
class _RecordingOutputHandle:
def __init__(self) -> None:
self._approval_surface = object()
@property
def approval_surface(self) -> object:
return self._approval_surface
async def write_through(self, payload: str) -> None:
return None
def stream_output(self):
raise AssertionError("stream_output is only consumed by renderers")
class _FalseyCallable:
def __call__(self, *args: Any, **kwargs: Any) -> Any:
return None
def __bool__(self) -> bool:
return False
class _FalseyConsole:
def __bool__(self) -> bool:
return False
class _ApprovalRenderer:
buffer = ""
def __init__(self, **_kwargs: Any) -> None:
return None
def __enter__(self) -> _ApprovalRenderer:
return self
def __exit__(self, *_args: Any) -> bool:
return False
async def aappend_text(self, _delta: str) -> None:
return None
def tool_start(self, *_args: Any, **_kwargs: Any) -> None:
return None
def tool_finished(self, *_args: Any, **_kwargs: Any) -> None:
return None
def pulse(self) -> None:
return None
def status(self, *_args: Any, **_kwargs: Any) -> None:
return None
def error(self, *_args: Any, **_kwargs: Any) -> None:
return None
def finalize(self, *_args: Any, **_kwargs: Any) -> None:
return None
class _ApprovalClient:
async def send_message(self, *_args: Any, **_kwargs: Any):
yield {
"event": "session.event.tool_result",
"result": {
"status": "approval_required",
"approval_id": "approval-1",
"command": "touch file",
},
"tool_use_id": "tool-1",
}
yield {"event": "session.event.done", "reason": "stop"}
async def resolve_approval(self, *_args: Any, **_kwargs: Any) -> None:
return None
async def abort_session(self, *_args: Any, **_kwargs: Any) -> None:
return None
class _SessionManager:
def __init__(self) -> None:
self.node = SimpleNamespace(
session_key="agent:main:webchat:abc",
agent_id="main",
origin=None,
)
async def get_session(self, session_key: str) -> object | None:
return self.node if session_key == self.node.session_key else None
async def update(self, session_key: str, **fields: Any) -> object:
for key, value in fields.items():
setattr(self.node, key, value)
return self.node
def _sandbox_config() -> SimpleNamespace:
return SimpleNamespace(
sandbox=SimpleNamespace(run_mode="standard", sandbox=True, security_grading=True),
permissions=SimpleNamespace(default_mode="off"),
)
def test_default_turn_stream_dependencies_preserves_explicit_falsey_overrides() -> None:
from opensquilla.cli.repl import turn_stream
renderer_factory = _FalseyCallable()
output_console = _FalseyConsole()
deps = turn_stream.default_turn_stream_dependencies(
renderer_factory=renderer_factory,
output_console=output_console,
)
assert deps.renderer_factory is renderer_factory
assert deps.output_console is output_console
def test_turn_stream_dependencies_preserve_frontend_owned_approval_surfaces() -> None:
from opensquilla.cli.repl import turn_stream
gateway_surface = object()
standalone_surface = object()
deps = turn_stream.default_turn_stream_dependencies(
gateway_approval_surface=gateway_surface,
standalone_approval_surface=standalone_surface,
)
assert deps.gateway_approval_surface is gateway_surface
assert deps.standalone_approval_surface is standalone_surface
def test_turn_stream_approval_surface_uses_structural_output_handle_value() -> None:
from opensquilla.cli.repl import turn_stream
output = _RecordingOutputHandle()
default_surface = object()
assert (
turn_stream.approval_surface_for_tui_output(output, default_surface)
is output.approval_surface
)
def test_tui_turn_bridge_supplies_cli_approval_surface_defaults() -> None:
from opensquilla.cli.repl import turn_bridge
deps = turn_bridge.default_turn_stream_dependencies()
assert deps.gateway_approval_surface is Surface.CLI_GATEWAY
assert deps.standalone_approval_surface is Surface.CLI_STANDALONE
def test_tui_turn_bridge_rejects_non_surface_output_approval_value() -> None:
from opensquilla.cli.repl import turn_bridge
assert (
turn_bridge.approval_surface_for_tui_output(
_RecordingOutputHandle(),
Surface.CLI_STANDALONE,
)
is Surface.CLI_STANDALONE
)
@pytest.mark.asyncio
async def test_tui_gateway_stream_coerces_invalid_output_approval_surface(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import turn_bridge
from opensquilla.cli.tui import turn_stream_defaults
captured: dict[str, Any] = {}
async def fake_approval_handler(*_args: Any, **kwargs: Any) -> None:
captured.update(kwargs)
# The default approval handler is built lazily per dependency set; patch
# its factory so the coerced surface kwarg can be observed.
monkeypatch.setattr(
turn_stream_defaults,
"tui_approval_handler",
lambda: fake_approval_handler,
)
deps = turn_bridge.default_turn_stream_dependencies(
renderer_factory=_ApprovalRenderer,
)
await turn_bridge.stream_response_gateway(
_ApprovalClient(),
"agent:main:test",
"hello",
tui_output=_RecordingOutputHandle(),
deps=deps,
)
assert captured["surface"] is Surface.CLI_GATEWAY
@pytest.mark.asyncio
async def test_local_approval_resolver_threads_sandbox_choice(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import turn_stream
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.sandbox.escalation import build_path_approval_params
from opensquilla.sandbox.path_validation import MountDecision
reset_approval_queue()
manager = _SessionManager()
workspace = tmp_path / "workspace"
outside = tmp_path / "outside"
workspace.mkdir()
outside.mkdir()
params = build_path_approval_params(
MountDecision(
status="request",
normalized_path=str(outside.resolve(strict=False)),
access="ro",
reason="outside_sandbox_mounts",
),
session_key=manager.node.session_key,
workspace=str(workspace),
)
assert params is not None
approval_id = get_approval_queue().request(namespace="exec", params=params)
captured: dict[str, Any] = {}
async def fake_apply_sandbox_approval_choice(
params: dict[str, Any] | None,
*,
choice: str | None,
approved: bool,
session_manager: object,
config: object,
) -> None:
captured.update(
{
"params": params,
"choice": choice,
"approved": approved,
"session_manager": session_manager,
"config": config,
}
)
monkeypatch.setattr(
"opensquilla.gateway.rpc_approvals.apply_sandbox_approval_choice",
fake_apply_sandbox_approval_choice,
)
resolver = turn_stream.local_approval_resolver(
session_manager=manager,
config=_sandbox_config(),
)
await resolver(approval_id, True, choice="allow_same_type")
pending = get_approval_queue().get(approval_id)
assert pending.resolved is True
assert pending.approved is True
assert pending.claim_token is None
assert captured["choice"] == "allow_same_type"
assert captured["approved"] is True
assert captured["params"] == params
assert captured["session_manager"] is manager
reset_approval_queue()
@pytest.mark.asyncio
async def test_turn_stream_adapter_threads_gateway_tui_output_without_chat_cmd(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import turn_stream
captured: dict[str, Any] = {}
async def fake_stream_response_gateway(
client: object,
session_key: str,
message: str,
elevated_state: dict[str, str | None] | None = None,
attachments: list[dict[str, Any]] | None = None,
*,
tui_output: TuiOutputHandle | None = None,
) -> TurnResult:
captured.update(
{
"client": client,
"session_key": session_key,
"message": message,
"elevated_state": elevated_state,
"attachments": attachments,
"tui_output": tui_output,
}
)
return TurnResult(text="ok")
monkeypatch.setattr(turn_stream, "stream_response_gateway", fake_stream_response_gateway)
output = _RecordingOutputHandle()
result = await turn_stream.dispatch_gateway_stream(
cast(turn_stream.GatewayStreamingClient, object()),
"agent:main:test",
"hello",
{"mode": "bypass"},
attachments=[{"id": "file-1"}],
tui_output=output,
)
assert result.text == "ok"
assert captured == {
"client": captured["client"],
"session_key": "agent:main:test",
"message": "hello",
"elevated_state": {"mode": "bypass"},
"attachments": [{"id": "file-1"}],
"tui_output": output,
}
def test_turn_stream_adapter_has_no_raw_prompt_application_dependency() -> None:
from opensquilla.cli.repl import turn_stream
source = inspect.getsource(turn_stream)
blocked_module = "prompt" + "_toolkit"
assert blocked_module not in source
assert "ChatApplication" not in source
@pytest.mark.asyncio
async def test_chat_cmd_stream_wrapper_uses_bridge_owned_renderer_dependency(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import turn_stream
from opensquilla.cli.tui.adapters import runtime_bridge
from opensquilla.cli.tui.opentui import renderer as opentui_renderer
captured: dict[str, Any] = {}
class _TemporaryOpenTuiRenderer:
pass
async def fake_stream_response_gateway(
client: object,
session_key: str,
message: str,
elevated_state: dict[str, str | None] | None = None,
attachments: list[dict[str, Any]] | None = None,
*,
tui_output: TuiOutputHandle | None = None,
deps: turn_stream.TurnStreamDependencies | None = None,
) -> TurnResult:
captured["renderer_dependency"] = None if deps is None else deps.renderer_factory
return TurnResult(text="ok")
# The bridge resolves the renderer from the selected backend, sourcing the
# class from the opentui renderer module — pin both halves of that wiring.
monkeypatch.setattr(
runtime_bridge,
"validate_tui_backend_selection",
lambda env=None: "opentui",
)
monkeypatch.setattr(
opentui_renderer,
"OpenTuiStreamRenderer",
_TemporaryOpenTuiRenderer,
)
monkeypatch.setattr(turn_stream, "stream_response_gateway", fake_stream_response_gateway)
result = await chat_cmd._stream_response_gateway(
cast(Any, object()),
"agent:main:test",
"hello",
{"mode": "bypass"},
)
assert result.text == "ok"
assert captured == {"renderer_dependency": _TemporaryOpenTuiRenderer}