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

543 lines
17 KiB
Python

from __future__ import annotations
from contextlib import asynccontextmanager
from types import SimpleNamespace
from typing import Any, cast
import pytest
from rich.console import Console
from rich.panel import Panel
from opensquilla.cli.repl import gateway_runtime, standalone_runtime
from opensquilla.cli.repl.session_state import ChatSessionState
from opensquilla.cli.repl.stream import TurnResult
from opensquilla.engine.commands import Surface
REMOVED_TEXT_BACKEND = "text" + "ual"
REMOVED_BACKEND_IDS = ["terminal", REMOVED_TEXT_BACKEND, f"live-{REMOVED_TEXT_BACKEND}"]
async def _fake_gateway_stream(*args: Any, **kwargs: Any) -> TurnResult:
return TurnResult(text="gateway")
async def _fake_gateway_slash(*args: Any, **kwargs: Any) -> bool:
return True
async def _fake_standalone_stream(*args: Any, **kwargs: Any) -> TurnResult:
return TurnResult(text="standalone")
def _fake_error_panel(message: str, *, title: str = "Error") -> Panel:
return Panel(message, title=title)
class _FakeStreamOutput:
async def __aenter__(self):
return lambda _payload: None
async def __aexit__(self, exc_type, exc, tb) -> None:
return None
class _FakeOutputHandle:
approval_surface = Surface.CLI_GATEWAY
async def write_through(self, payload: str) -> None:
return None
def stream_output(self) -> _FakeStreamOutput:
return _FakeStreamOutput()
def set_toolbar(self, key: str, value: object | None) -> None:
return None
def invalidate(self) -> None:
return None
class _FakeTuiSurface:
output_handle = _FakeOutputHandle()
async def next_line(self) -> str | None:
return None
def set_cancel_callback(self, cb) -> None:
return None
def set_shutdown_callback(self, cb) -> None:
return None
def emit_eof(self) -> None:
return None
async def write_through(self, payload: str) -> None:
return None
@property
def redraw_callback(self):
return lambda: None
class _RecordingConsole:
def __init__(self) -> None:
self.printed: list[object] = []
def print(self, value: object) -> None:
self.printed.append(value)
def test_gateway_runtime_notifier_maps_all_notice_kinds() -> None:
from opensquilla.cli.repl import runtime_bridge
output_console = _RecordingConsole()
notify = runtime_bridge._gateway_runtime_notifier(
output_console,
_fake_error_panel,
)
for notice in (
gateway_runtime.GatewayRuntimeNotice(
kind="created",
session_key="agent:main:new",
),
gateway_runtime.GatewayRuntimeNotice(
kind="resumed",
session_key="agent:main:old",
),
gateway_runtime.GatewayRuntimeNotice(kind="resume_model_ignored"),
gateway_runtime.GatewayRuntimeNotice(kind="model", model="openai/test"),
gateway_runtime.GatewayRuntimeNotice(kind="welcome"),
gateway_runtime.GatewayRuntimeNotice(kind="goodbye"),
gateway_runtime.GatewayRuntimeNotice(kind="unknown_command"),
gateway_runtime.GatewayRuntimeNotice(kind="error", message="boom"),
):
notify(notice)
assert output_console.printed[0] == (
"[dim]Connected to gateway. Session: agent:main:new[/dim]"
)
assert output_console.printed[1] == (
"[dim]Connected to gateway. Resuming session: agent:main:old[/dim]"
)
assert output_console.printed[2] == (
"[yellow]Note: --model is honored only at session creation; ignored "
"when resuming a session.[/yellow]"
)
assert output_console.printed[3] == "[dim]Model: openai/test[/dim]"
assert isinstance(output_console.printed[4], Panel)
assert output_console.printed[5] == "[yellow]Goodbye.[/yellow]"
assert output_console.printed[6] == "[red]Unknown command.[/red] [dim]Use /help.[/dim]"
assert isinstance(output_console.printed[7], Panel)
@pytest.mark.asyncio
async def test_gateway_runtime_bridge_assembles_gateway_dependencies(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
captured: dict[str, Any] = {}
output_console = _RecordingConsole()
async def fake_run_gateway_chat(**kwargs: Any) -> None:
captured.update(kwargs)
monkeypatch.setattr(gateway_runtime, "run_gateway_chat", fake_run_gateway_chat)
await runtime_bridge.run_gateway_chat(
model="openai/test",
session_id="agent:main:test",
stream_response=_fake_gateway_stream,
handle_slash_command=_fake_gateway_slash,
output_console=output_console,
error_panel_factory=_fake_error_panel,
)
deps = cast(gateway_runtime.GatewayRuntimeDependencies, captured["deps"])
assert captured["model"] == "openai/test"
assert captured["session_id"] == "agent:main:test"
assert deps.stream_response is _fake_gateway_stream
assert deps.handle_slash_command is _fake_gateway_slash
assert callable(deps.run_input_loop)
assert deps.get_tui_output is runtime_bridge.get_tui_output
deps.notify(gateway_runtime.GatewayRuntimeNotice(kind="error", message="boom"))
assert isinstance(output_console.printed[-1], Panel)
@pytest.mark.asyncio
async def test_gateway_runtime_bridge_resolves_default_repl_runner_at_call_time(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
captured: dict[str, Any] = {}
async def fake_run_gateway_chat(**kwargs: Any) -> None:
captured.update(kwargs)
runner_kwargs: dict[str, Any] = {}
async def replacement_run_concurrent_repl(**kwargs: Any) -> None:
runner_kwargs.update(kwargs)
return None
async def fake_dispatch(_value: str) -> bool:
return True
async def fake_abort() -> None:
return None
monkeypatch.setattr(gateway_runtime, "run_gateway_chat", fake_run_gateway_chat)
monkeypatch.setattr(runtime_bridge, "run_concurrent_repl", replacement_run_concurrent_repl)
await runtime_bridge.run_gateway_chat(
model=None,
session_id=None,
stream_response=_fake_gateway_stream,
handle_slash_command=_fake_gateway_slash,
)
deps = cast(gateway_runtime.GatewayRuntimeDependencies, captured["deps"])
scope = {
"session_key": "agent:main:test",
"state": ChatSessionState(session_key="agent:main:test"),
"model": None,
}
await deps.run_input_loop(
scope=scope,
dispatch=fake_dispatch,
abort_active_turn=fake_abort,
)
assert runner_kwargs["surface"] is Surface.CLI_GATEWAY
assert runner_kwargs["scope"] is scope
assert runner_kwargs["dispatch"] is fake_dispatch
assert runner_kwargs["abort_active_turn"] is fake_abort
@pytest.mark.asyncio
async def test_gateway_runtime_bridge_owns_default_turn_callbacks(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
captured: dict[str, Any] = {}
async def fake_run_gateway_chat(**kwargs: Any) -> None:
captured.update(kwargs)
monkeypatch.setattr(gateway_runtime, "run_gateway_chat", fake_run_gateway_chat)
await runtime_bridge.run_gateway_chat(model=None, session_id=None)
deps = cast(gateway_runtime.GatewayRuntimeDependencies, captured["deps"])
assert deps.stream_response is runtime_bridge.stream_response_gateway
assert deps.handle_slash_command is runtime_bridge.handle_gateway_slash_command
@pytest.mark.asyncio
async def test_run_concurrent_repl_defaults_to_native_bridge(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
monkeypatch.delenv("OPENSQUILLA_TUI_BACKEND", raising=False)
calls: list[dict[str, Any]] = []
async def fake_native_repl(**kwargs: Any) -> None:
calls.append(kwargs)
async def fake_dispatch(_value: str) -> bool:
return True
scope: dict[str, Any] = {}
monkeypatch.setattr(runtime_bridge._native_bridge, "run_concurrent_repl", fake_native_repl)
await runtime_bridge.run_concurrent_repl(
surface=Surface.CLI_GATEWAY,
scope=scope,
dispatch=fake_dispatch,
)
assert calls == [
{
"surface": Surface.CLI_GATEWAY,
"scope": scope,
"dispatch": fake_dispatch,
"queue_max_size": runtime_bridge.PENDING_QUEUE_MAX_SIZE,
"abort_active_turn": None,
}
]
@pytest.mark.parametrize("backend_id", REMOVED_BACKEND_IDS)
def test_validate_tui_backend_selection_rejects_removed_backends(
monkeypatch: pytest.MonkeyPatch,
backend_id: str,
) -> None:
from opensquilla.cli.repl import runtime_bridge
monkeypatch.setenv("OPENSQUILLA_TUI_BACKEND", backend_id)
with pytest.raises(ValueError) as exc_info:
runtime_bridge.validate_tui_backend_selection()
assert "Unsupported TUI backend" in str(exc_info.value)
assert backend_id in str(exc_info.value)
@pytest.mark.asyncio
async def test_run_concurrent_repl_uses_opentui_bridge_when_selected(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
monkeypatch.setenv("OPENSQUILLA_TUI_BACKEND", "opentui")
calls: list[dict[str, Any]] = []
async def fake_opentui_repl(**kwargs: Any) -> None:
calls.append(kwargs)
async def fake_dispatch(_value: str) -> bool:
return True
scope: dict[str, Any] = {}
monkeypatch.setattr(
runtime_bridge,
"validate_tui_backend_selection",
lambda env=None: "opentui",
)
monkeypatch.setattr(runtime_bridge._opentui_bridge, "run_concurrent_repl", fake_opentui_repl)
await runtime_bridge.run_concurrent_repl(
surface=Surface.CLI_GATEWAY,
scope=scope,
dispatch=fake_dispatch,
queue_max_size=5,
)
assert calls == [
{
"surface": Surface.CLI_GATEWAY,
"scope": scope,
"dispatch": fake_dispatch,
"queue_max_size": 5,
"abort_active_turn": None,
}
]
def test_turn_stream_dependencies_use_native_renderer_by_default(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
from opensquilla.cli.tui.native.renderer import NativeStreamRenderer
monkeypatch.delenv("OPENSQUILLA_TUI_BACKEND", raising=False)
deps = runtime_bridge._turn_stream_dependencies()
assert deps.renderer_factory is NativeStreamRenderer
def test_turn_stream_dependencies_use_opentui_renderer_when_selected(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
from opensquilla.cli.tui.opentui.renderer import OpenTuiStreamRenderer
monkeypatch.setattr(
runtime_bridge,
"validate_tui_backend_selection",
lambda env=None: "opentui",
)
deps = runtime_bridge._turn_stream_dependencies()
assert deps.renderer_factory is OpenTuiStreamRenderer
@pytest.mark.asyncio
async def test_opentui_chat_runtime_exposes_launch_scoped_plugin_manager(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.tui.backend.plugins import TuiPluginManager
from opensquilla.cli.tui.opentui import runtime as opentui_runtime
from opensquilla.cli.tui.plugins.router_hud import RouterHudPlugin
scope: dict[str, object] = {}
captured: dict[str, object] = {}
fake_surface = _FakeTuiSurface()
@asynccontextmanager
async def fake_open_opentui_surface(**_kwargs: object):
yield fake_surface
async def fake_run_tui_runtime(**kwargs: object):
hooks = kwargs["hooks"]
assert not opentui_runtime.get_tui_output(scope)
hooks.expose_surface(fake_surface)
output = opentui_runtime.get_tui_output(scope)
captured["output"] = output
captured["manager"] = getattr(output, "plugin_manager", None)
hooks.clear_exposed_surface()
return SimpleNamespace()
monkeypatch.setattr(
opentui_runtime,
"open_opentui_surface",
fake_open_opentui_surface,
)
monkeypatch.setattr(opentui_runtime, "run_tui_runtime", fake_run_tui_runtime)
async def fake_dispatch(_value: str) -> bool:
return True
await opentui_runtime.run_opentui_chat_runtime(
surface=Surface.CLI_GATEWAY,
scope=scope,
dispatch=fake_dispatch,
queue_max_size=8,
)
assert opentui_runtime.get_tui_output(scope) is None
manager = captured["manager"]
assert isinstance(manager, TuiPluginManager)
assert any(isinstance(plugin, RouterHudPlugin) for plugin in manager.plugins)
@pytest.mark.asyncio
async def test_gateway_runtime_bridge_threads_stream_override_to_default_slash(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
captured: dict[str, Any] = {}
slash_captured: dict[str, Any] = {}
output_console = Console(file=None, force_terminal=False)
async def fake_run_gateway_chat(**kwargs: Any) -> None:
captured.update(kwargs)
async def fake_handle_gateway_slash_command(*args: Any, **kwargs: Any) -> bool:
slash_captured.update({"args": args, **kwargs})
return True
monkeypatch.setattr(gateway_runtime, "run_gateway_chat", fake_run_gateway_chat)
monkeypatch.setattr(
runtime_bridge._slash_bridge,
"handle_gateway_slash_command",
fake_handle_gateway_slash_command,
)
await runtime_bridge.run_gateway_chat(
model=None,
session_id=None,
stream_response=_fake_gateway_stream,
output_console=output_console,
error_panel_factory=_fake_error_panel,
)
deps = cast(gateway_runtime.GatewayRuntimeDependencies, captured["deps"])
handled = await deps.handle_slash_command(
"/path report.md summarize",
ChatSessionState(session_key="agent:main:test"),
cast(gateway_runtime.GatewayClientLike, object()),
{"mode": None},
tui_output=None,
)
assert handled is True
assert slash_captured["stream_response"] is _fake_gateway_stream
assert slash_captured["output_console"] is output_console
assert slash_captured["error_panel_factory"] is _fake_error_panel
@pytest.mark.asyncio
async def test_standalone_runtime_bridge_assembles_standalone_dependencies(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
captured: dict[str, Any] = {}
output_console = Console(file=None, force_terminal=False)
async def fake_run_standalone_chat(**kwargs: Any) -> None:
captured.update(kwargs)
monkeypatch.setattr(standalone_runtime, "run_standalone_chat", fake_run_standalone_chat)
await runtime_bridge.run_standalone_chat(
model="openai/test",
session_id="agent:main:test",
workspace="repo",
workspace_strict=True,
timeout=7.25,
stream_response=_fake_standalone_stream,
image_command_handler=_fake_standalone_stream,
output_console=output_console,
error_panel_factory=_fake_error_panel,
)
deps = cast(standalone_runtime.StandaloneRuntimeDependencies, captured["deps"])
assert captured["model"] == "openai/test"
assert captured["session_id"] == "agent:main:test"
assert captured["workspace"] == "repo"
assert captured["workspace_strict"] is True
assert captured["timeout"] == 7.25
assert deps.stream_response is _fake_standalone_stream
assert deps.image_command_handler is _fake_standalone_stream
assert deps.run_concurrent_repl is runtime_bridge.run_concurrent_repl
assert deps.slash_services_factory is runtime_bridge.standalone_slash_services_from_runtime
assert deps.get_tui_output is runtime_bridge.get_tui_output
assert deps.output_console is output_console
@pytest.mark.asyncio
async def test_standalone_runtime_bridge_resolves_default_repl_runner_at_call_time(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
captured: dict[str, Any] = {}
async def fake_run_standalone_chat(**kwargs: Any) -> None:
captured.update(kwargs)
async def replacement_run_concurrent_repl(**kwargs: Any) -> None:
return None
monkeypatch.setattr(standalone_runtime, "run_standalone_chat", fake_run_standalone_chat)
monkeypatch.setattr(runtime_bridge, "run_concurrent_repl", replacement_run_concurrent_repl)
await runtime_bridge.run_standalone_chat(
model=None,
session_id=None,
stream_response=_fake_standalone_stream,
image_command_handler=_fake_standalone_stream,
)
deps = cast(standalone_runtime.StandaloneRuntimeDependencies, captured["deps"])
assert deps.run_concurrent_repl is replacement_run_concurrent_repl
@pytest.mark.asyncio
async def test_standalone_runtime_bridge_owns_default_turn_callbacks(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import runtime_bridge
captured: dict[str, Any] = {}
async def fake_run_standalone_chat(**kwargs: Any) -> None:
captured.update(kwargs)
monkeypatch.setattr(standalone_runtime, "run_standalone_chat", fake_run_standalone_chat)
await runtime_bridge.run_standalone_chat(model=None, session_id=None)
deps = cast(standalone_runtime.StandaloneRuntimeDependencies, captured["deps"])
assert deps.stream_response is runtime_bridge.stream_response_turnrunner
assert deps.image_command_handler is runtime_bridge.handle_image_command_turnrunner