#!/usr/bin/env python3 """Exercise the TUI through the native Windows ConPTY boundary.""" from __future__ import annotations import os import shutil import sys import time from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] ENTER_ALT_SCREEN = "\x1b[?1049h" LEAVE_ALT_SCREEN = "\x1b[?1049l" def _read(process: Any, timeout: float) -> str: output: list[str] = [] deadline = time.monotonic() + timeout while time.monotonic() < deadline: try: chunk = process.read(65_536) except EOFError: break if chunk: output.append(chunk) else: time.sleep(0.01) return "".join(output) def _wait_for(process: Any, needle: str, timeout: float) -> str: output: list[str] = [] deadline = time.monotonic() + timeout while needle not in "".join(output) and time.monotonic() < deadline: output.append(_read(process, min(0.1, deadline - time.monotonic()))) text = "".join(output) if needle not in text: raise AssertionError(f"terminal output did not contain {needle!r}") return text def _wait_for_exit(process: Any, timeout: float) -> int: deadline = time.monotonic() + timeout while process.isalive() and time.monotonic() < deadline: time.sleep(0.05) if process.isalive(): process.close(force=True) raise AssertionError("TUI did not exit after Ctrl+C") return int(process.exitstatus or 0) def main() -> int: if os.name != "nt": raise SystemExit("ConPTY smoke test requires Windows") # PyWinPTY's high-level reader otherwise blocks when ConPTY has no new # bytes, preventing bounded CI assertions. os.environ["PYWINPTY_BLOCK"] = "0" from winpty import PtyProcess # type: ignore[import-not-found] from winpty.enums import Backend # type: ignore[import-not-found] bun = shutil.which("bun") if not bun: raise SystemExit("bun is required") env = { **os.environ, "NANOBOT_TUI_WS_URL": "ws://127.0.0.1:9/ws", "NANOBOT_TUI_API_URL": "", "NANOBOT_TUI_API_TOKEN": "", "NANOBOT_TUI_MODEL": "test/model", "NANOBOT_TUI_WORKSPACE": r"C:\nanobot-tui-conpty", "NANOBOT_TUI_VERSION": "test", "NANOBOT_TUI_ACCESS": "workspace access", "NANOBOT_TUI_THEME": "dark", # Headless ConPTY has no terminal emulator to answer optional OSC 66 # width probes. Keep this smoke test focused on application behavior. "OPENTUI_FORCE_EXPLICIT_WIDTH": "false", } process = PtyProcess.spawn( [bun, "src/index.ts"], cwd=str(ROOT), env=env, dimensions=(24, 80), backend=Backend.ConPTY, ) output: list[str] = [] try: output.append(_wait_for(process, ENTER_ALT_SCREEN, 15)) for grapheme in "abc中文🙂": process.write(grapheme) time.sleep(0.03) output.append(_read(process, 0.5)) process.setwinsize(18, 42) resized = _read(process, 0.75) output.append(resized) if "\x1b[18;" not in resized or ";42H" not in resized: raise AssertionError("TUI did not repaint to the resized ConPTY dimensions") # First Ctrl+C clears the draft. The second exits the app and must # restore the alternate screen without an unhandled exception. process.sendcontrol("c") output.append(_read(process, 0.2)) process.sendcontrol("c") output.append(_wait_for(process, LEAVE_ALT_SCREEN, 8)) exit_code = _wait_for_exit(process, 8) finally: if process.isalive(): process.close(force=True) text = "".join(output) if exit_code != 0: raise AssertionError(f"TUI exited with status {exit_code}") for glyph in "abc中文🙂": if glyph not in text: raise AssertionError(f"TUI did not render committed input {glyph!r}") if "Traceback" in text or "Task exception was never retrieved" in text: raise AssertionError("TUI emitted an exception during shutdown") print("ConPTY smoke test passed: Unicode input, resize, and terminal restoration") return 0 if __name__ == "__main__": sys.exit(main())