4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
288 lines
8.1 KiB
Python
288 lines
8.1 KiB
Python
"""Tests for Rich rendering helpers used by the interactive shell."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import re
|
|
import threading
|
|
|
|
import pytest
|
|
from rich.console import Console
|
|
|
|
from surfaces.interactive_shell.runtime.core.state import SpinnerState
|
|
from surfaces.interactive_shell.ui.components.rendering import (
|
|
_repl_write_buffer,
|
|
print_repl_json,
|
|
refresh_welcome_poster,
|
|
repl_print,
|
|
repl_render_launch_poster,
|
|
repl_table,
|
|
)
|
|
from surfaces.interactive_shell.ui.streaming.console import StreamingConsole
|
|
from surfaces.interactive_shell.ui.tables import (
|
|
render_integrations_table,
|
|
render_mcp_table,
|
|
)
|
|
|
|
|
|
def test_repl_table_minimal_box() -> None:
|
|
t = repl_table(title="T")
|
|
assert t.title == "T"
|
|
|
|
|
|
def test_print_repl_json_tty_uses_single_buffered_write(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
class _FakeStdout:
|
|
def __init__(self) -> None:
|
|
self.writes: list[str] = []
|
|
|
|
def write(self, text: str) -> int:
|
|
self.writes.append(text)
|
|
return len(text)
|
|
|
|
def flush(self) -> None:
|
|
return None
|
|
|
|
def isatty(self) -> bool:
|
|
return True
|
|
|
|
fake_stdout = _FakeStdout()
|
|
monkeypatch.setattr("sys.stdout", fake_stdout)
|
|
|
|
console = Console(file=fake_stdout, force_terminal=True, width=80)
|
|
print_repl_json(console, '{"ok": true}')
|
|
|
|
assert len(fake_stdout.writes) == 1
|
|
rendered = re.sub(r"\x1b\[[0-9;]*m", "", fake_stdout.writes[0])
|
|
assert rendered.startswith("\r\n")
|
|
assert '"ok": true' in rendered
|
|
|
|
|
|
def test_render_integrations_table_empty_shows_hint() -> None:
|
|
buf = io.StringIO()
|
|
console = Console(file=buf, force_terminal=False)
|
|
render_integrations_table(console, [])
|
|
assert "opensre onboard" in buf.getvalue()
|
|
|
|
|
|
def test_repl_print_resets_before_each_line(monkeypatch) -> None:
|
|
resets: list[bool] = []
|
|
|
|
monkeypatch.setattr(
|
|
"surfaces.interactive_shell.ui.components.choice_menu.prepare_repl_output_line",
|
|
lambda: resets.append(True),
|
|
)
|
|
|
|
buf = io.StringIO()
|
|
console = Console(file=buf, force_terminal=False, width=80)
|
|
repl_print(console, "line one")
|
|
repl_print(console, "line two")
|
|
|
|
assert len(resets) == 2
|
|
|
|
|
|
def test_repl_print_does_not_double_prepare_with_streaming_console(monkeypatch) -> None:
|
|
resets: list[bool] = []
|
|
|
|
monkeypatch.setattr(
|
|
"surfaces.interactive_shell.ui.components.choice_menu.prepare_repl_output_line",
|
|
lambda: resets.append(True),
|
|
)
|
|
|
|
console = StreamingConsole(
|
|
SpinnerState(),
|
|
threading.Event(),
|
|
file=io.StringIO(),
|
|
force_terminal=False,
|
|
width=80,
|
|
)
|
|
repl_print(console, "line")
|
|
|
|
assert len(resets) == 1
|
|
|
|
|
|
def test_repl_print_streaming_console_prepares_tty_once_when_interactive(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class _FakeStdout:
|
|
def __init__(self) -> None:
|
|
self.writes: list[str] = []
|
|
|
|
def write(self, text: str) -> int:
|
|
self.writes.append(text)
|
|
return len(text)
|
|
|
|
def flush(self) -> None:
|
|
return None
|
|
|
|
def isatty(self) -> bool:
|
|
return True
|
|
|
|
fake_stdout = _FakeStdout()
|
|
monkeypatch.setattr("sys.stdout", fake_stdout)
|
|
monkeypatch.setattr(
|
|
"surfaces.interactive_shell.ui.components.choice_menu.repl_tty_interactive",
|
|
lambda: True,
|
|
)
|
|
|
|
console = StreamingConsole(
|
|
SpinnerState(),
|
|
threading.Event(),
|
|
file=io.StringIO(),
|
|
force_terminal=False,
|
|
width=80,
|
|
)
|
|
repl_print(console, "line")
|
|
|
|
assert fake_stdout.writes == ["\r\n", "\r"]
|
|
|
|
|
|
def test_repl_render_launch_poster_uses_crlf_on_tty(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
class _FakeStdout:
|
|
def __init__(self) -> None:
|
|
self.writes: list[str] = []
|
|
|
|
def write(self, text: str) -> int:
|
|
self.writes.append(text)
|
|
return len(text)
|
|
|
|
def flush(self) -> None:
|
|
return None
|
|
|
|
def isatty(self) -> bool:
|
|
return True
|
|
|
|
fake_stdout = _FakeStdout()
|
|
monkeypatch.setattr("sys.stdout", fake_stdout)
|
|
|
|
from platform.terminal.theme import set_active_theme
|
|
|
|
set_active_theme("blue")
|
|
console = Console(
|
|
file=fake_stdout,
|
|
force_terminal=True,
|
|
highlight=False,
|
|
color_system="truecolor",
|
|
width=120,
|
|
)
|
|
repl_render_launch_poster(console, theme_notice="blue")
|
|
|
|
written = "".join(fake_stdout.writes)
|
|
assert "theme set:" in written
|
|
assert "blue" in written
|
|
assert "38;2;168;212;255" in written
|
|
assert "185;237;175" not in written
|
|
assert "opensre" in written
|
|
assert "Welcome back" in written
|
|
assert "\r\n" in written
|
|
# REPL path must not emit bare \\n (causes double-spaced splash under patch_stdout).
|
|
assert "\r" not in written.replace("\r\n", "")
|
|
|
|
|
|
def test_repl_write_buffer_strips_only_escaped_cpr_sequences(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class _FakeStdout:
|
|
def __init__(self) -> None:
|
|
self.writes: list[str] = []
|
|
|
|
def write(self, text: str) -> int:
|
|
self.writes.append(text)
|
|
return len(text)
|
|
|
|
def flush(self) -> None:
|
|
return None
|
|
|
|
fake_stdout = _FakeStdout()
|
|
monkeypatch.setattr("sys.stdout", fake_stdout)
|
|
|
|
_repl_write_buffer("\x1b[1;1Rtheme set: pink 12;5R\r\n")
|
|
|
|
written = "".join(fake_stdout.writes)
|
|
assert "theme set: pink" in written
|
|
assert "12;5R" in written
|
|
assert "\x1b[1;1R" not in written
|
|
|
|
|
|
def test_refresh_welcome_poster_drains_cpr_after_clear(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
drains: list[str] = []
|
|
|
|
monkeypatch.setattr(
|
|
"surfaces.interactive_shell.ui.components.rendering.repl_clear_screen",
|
|
lambda: drains.append("clear"),
|
|
)
|
|
monkeypatch.setattr(
|
|
"surfaces.interactive_shell.ui.components.cpr_stdin.drain_stale_cpr_bytes",
|
|
lambda: drains.append("drain"),
|
|
)
|
|
monkeypatch.setattr(
|
|
"surfaces.interactive_shell.ui.components.rendering.repl_render_launch_poster",
|
|
lambda *_args, **_kwargs: drains.append("render"),
|
|
)
|
|
|
|
console = Console(file=io.StringIO(), force_terminal=False)
|
|
refresh_welcome_poster(console)
|
|
|
|
assert drains == ["clear", "drain", "render"]
|
|
|
|
|
|
def test_render_integrations_table_renders_content(
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
"""print_repl_table on a non-TTY console writes via console.print to stdout.
|
|
|
|
The cursor-reset (prepare_repl_output_line) is no longer called from the
|
|
table rendering path; on a real TTY the blank line and \\r\\n normalisation
|
|
are folded into a single sys.stdout.write call in print_repl_table.
|
|
"""
|
|
console = Console(force_terminal=False, width=80)
|
|
render_integrations_table(
|
|
console,
|
|
[
|
|
{
|
|
"service": "grafana",
|
|
"source": "local store",
|
|
"status": "passed",
|
|
"detail": "Connected to https://example.grafana.net",
|
|
}
|
|
],
|
|
)
|
|
|
|
assert "grafana" in capsys.readouterr().out
|
|
|
|
|
|
def test_render_integrations_table_sorts_services_and_includes_mcp(
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
console = Console(force_terminal=False, width=80)
|
|
render_integrations_table(
|
|
console,
|
|
[
|
|
{"service": "sentry", "source": "-", "status": "missing", "detail": "missing"},
|
|
{"service": "github", "source": "-", "status": "missing", "detail": "missing"},
|
|
{"service": "datadog", "source": "env", "status": "passed", "detail": "ok"},
|
|
],
|
|
)
|
|
|
|
output = capsys.readouterr().out
|
|
assert output.index("datadog") < output.index("github") < output.index("sentry")
|
|
assert "github" in output
|
|
|
|
|
|
def test_render_mcp_table_renders_content(
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
console = Console(force_terminal=False, width=80)
|
|
render_mcp_table(
|
|
console,
|
|
[
|
|
{
|
|
"service": "github",
|
|
"source": "local store",
|
|
"status": "configured",
|
|
"detail": "Connected",
|
|
}
|
|
],
|
|
)
|
|
|
|
assert "github" in capsys.readouterr().out
|