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

350 lines
11 KiB
Python

from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
import pytest
HARNESS_PARENT = Path(__file__).resolve().parents[1]
if str(HARNESS_PARENT) not in sys.path:
sys.path.insert(0, str(HARNESS_PARENT))
from tui_real_terminal import driver # noqa: E402
from tui_real_terminal.driver import ( # noqa: E402
PtyTerminalSession,
TerminalFrame,
TerminalSize,
TmuxTerminalSession,
build_run_id,
open_real_terminal_session,
probe_terminal_capabilities,
)
class _FakePtyModule:
"""Stand-in for the Unix-only :mod:`pty` module.
Probe logic keys off ``driver.pty`` exposing ``openpty``; on Windows
``import pty`` yields ``None``. Patching this fake in lets the capability
tests exercise the probe's PTY branch on any platform instead of skipping.
"""
@staticmethod
def openpty() -> tuple[int, int]: # pragma: no cover - never invoked in probe
raise NotImplementedError
def _force_pty_module(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(driver, "pty", _FakePtyModule)
def test_terminal_size_validates_positive_dimensions() -> None:
size = TerminalSize(cols=100, rows=30)
assert size.cols == 100
assert size.rows == 30
with pytest.raises(ValueError, match="terminal size must be positive"):
TerminalSize(cols=0, rows=30)
with pytest.raises(ValueError, match="terminal size must be positive"):
TerminalSize(cols=100, rows=-1)
def test_terminal_frame_records_checkpoint_text_time_and_size() -> None:
size = TerminalSize(cols=80, rows=24)
frame = TerminalFrame(
checkpoint="ready",
text="OPEN_SQUILLA_TUI_READY",
captured_at_ms=123,
size=size,
)
assert frame.checkpoint == "ready"
assert frame.text == "OPEN_SQUILLA_TUI_READY"
assert frame.captured_at_ms == 123
assert frame.size is size
def test_build_run_id_is_tmux_safe(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(driver, "_run_id_suffix", lambda: "123456789-777-0")
run_id = build_run_id(" Launch input/loop!? ")
assert run_id == "opensquilla-tui-launch-input-loop-123456789-777-0"
assert all(ch.isalnum() or ch in "-_" for ch in run_id)
def test_build_run_id_uses_scenario_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(driver, "_run_id_suffix", lambda: "42-777-0")
assert build_run_id(" !!! ") == "opensquilla-tui-scenario-42-777-0"
def test_capability_probe_prefers_tmux_when_available(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fake_which(name: str) -> str | None:
return "/usr/bin/tmux" if name == "tmux" else None
monkeypatch.setattr(driver.shutil, "which", fake_which)
monkeypatch.setattr(driver.sys, "platform", "linux")
_force_pty_module(monkeypatch)
capabilities = probe_terminal_capabilities()
assert capabilities.tmux_available is True
assert capabilities.pty_available is True
assert capabilities.screenshot_available is False
assert capabilities.resize_available is True
assert capabilities.preferred_driver == "tmux"
assert capabilities.skip_reason is None
def test_capability_probe_falls_back_to_pty(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(driver.shutil, "which", lambda name: None)
monkeypatch.setattr(driver.sys, "platform", "linux")
_force_pty_module(monkeypatch)
capabilities = probe_terminal_capabilities()
assert capabilities.tmux_available is False
assert capabilities.pty_available is True
assert capabilities.preferred_driver == "pty"
assert capabilities.skip_reason is None
def test_capability_probe_reports_none_when_no_terminal_driver(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(driver.shutil, "which", lambda name: None)
monkeypatch.setattr(driver.sys, "platform", "win32")
capabilities = probe_terminal_capabilities()
assert capabilities.tmux_available is False
assert capabilities.pty_available is False
assert capabilities.preferred_driver == "none"
assert capabilities.resize_available is False
assert capabilities.skip_reason is not None
assert "WSL2" in capabilities.skip_reason
def test_factory_selects_available_driver_and_reports_unavailable(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(driver.shutil, "which", lambda name: None)
monkeypatch.setattr(driver.sys, "platform", "linux")
_force_pty_module(monkeypatch)
session = open_real_terminal_session(
command=[sys.executable, "-c", "print('ready')"],
cwd=tmp_path,
env={},
run_id="opensquilla-tui-test-1",
size=TerminalSize(),
artifact_dir=tmp_path,
driver="auto",
)
assert isinstance(session, PtyTerminalSession)
assert session.kind == "pty"
with pytest.raises(RuntimeError, match="requested terminal driver 'tmux' is unavailable"):
open_real_terminal_session(
command=[sys.executable, "-c", "print('ready')"],
cwd=tmp_path,
env={},
run_id="opensquilla-tui-test-2",
size=TerminalSize(),
artifact_dir=tmp_path,
driver="tmux",
)
def test_factory_reports_when_no_driver_is_available(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(driver.shutil, "which", lambda name: None)
monkeypatch.setattr(driver.sys, "platform", "win32")
with pytest.raises(RuntimeError, match="WSL2"):
open_real_terminal_session(
command=[sys.executable, "-c", "print('ready')"],
cwd=tmp_path,
env={},
run_id="opensquilla-tui-test-3",
size=TerminalSize(),
artifact_dir=tmp_path,
)
def test_tmux_session_uses_owned_session_commands(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
calls: list[tuple[list[str], dict[str, Any]]] = []
def fake_run(
args: list[str],
**kwargs: Any,
) -> subprocess.CompletedProcess[str]:
calls.append((args, kwargs))
if args[:2] == ["tmux", "capture-pane"]:
return subprocess.CompletedProcess(args, 0, stdout="ready screen")
if args[:2] == ["tmux", "has-session"]:
return subprocess.CompletedProcess(args, 0)
return subprocess.CompletedProcess(args, 0)
monkeypatch.setattr(driver.subprocess, "run", fake_run)
session = TmuxTerminalSession(
command=["python", "-c", "print('ready')"],
cwd=tmp_path,
env={"TERM": "xterm-256color"},
run_id="opensquilla-tui-owned-1",
size=TerminalSize(cols=100, rows=30),
terminal_log=tmp_path / "terminal.log",
)
session.start()
session.send_text("hello")
session.send_key("C-c")
session.paste("line 1\nline 2")
session.resize(TerminalSize(cols=120, rows=40))
frame = session.wait_for_text("ready", timeout_s=0.1, checkpoint="ready")
alive = session.is_alive()
session.terminate()
assert session.kind == "tmux"
assert calls[0][0][:5] == ["tmux", "new-session", "-d", "-s", "opensquilla-tui-owned-1"]
assert ["tmux", "send-keys", "-t", "opensquilla-tui-owned-1", "-l", "hello"] in [
call for call, _ in calls
]
assert ["tmux", "send-keys", "-t", "opensquilla-tui-owned-1", "Enter"] in [
call for call, _ in calls
]
assert ["tmux", "send-keys", "-t", "opensquilla-tui-owned-1", "C-c"] in [
call for call, _ in calls
]
paste_call = next(call for call in calls if call[0][:3] == ["tmux", "load-buffer", "-b"])
assert paste_call[1]["input"] == "line 1\nline 2"
assert ["tmux", "resize-window", "-t", "opensquilla-tui-owned-1", "-x", "120", "-y", "40"] in [
call for call, _ in calls
]
assert frame.text == "ready screen"
assert frame.size == TerminalSize(cols=120, rows=40)
assert alive is True
assert calls[-1][0] == ["tmux", "kill-session", "-t", "opensquilla-tui-owned-1"]
@pytest.mark.skipif(sys.platform == "win32", reason="PTY fallback is only available on Unix")
def test_pty_session_drives_text_process_and_cleans_up(tmp_path: Path) -> None:
command = [
sys.executable,
"-u",
"-c",
(
"import sys\n"
"print('READY', flush=True)\n"
"for line in sys.stdin:\n"
" print('ECHO:' + line.strip(), flush=True)\n"
),
]
session = PtyTerminalSession(
command=command,
cwd=tmp_path,
env=os.environ.copy(),
run_id="opensquilla-tui-pty-1",
size=TerminalSize(cols=80, rows=24),
terminal_log=tmp_path / "terminal.log",
)
try:
session.start()
ready = session.wait_for_text("READY", timeout_s=2, checkpoint="ready")
session.send_text("hello")
echoed = session.wait_for_text("ECHO:hello", timeout_s=2, checkpoint="echo")
session.paste("draft")
session.send_key("Enter")
pasted = session.wait_for_text("ECHO:draft", timeout_s=2, checkpoint="paste")
session.resize(TerminalSize(cols=90, rows=20))
assert session.kind == "pty"
assert "READY" in ready.text
assert "ECHO:hello" in echoed.text
assert "ECHO:draft" in pasted.text
assert session.size == TerminalSize(cols=90, rows=20)
assert session.is_alive() is True
finally:
session.terminate()
assert session.is_alive() is False
def test_tmux_alternate_screen_probe_reads_pane_flag(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
responses = iter(["1\n", "0\n"])
def fake_run(
args: list[str],
**kwargs: Any,
) -> subprocess.CompletedProcess[str]:
assert args == [
"tmux",
"display-message",
"-p",
"-t",
"opensquilla-tui-owned-3",
"#{alternate_on}",
]
return subprocess.CompletedProcess(args, 0, stdout=next(responses))
monkeypatch.setattr(driver.subprocess, "run", fake_run)
session = TmuxTerminalSession(
command=["python", "-c", "print('ready')"],
cwd=tmp_path,
env={},
run_id="opensquilla-tui-owned-3",
size=TerminalSize(),
terminal_log=tmp_path / "terminal.log",
)
assert session.alternate_screen_active() is True
assert session.alternate_screen_active() is False
def test_wait_for_text_times_out_with_last_screen(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fake_run(
args: list[str],
**kwargs: Any,
) -> subprocess.CompletedProcess[str]:
if args[:2] == ["tmux", "capture-pane"]:
return subprocess.CompletedProcess(args, 0, stdout="not yet")
return subprocess.CompletedProcess(args, 0)
monkeypatch.setattr(driver.subprocess, "run", fake_run)
session = TmuxTerminalSession(
command=["python", "-c", "print('ready')"],
cwd=tmp_path,
env={},
run_id="opensquilla-tui-owned-2",
size=TerminalSize(),
terminal_log=tmp_path / "terminal.log",
)
with pytest.raises(TimeoutError, match="timed out waiting for 'missing'.*not yet"):
session.wait_for_text("missing", timeout_s=0, checkpoint="missing")