test(tui): exercise Windows ConPTY boundary

This commit is contained in:
Xubin Ren
2026-08-17 20:56:10 +08:00
parent c0e9ce77ef
commit 7ed37e5b70
2 changed files with 141 additions and 0 deletions
+14
View File
@@ -223,6 +223,20 @@ jobs:
working-directory: tui
run: python3 scripts/pty_smoke.py
- name: Set up Python for ConPTY smoke test
if: runner.os == 'Windows'
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Test TUI in a real ConPTY terminal
if: runner.os == 'Windows'
working-directory: tui
shell: pwsh
run: |
python -m pip install --disable-pip-version-check pywinpty==3.0.5
python scripts/conpty_smoke.py
- name: Build TUI
working-directory: tui
run: bun run build
+127
View File
@@ -0,0 +1,127 @@
#!/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())