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

367 lines
12 KiB
Python

from __future__ import annotations
import asyncio
import importlib
import inspect
import sys
from typing import Any, cast
import pytest
from opensquilla.cli.repl.session_state import ChatSessionState
from opensquilla.cli.repl.stream import TurnResult, UsageSummary
from opensquilla.cli.tui.contracts import TuiOutputHandle
def test_gateway_runtime_has_no_raw_prompt_application_dependency(monkeypatch) -> None:
monkeypatch.delitem(
sys.modules,
"opensquilla.cli.repl.gateway_runtime",
raising=False,
)
original_import = __import__
blocked_module = "prompt" + "_toolkit"
def _guarded_import(name, globals=None, locals=None, fromlist=(), level=0): # noqa: ANN001
if name == blocked_module or name.startswith(f"{blocked_module}."):
raise AssertionError(f"gateway runtime imported {blocked_module} via {name}")
return original_import(name, globals, locals, fromlist, level)
monkeypatch.setattr("builtins.__import__", _guarded_import)
module = importlib.import_module("opensquilla.cli.repl.gateway_runtime")
source = inspect.getsource(module)
assert "ChatApplication" not in source
def test_gateway_session_context_mirrors_state_to_legacy_scope() -> None:
from opensquilla.cli.repl.gateway_runtime import GatewaySessionContext
state = ChatSessionState(session_key="agent:main:original", model="gateway/original")
context = GatewaySessionContext.create(state)
assert context.scope["session_key"] == "agent:main:original"
assert context.scope["state"] is state
assert context.scope["model"] == "gateway/original"
state.session_key = "agent:main:slash"
state.model = "gateway/slash-model"
context.sync_from_state()
assert context.session_key == "agent:main:slash"
assert context.model == "gateway/slash-model"
assert context.scope["session_key"] == "agent:main:slash"
assert context.scope["model"] == "gateway/slash-model"
@pytest.mark.asyncio
async def test_gateway_runtime_connects_to_configured_gateway_target(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import gateway_runtime
class _FakeGatewayClient:
connected_url: str | None = None
connected_token: str | None = None
closed = False
async def connect(self, url: str, *, token: str | None = None) -> None:
type(self).connected_url = url
type(self).connected_token = token
async def create_session(self, model: str | None = None) -> str:
return "agent:main:new"
async def resolve_session(self, key: str) -> dict[str, str]:
return {"model": "gateway/resolved"}
async def close(self) -> None:
type(self).closed = True
monkeypatch.setattr("opensquilla.cli.gateway_client.GatewayClient", _FakeGatewayClient)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_URL", "http://127.0.0.1:18790")
monkeypatch.setenv("OPENSQUILLA_GATEWAY_TOKEN", "branch-token")
async def fake_run_concurrent_repl(
*,
scope: gateway_runtime.GatewayRuntimeScope,
dispatch,
abort_active_turn=None,
) -> None:
return None
deps = gateway_runtime.GatewayRuntimeDependencies(
stream_response=cast(Any, None),
handle_slash_command=cast(Any, None),
run_input_loop=fake_run_concurrent_repl,
get_tui_output=lambda _scope: None,
is_exit_command=lambda _value: False,
notify=lambda _notice: None,
)
await gateway_runtime.run_gateway_chat(
model=None,
session_id=None,
deps=deps,
)
assert _FakeGatewayClient.connected_url == "ws://127.0.0.1:18790/ws"
assert _FakeGatewayClient.connected_token == "branch-token"
assert _FakeGatewayClient.closed is True
@pytest.mark.asyncio
async def test_gateway_runtime_dispatches_messages_slash_commands_and_exit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import gateway_runtime
class _FakeGatewayClient:
instances: list[_FakeGatewayClient] = []
def __init__(self) -> None:
self.connected = False
self.closed = False
self.create_calls: list[str | None] = []
self.resolve_calls: list[str] = []
self.abort_calls: list[str] = []
_FakeGatewayClient.instances.append(self)
async def connect(self, url: str, *, token: str | None = None) -> None:
self.connected = True
async def create_session(self, model: str | None = None) -> str:
self.create_calls.append(model)
return "agent:main:new"
async def resolve_session(self, key: str) -> dict[str, str]:
self.resolve_calls.append(key)
return {"model": "gateway/resolved"}
async def abort_session(self, key: str) -> dict[str, object]:
self.abort_calls.append(key)
return {"aborted": True, "key": key}
async def close(self) -> None:
self.closed = True
monkeypatch.setattr("opensquilla.cli.gateway_client.GatewayClient", _FakeGatewayClient)
output = cast(TuiOutputHandle, object())
captured: dict[str, Any] = {}
async def fake_stream_response(
client: object,
session_key: str,
message: str,
elevated_state: dict[str, str | None] | None = None,
attachments: list[dict] | None = None,
*,
tui_output: object | None = None,
) -> TurnResult:
captured["stream"] = {
"client": client,
"session_key": session_key,
"message": message,
"elevated_state": elevated_state,
"attachments": attachments,
"tui_output": tui_output,
}
return TurnResult(
text="assistant reply",
usage=UsageSummary(input_tokens=2, output_tokens=3),
model_after="gateway/after",
)
async def fake_handle_slash_command(
cmd: str,
state: ChatSessionState,
client: object,
elevated_state: dict[str, str | None],
*,
tui_output: object | None = None,
) -> bool:
captured["slash"] = {
"cmd": cmd,
"client": client,
"elevated_state": elevated_state,
"tui_output": tui_output,
}
state.session_key = "agent:main:slash"
state.model = "gateway/slash-model"
return True
async def fake_run_concurrent_repl(
*,
scope: gateway_runtime.GatewayRuntimeScope,
dispatch,
abort_active_turn=None,
) -> None:
captured["initial_scope"] = dict(scope)
captured["abort_active_turn"] = abort_active_turn
assert await dispatch("hello") is True
active_state = cast(ChatSessionState, scope["state"])
captured["state_after_message"] = active_state
assert active_state.model == "gateway/after"
assert active_state.transcript.to_markdown()
assert active_state.usage.input_tokens == 2
assert active_state.usage.output_tokens == 3
assert await dispatch("/reset") is True
captured["scope_after_slash"] = dict(scope)
assert await dispatch("/exit") is False
def fake_get_tui_output(
_scope: gateway_runtime.GatewayRuntimeScope,
) -> TuiOutputHandle | None:
return output
deps = gateway_runtime.GatewayRuntimeDependencies(
stream_response=fake_stream_response,
handle_slash_command=fake_handle_slash_command,
run_input_loop=fake_run_concurrent_repl,
get_tui_output=fake_get_tui_output,
is_exit_command=lambda value: value.strip() == "/exit",
notify=lambda notice: captured.setdefault("notices", []).append(notice),
)
await gateway_runtime.run_gateway_chat(
model="anthropic/claude-sonnet-4",
session_id=None,
deps=deps,
)
client = _FakeGatewayClient.instances[-1]
assert client.connected is True
assert client.closed is True
assert client.create_calls == ["anthropic/claude-sonnet-4"]
assert client.resolve_calls == ["agent:main:new"]
assert captured["abort_active_turn"] is not None
await captured["abort_active_turn"]()
assert client.abort_calls == []
assert captured["initial_scope"]["session_key"] == "agent:main:new"
assert captured["initial_scope"]["model"] == "gateway/resolved"
assert captured["stream"]["client"] is client
assert captured["stream"]["session_key"] == "agent:main:new"
assert captured["stream"]["message"] == "hello"
assert captured["stream"]["elevated_state"] == {"mode": None}
assert captured["stream"]["tui_output"] is output
assert captured["slash"]["cmd"] == "/reset"
assert captured["slash"]["client"] is client
assert captured["slash"]["elevated_state"] == {"mode": None}
assert captured["slash"]["tui_output"] is output
assert captured["scope_after_slash"]["session_key"] == "agent:main:slash"
assert captured["scope_after_slash"]["model"] == "gateway/slash-model"
assert [notice.kind for notice in captured["notices"]] == [
"created",
"model",
"welcome",
"goodbye",
]
@pytest.mark.asyncio
async def test_gateway_abort_targets_active_turn_session_after_session_changes(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import gateway_runtime
class _FakeGatewayClient:
instances: list[_FakeGatewayClient] = []
def __init__(self) -> None:
self.abort_calls: list[str] = []
_FakeGatewayClient.instances.append(self)
async def connect(self, url: str, *, token: str | None = None) -> None:
return None
async def create_session(self, model: str | None = None) -> str:
return "agent:main:old"
async def resolve_session(self, key: str) -> dict[str, str]:
return {"model": "gateway/old"}
async def abort_session(self, key: str) -> dict[str, object]:
self.abort_calls.append(key)
return {"aborted": True, "key": key}
async def close(self) -> None:
return None
monkeypatch.setattr("opensquilla.cli.gateway_client.GatewayClient", _FakeGatewayClient)
stream_started = asyncio.Event()
release_stream = asyncio.Event()
async def fake_stream_response(
_client: object,
session_key: str,
_message: str,
_elevated_state: dict[str, str | None] | None = None,
_attachments: list[dict] | None = None,
*,
tui_output: object | None = None,
) -> TurnResult:
assert session_key == "agent:main:old"
stream_started.set()
await release_stream.wait()
return TurnResult(
text="assistant reply",
usage=UsageSummary(input_tokens=1, output_tokens=1),
model_after="gateway/old",
)
async def fake_handle_slash_command(
_cmd: str,
_state: ChatSessionState,
_client: object,
_elevated_state: dict[str, str | None],
*,
tui_output: object | None = None,
) -> bool:
return True
async def fake_run_concurrent_repl(
*,
scope: gateway_runtime.GatewayRuntimeScope,
dispatch,
abort_active_turn=None,
) -> None:
assert abort_active_turn is not None
turn = asyncio.create_task(dispatch("hello"))
await asyncio.wait_for(stream_started.wait(), timeout=2.0)
active_state = cast(ChatSessionState, scope["state"])
active_state.session_key = "agent:main:new"
active_state.model = "gateway/new"
scope["session_key"] = active_state.session_key
scope["model"] = active_state.model
await abort_active_turn()
release_stream.set()
assert await asyncio.wait_for(turn, timeout=2.0) is True
deps = gateway_runtime.GatewayRuntimeDependencies(
stream_response=fake_stream_response,
handle_slash_command=fake_handle_slash_command,
run_input_loop=fake_run_concurrent_repl,
get_tui_output=lambda _scope: None,
is_exit_command=lambda _value: False,
notify=lambda _notice: None,
)
await gateway_runtime.run_gateway_chat(
model=None,
session_id=None,
deps=deps,
)
client = _FakeGatewayClient.instances[-1]
assert client.abort_calls == ["agent:main:old"]