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

833 lines
27 KiB
Python

from __future__ import annotations
import asyncio
from dataclasses import asdict
from typing import Any
import pytest
from opensquilla.cli.tui.opentui.messages import (
CompletionContext,
HostApprovalResponse,
HostCompletionRequest,
HostError,
HostInputCancel,
HostInputEof,
HostInputSubmit,
HostProtocolUnknown,
HostResize,
RouterPluginState,
ScrollbackWrite,
)
from opensquilla.cli.tui.opentui.surface import (
OpenTuiOutputHandle,
OpenTuiSurface,
open_opentui_surface,
)
from opensquilla.engine.commands import Surface
class FakeOpenTuiBridge:
def __init__(self) -> None:
self.messages: asyncio.Queue[object] = asyncio.Queue()
self.sent: list[tuple[str, dict[str, Any] | None]] = []
async def send(self, message_type: str, payload: object | None = None) -> None:
if payload is None:
self.sent.append((message_type, None))
return
self.sent.append(
(message_type, payload if isinstance(payload, dict) else asdict(payload))
)
async def next_message(self) -> object | None:
return await self.messages.get()
async def start(self) -> None:
return None
async def close(self) -> None:
return None
@pytest.mark.asyncio
async def test_opentui_surface_returns_submitted_lines_and_eof() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
bridge.messages.put_nowait(HostInputSubmit(text="中文 prompt"))
assert await surface.next_line() == "中文 prompt"
bridge.messages.put_nowait(HostInputEof())
assert await surface.next_line() is None
@pytest.mark.asyncio
async def test_opentui_surface_delegates_cancel_and_keeps_waiting() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
cancelled: list[str] = []
surface.set_cancel_callback(lambda: cancelled.append("cancel"))
bridge.messages.put_nowait(HostInputCancel())
bridge.messages.put_nowait(HostInputSubmit(text="after cancel"))
assert await surface.next_line() == "after cancel"
assert cancelled == ["cancel"]
@pytest.mark.asyncio
async def test_host_error_frames_are_non_fatal_diagnostics() -> None:
"""The host recovers from per-message failures and keeps running; one
error frame must not end the session."""
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
for _ in range(3):
bridge.messages.put_nowait(HostError(message="render hiccup"))
bridge.messages.put_nowait(HostInputSubmit(text="still alive"))
assert await surface.next_line() == "still alive"
@pytest.mark.asyncio
async def test_uninterrupted_host_error_flood_tears_the_session_down() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
for _ in range(8):
bridge.messages.put_nowait(HostError(message="dispatch is broken"))
with pytest.raises(RuntimeError, match="OpenTUI host error: dispatch is broken"):
await surface.next_line()
@pytest.mark.asyncio
async def test_host_error_streak_resets_on_any_other_frame() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
for _ in range(7):
bridge.messages.put_nowait(HostError(message="hiccup"))
bridge.messages.put_nowait(HostResize(width=100, height=30))
for _ in range(7):
bridge.messages.put_nowait(HostError(message="hiccup"))
bridge.messages.put_nowait(HostInputSubmit(text="survived"))
assert await surface.next_line() == "survived"
@pytest.mark.asyncio
async def test_protocol_unknown_frames_are_logged_and_skipped() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
bridge.messages.put_nowait(HostProtocolUnknown(message_type="future.type"))
bridge.messages.put_nowait(HostInputSubmit(text="next"))
assert await surface.next_line() == "next"
@pytest.mark.asyncio
async def test_resize_frames_update_last_known_size() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
assert surface.last_known_size is None
bridge.messages.put_nowait(HostResize(width=120, height=40))
bridge.messages.put_nowait(HostInputSubmit(text="go"))
assert await surface.next_line() == "go"
assert surface.last_known_size == (120, 40)
@pytest.mark.asyncio
async def test_approval_response_resolves_waiter_and_next_line_keeps_pumping() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
output = surface.output_handle
request_task = asyncio.create_task(
output.request_approval(
{"id": "appr-1", "tool": "shell", "summary": "touch demo.txt", "choices": []},
)
)
await asyncio.sleep(0)
assert bridge.sent[-1][0] == "approval.request"
assert bridge.sent[-1][1]["id"] == "appr-1"
bridge.messages.put_nowait(HostApprovalResponse(id="appr-1", approved=True, choice=None))
bridge.messages.put_nowait(HostInputSubmit(text="next input"))
# The decision frame resolves the waiter and is never surfaced as input.
assert await surface.next_line() == "next input"
response = await request_task
assert response == HostApprovalResponse(id="appr-1", approved=True, choice=None)
# A delivered decision already closed the overlay host-side; no dismiss.
assert "approval.dismiss" not in [message_type for message_type, _payload in bridge.sent]
@pytest.mark.asyncio
async def test_approval_waiter_survives_next_line_task_recreation() -> None:
"""The waiter registry lives on the surface's output handle, so a pending
approval resolves even when the runtime cancels and re-creates its input
task while the request is outstanding."""
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
output = surface.output_handle
request_task = asyncio.create_task(
output.request_approval({"id": "appr-2", "tool": "shell", "summary": "", "choices": []})
)
await asyncio.sleep(0)
first_reader = asyncio.create_task(surface.next_line())
await asyncio.sleep(0)
first_reader.cancel()
with pytest.raises(asyncio.CancelledError):
await first_reader
bridge.messages.put_nowait(
HostApprovalResponse(id="appr-2", approved=False, choice="deny")
)
bridge.messages.put_nowait(HostInputSubmit(text="after recreation"))
assert await surface.next_line() == "after recreation"
response = await request_task
assert response == HostApprovalResponse(id="appr-2", approved=False, choice="deny")
@pytest.mark.asyncio
async def test_request_approval_times_out_to_none() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
response = await output.request_approval(
{"id": "appr-3", "tool": "shell", "summary": "", "choices": []},
timeout=0.01,
)
assert response is None
@pytest.mark.asyncio
async def test_request_approval_timeout_dismisses_host_overlay() -> None:
"""A timed-out approval must close the host overlay, or the stale modal
swallows the user's next Enter/Esc/y/n keypress."""
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
response = await output.request_approval(
{"id": "appr-6", "tool": "shell", "summary": "", "choices": []},
timeout=0.01,
)
assert response is None
assert bridge.sent[-1] == ("approval.dismiss", {"id": "appr-6"})
@pytest.mark.asyncio
async def test_cancelled_approval_request_dismisses_host_overlay() -> None:
"""Turn cancellation (Ctrl+C) stops waiting on the overlay; the host must
be told to close it instead of leaving a dead modal mounted."""
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
request_task = asyncio.create_task(
output.request_approval({"id": "appr-7", "tool": "shell", "summary": "", "choices": []})
)
await asyncio.sleep(0)
assert bridge.sent[-1][0] == "approval.request"
request_task.cancel()
with pytest.raises(asyncio.CancelledError):
await request_task
assert bridge.sent[-1] == ("approval.dismiss", {"id": "appr-7"})
assert output._approval_waiters == {}
@pytest.mark.asyncio
async def test_request_approval_returns_none_when_bridge_send_fails() -> None:
bridge = _FailingBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
response = await output.request_approval(
{"id": "appr-4", "tool": "shell", "summary": "", "choices": []},
)
assert response is None
@pytest.mark.asyncio
async def test_request_approval_without_id_returns_none_without_sending() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
assert await output.request_approval({"tool": "shell"}) is None
assert bridge.sent == []
@pytest.mark.asyncio
async def test_host_eof_denies_pending_approval_waiters() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
output = surface.output_handle
request_task = asyncio.create_task(
output.request_approval({"id": "appr-5", "tool": "shell", "summary": "", "choices": []})
)
await asyncio.sleep(0)
bridge.messages.put_nowait(HostInputEof())
assert await surface.next_line() is None
assert await request_task is None
@pytest.mark.asyncio
async def test_unmatched_approval_response_is_skipped_and_loop_continues() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
bridge.messages.put_nowait(HostApprovalResponse(id="ghost", approved=True, choice=None))
bridge.messages.put_nowait(HostInputSubmit(text="still serving input"))
assert await surface.next_line() == "still serving input"
@pytest.mark.asyncio
async def test_opentui_output_handle_writes_to_scrollback() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
await output.write_through("tool output\nfinal answer")
assert bridge.sent == [
(
"scrollback.write",
asdict(ScrollbackWrite(text="tool output\nfinal answer")),
)
]
@pytest.mark.asyncio
async def test_open_opentui_surface_sends_completion_context_on_startup() -> None:
bridge = FakeOpenTuiBridge()
async with open_opentui_surface(
surface=Surface.CLI_GATEWAY,
ready_marker="",
print_ready_marker=False,
bridge=bridge,
completion_context=CompletionContext(catalog=(), files=()),
):
pass
assert bridge.sent == [
(
"composer.set",
{
"placeholder": "send a message",
"text": "",
"disabled": False,
},
),
(
"completion.context",
{
"catalog": (),
"files": (),
"filters_sensitive_paths": True,
},
),
]
@pytest.mark.asyncio
async def test_open_opentui_surface_marks_ready_after_completion_context() -> None:
bridge = FakeOpenTuiBridge()
async with open_opentui_surface(
surface=Surface.CLI_GATEWAY,
ready_marker="READY",
bridge=bridge,
completion_context=CompletionContext(catalog=(), files=()),
):
pass
assert [message_type for message_type, _payload in bridge.sent] == [
"composer.set",
"completion.context",
"scrollback.write",
]
assert bridge.sent[-1] == (
"scrollback.write",
asdict(ScrollbackWrite(text="READY\n")),
)
@pytest.mark.asyncio
async def test_open_opentui_surface_omits_ready_marker_by_default(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The readiness sentinel is harness scaffolding — a real session must not
render it unless the env var (or caller) explicitly opts in."""
monkeypatch.delenv("OPENSQUILLA_TUI_READY_MARKER", raising=False)
bridge = FakeOpenTuiBridge()
async with open_opentui_surface(
surface=Surface.CLI_GATEWAY,
bridge=bridge,
completion_context=CompletionContext(catalog=(), files=()),
):
pass
assert [message_type for message_type, _payload in bridge.sent] == [
"composer.set",
"completion.context",
]
@pytest.mark.asyncio
async def test_open_opentui_surface_env_var_opts_into_ready_marker(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("OPENSQUILLA_TUI_READY_MARKER", "HARNESS_READY")
bridge = FakeOpenTuiBridge()
async with open_opentui_surface(
surface=Surface.CLI_GATEWAY,
bridge=bridge,
completion_context=CompletionContext(catalog=(), files=()),
):
pass
assert bridge.sent[-1] == (
"scrollback.write",
asdict(ScrollbackWrite(text="HARNESS_READY\n")),
)
@pytest.mark.asyncio
async def test_opentui_surface_answers_file_completion_without_returning_input(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
from opensquilla.cli.tui.opentui import surface as surface_module
calls: list[tuple[object, str, int]] = []
def fake_enumerate_workspace_files(root, *, query: str, max_results: int):
calls.append((root, query, max_results))
return ["foo.py", "src/foo_bar.py"]
monkeypatch.setattr(
surface_module,
"enumerate_workspace_files",
fake_enumerate_workspace_files,
)
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(
bridge,
approval_surface=Surface.CLI_GATEWAY,
workspace_dir=tmp_path,
)
bridge.messages.put_nowait(
HostCompletionRequest(kind="file", query="foo", request_id=7)
)
bridge.messages.put_nowait(HostInputSubmit(text="hello"))
# Completion is served off the input loop so a queued submit is never
# delayed behind enumeration; drain the tracked task before asserting.
assert await surface.next_line() == "hello"
completion_task = surface._completion_task
assert completion_task is not None
await completion_task
assert calls == [(tmp_path, "foo", 50)]
assert bridge.sent == [
(
"completion.response",
{
"request_id": 7,
"kind": "file",
"items": [
{
"label": "foo.py",
"description": "foo.py",
"insert_text": "@foo.py ",
"category": "file",
},
{
"label": "src/foo_bar.py",
"description": "src/foo_bar.py",
"insert_text": "@src/foo_bar.py ",
"category": "file",
},
],
},
)
]
@pytest.mark.asyncio
async def test_opentui_surface_answers_file_completion_with_empty_items_without_workspace() -> None:
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(bridge, approval_surface=Surface.CLI_GATEWAY)
bridge.messages.put_nowait(HostCompletionRequest(kind="file", query="foo", request_id=8))
bridge.messages.put_nowait(HostInputSubmit(text="after completion"))
assert await surface.next_line() == "after completion"
completion_task = surface._completion_task
assert completion_task is not None
await completion_task
assert bridge.sent == [
(
"completion.response",
{
"request_id": 8,
"kind": "file",
"items": [],
},
)
]
@pytest.mark.asyncio
async def test_newer_completion_request_supersedes_older_one(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
from opensquilla.cli.tui.opentui import surface as surface_module
def fake_enumerate_workspace_files(root, *, query: str, max_results: int):
return [f"{query}.py"]
monkeypatch.setattr(
surface_module,
"enumerate_workspace_files",
fake_enumerate_workspace_files,
)
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(
bridge,
approval_surface=Surface.CLI_GATEWAY,
workspace_dir=tmp_path,
)
bridge.messages.put_nowait(HostCompletionRequest(kind="file", query="old", request_id=1))
bridge.messages.put_nowait(HostCompletionRequest(kind="file", query="new", request_id=2))
bridge.messages.put_nowait(HostInputSubmit(text="go"))
assert await surface.next_line() == "go"
completion_task = surface._completion_task
assert completion_task is not None
await completion_task
responses = [
payload for message_type, payload in bridge.sent
if message_type == "completion.response"
]
assert [response["request_id"] for response in responses] == [2]
@pytest.mark.asyncio
async def test_completion_failure_does_not_kill_the_input_loop(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
from opensquilla.cli.tui.opentui import surface as surface_module
def broken_enumerate(root, *, query: str, max_results: int):
raise OSError("workspace walk failed")
monkeypatch.setattr(surface_module, "enumerate_workspace_files", broken_enumerate)
bridge = FakeOpenTuiBridge()
surface = OpenTuiSurface(
bridge,
approval_surface=Surface.CLI_GATEWAY,
workspace_dir=tmp_path,
)
bridge.messages.put_nowait(HostCompletionRequest(kind="file", query="q", request_id=3))
bridge.messages.put_nowait(HostInputSubmit(text="still here"))
assert await surface.next_line() == "still here"
completion_task = surface._completion_task
assert completion_task is not None
await completion_task
assert bridge.sent == []
@pytest.mark.asyncio
async def test_open_opentui_surface_uses_explicit_workspace_for_completion_context(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
from opensquilla.cli.tui.opentui import surface as surface_module
bridge = FakeOpenTuiBridge()
captured: dict[str, Any] = {}
def fake_build_completion_context(
surface: Surface,
*,
workspace_dir,
) -> CompletionContext:
captured["surface"] = surface
captured["workspace_dir"] = workspace_dir
return CompletionContext(catalog=(), files=("src/main.py",))
monkeypatch.setenv("OPENSQUILLA_WORKSPACE_DIR", "/tmp/wrong-env-workspace")
monkeypatch.setattr(
surface_module,
"build_completion_context",
fake_build_completion_context,
)
async with open_opentui_surface(
surface=Surface.CLI_STANDALONE,
ready_marker="",
print_ready_marker=False,
bridge=bridge,
workspace_dir=tmp_path,
):
pass
assert captured == {
"surface": Surface.CLI_STANDALONE,
"workspace_dir": tmp_path,
}
assert bridge.sent[-1] == (
"completion.context",
{
"catalog": (),
"files": ("src/main.py",),
"filters_sensitive_paths": True,
},
)
def test_opentui_output_toolbar_invalidates_router_plugin() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
output.set_toolbar("router_hud", "route standard -> fake-terminal 99% save 42%")
output.set_toolbar("router_hud_style", "normal")
output.set_toolbar("router_baseline_model", "vendor/big-model")
output.set_toolbar("router_source", "router")
output.set_toolbar("router_routing_applied", True)
output.set_toolbar("router_rollout_phase", "full")
output.invalidate()
assert bridge.sent == [
(
"router.update",
asdict(
RouterPluginState(
model="fake-terminal",
route="standard 99%",
saving="42%",
context="-",
style="normal",
baseline_model="vendor/big-model",
source="router",
routing_applied=True,
rollout_phase="full",
)
),
)
]
def test_router_plugin_state_carries_observe_source_and_usage() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
output.set_toolbar("router_hud", "observe standard -> fake-terminal 80%")
output.set_toolbar("router_hud_style", "dim")
output.set_toolbar("router_source", "observe")
output.set_toolbar("router_routing_applied", False)
output.set_toolbar("router_rollout_phase", "observe")
output.set_toolbar("router_usage", "1.2k/856")
output.invalidate()
(_, payload) = bridge.sent[0]
assert payload["source"] == "observe"
assert payload["routing_applied"] is False
assert payload["rollout_phase"] == "observe"
assert payload["context"] == "-"
assert payload["io"] == "1.2k/856"
def test_router_plugin_state_formats_cumulative_context_usage_percent() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
output.set_toolbar("router_hud", "observe standard -> fake-terminal 80%")
output.set_toolbar("router_hud_style", "dim")
output.set_toolbar("router_usage", "1/2")
output.set_toolbar("router_session_input", 84_000)
output.set_toolbar("router_context_window", 200_000)
output.invalidate()
(_, payload) = bridge.sent[0]
assert payload["context"] == "42%"
assert payload["io"] == "1/2"
@pytest.mark.parametrize(
("session_input", "context_window"),
[
(None, 200_000),
(84_000, None),
],
)
def test_router_plugin_state_reports_no_pressure_without_context_data(
session_input: int | None,
context_window: int | None,
) -> None:
"""Without a known context window there is no honest pressure value: ctx
shows "-" and the io field still carries the turn's token pair (the old
fallback packed the pair into ctx, reading like "1/2 of a window")."""
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
output.set_toolbar("router_hud", "observe standard -> fake-terminal 80%")
output.set_toolbar("router_hud_style", "dim")
output.set_toolbar("router_usage", "1/2")
output.set_toolbar("router_session_input", session_input)
output.set_toolbar("router_context_window", context_window)
output.invalidate()
(_, payload) = bridge.sent[0]
assert payload["context"] == "-"
assert payload["io"] == "1/2"
def test_router_plugin_state_reports_pressure_even_before_turn_usage() -> None:
"""Pressure derives from session input vs window and no longer waits on a
turn-usage pair; io stays empty until a turn reports traffic."""
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
output.set_toolbar("router_session_input", 84_000)
output.set_toolbar("router_context_window", 200_000)
output.invalidate()
(_, payload) = bridge.sent[0]
assert payload["context"] == "42%"
assert payload["io"] == ""
def test_router_plugin_state_clamps_context_usage_percent() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
output.set_toolbar("router_hud", "observe standard -> fake-terminal 80%")
output.set_toolbar("router_hud_style", "dim")
output.set_toolbar("router_usage", "1.2k/856")
output.set_toolbar("router_session_input", 250_000)
output.set_toolbar("router_context_window", 200_000)
output.invalidate()
(_, payload) = bridge.sent[0]
assert payload["context"] == "100%"
assert payload["io"] == "1.2k/856"
def test_router_plugin_state_fallback_keeps_defaults_and_usage() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
output.set_toolbar("router_hud", "fallback -> fake-terminal")
output.set_toolbar("router_hud_style", "warning")
output.set_toolbar("router_source", "fallback")
output.set_toolbar("router_usage", "856/12")
output.invalidate()
(_, payload) = bridge.sent[0]
assert payload["model"] == "fake-terminal"
assert payload["route"] == "fallback"
assert payload["source"] == "fallback"
assert payload["context"] == "-"
assert payload["io"] == "856/12"
assert payload["style"] == "warning"
def test_router_plugin_state_pending_defaults_without_usage() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
output.invalidate()
(_, payload) = bridge.sent[0]
assert payload["model"] == "pending"
assert payload["route"] == "pending"
assert payload["context"] == "-"
assert payload["baseline_model"] == ""
assert payload["routing_applied"] is True
assert payload["rollout_phase"] == "full"
@pytest.mark.asyncio
async def test_output_handle_send_message_forwards_to_bridge() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
await output.send_message("turn.begin", {"id": "t1"})
await output.send_message("model.text", {"text": "hi"})
assert bridge.sent == [
("turn.begin", {"id": "t1"}),
("model.text", {"text": "hi"}),
]
@pytest.mark.asyncio
async def test_stream_output_preserves_delta_order_while_pruning_tasks() -> None:
bridge = FakeOpenTuiBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
async with output.stream_output() as write:
for index in range(50):
write(f"delta-{index}")
await asyncio.sleep(0)
texts = [
payload["text"]
for message_type, payload in bridge.sent
if message_type == "scrollback.write"
]
assert texts == [f"delta-{index}" for index in range(50)]
class _FailingBridge(FakeOpenTuiBridge):
async def send(self, message_type: str, payload: object | None = None) -> None:
raise RuntimeError("pipe down")
@pytest.mark.asyncio
async def test_stream_output_surfaces_write_failure_on_next_delta() -> None:
bridge = _FailingBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
async with output.stream_output() as write:
write("first")
# Let the failed write task complete so the next delta reports it.
await asyncio.sleep(0)
await asyncio.sleep(0)
with pytest.raises(RuntimeError, match="pipe down"):
write("second")
@pytest.mark.asyncio
async def test_stream_output_surfaces_write_failure_at_context_exit() -> None:
bridge = _FailingBridge()
output = OpenTuiOutputHandle(bridge, approval_surface=Surface.CLI_GATEWAY)
with pytest.raises(RuntimeError, match="pipe down"):
async with output.stream_output() as write:
write("only")