Files
hermes-agent/tests/tools/test_pty_query_responder.py
T
Teknium 7f61ae589f Port from openai/codex#41436: answer blocking terminal queries in background PTY sessions
Programs run via terminal(background=true, pty=true) can block forever when
they probe their terminal — device-status (ESC[5n), window-size (ESC[18t),
cursor-position (ESC[6n), or DEC private-mode (ESC[?N$p) queries — because
nothing on the PTY master side answers, and the raw query bytes leak into
captured output.

- tools/pty_query_responder.py: incremental byte scanner that strips the
  handled queries from PTY output (chunk splits included) and produces
  bounded replies; everything else passes through untouched.
- tools/process_registry.py: wire the responder into _pty_reader_loop
  (POSIX only — ConPTY answers its own queries); flush partial escape
  tails at end-of-stream.
- tests mirror the codex fixtures plus a live-PTY E2E where a subprocess
  blocks on ESC[6n until answered.
2026-09-12 22:09:19 -07:00

174 lines
5.1 KiB
Python

"""Tests for tools.pty_query_responder (port of openai/codex#41436).
Covers the byte-level scanner (exact queries, chunk splits, DEC private-mode
queries, passthrough of unhandled sequences) and a live PTY E2E where a
subprocess blocks on a cursor-position report until answered.
"""
import os
import sys
import time
import pytest
from tools.pty_query_responder import PtyQueryResponder
def test_plain_output_passes_through():
r = PtyQueryResponder()
out, resp = r.process(b"hello world\n")
assert out == b"hello world\n"
assert resp == b""
def test_device_status_report_answered_and_stripped():
r = PtyQueryResponder()
out, resp = r.process(b"before\x1b[5nafter")
assert out == b"beforeafter"
assert resp == b"\x1b[0n"
def test_window_size_query_reports_spawn_dimensions():
r = PtyQueryResponder(rows=30, cols=120)
out, resp = r.process(b"\x1b[18t")
assert out == b""
assert resp == b"\x1b[8;30;120t"
def test_cursor_position_report():
r = PtyQueryResponder()
out, resp = r.process(b"\x1b[6n")
assert out == b""
assert resp == b"\x1b[1;1R"
def test_dec_private_mode_query_reported_unrecognized():
r = PtyQueryResponder()
out, resp = r.process(b"\x1b[?1049$p")
assert out == b""
assert resp == b"\x1b[?1049;0$y"
def test_combined_stream_matches_codex_fixture():
# Mirrors the driver-backed test in openai/codex#41436: queries split
# across chunks, mixed with a color escape and plain text.
r = PtyQueryResponder()
out1, resp1 = r.process(b"before\x1b[")
out2, resp2 = r.process(b"5n\x1b[18t\x1b[6n\x1b[?1049$p\x1b[31mafter")
assert resp1 + resp2 == b"\x1b[0n\x1b[8;24;80t\x1b[1;1R\x1b[?1049;0$y"
assert out1 + out2 + r.flush() == b"before\x1b[31mafter"
def test_query_split_across_many_chunks():
r = PtyQueryResponder()
total_out = b""
total_resp = b""
for b in (b"\x1b", b"[", b"6", b"n"):
out, resp = r.process(b)
total_out += out
total_resp += resp
assert total_out == b""
assert total_resp == b"\x1b[1;1R"
def test_unhandled_csi_sequence_passes_through():
r = PtyQueryResponder()
out, resp = r.process(b"\x1b[31mred\x1b[0m")
assert out == b"\x1b[31mred\x1b[0m"
assert resp == b""
def test_non_csi_escape_passes_through():
r = PtyQueryResponder()
out, resp = r.process(b"\x1bMreverse")
assert out == b"\x1bMreverse"
assert resp == b""
def test_fresh_esc_aborts_partial_sequence():
r = PtyQueryResponder()
out, resp = r.process(b"\x1b[6\x1b[5n")
# The aborted partial "\x1b[6" is flushed through; the complete
# device-status query is answered and stripped.
assert out == b"\x1b[6"
assert resp == b"\x1b[0n"
def test_oversized_mode_query_passes_through():
r = PtyQueryResponder()
seq = b"\x1b[?12345678901$p" # 11 digits > MAX_MODE_DIGITS
out, resp = r.process(seq)
assert resp == b""
assert out + r.flush() == seq
def test_flush_returns_incomplete_tail():
r = PtyQueryResponder()
out, resp = r.process(b"text\x1b[1")
assert out == b"text"
assert resp == b""
assert r.flush() == b"\x1b[1"
# flush is destructive
assert r.flush() == b""
def test_dec_mode_non_digit_passes_through():
r = PtyQueryResponder()
seq = b"\x1b[?10a9$p"
out, resp = r.process(seq)
assert resp == b""
assert out + r.flush() == seq
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX ptyprocess only")
def test_live_pty_subprocess_unblocked_by_cursor_report(tmp_path):
"""E2E: a PTY subprocess blocking on ESC[6n exits once answered.
Mirrors direct_terminal_queries_are_answered from openai/codex#41436.
"""
ptyprocess = pytest.importorskip("ptyprocess")
from tools.pty_query_responder import PtyQueryResponder
script = (
"stty -echo -icanon; printf 'alpha\\033[6n'; "
"dd bs=1 count=6 2>/dev/null; printf '\\nok'"
)
proc = ptyprocess.PtyProcess.spawn(
["/bin/sh", "-c", script], dimensions=(24, 80)
)
responder = PtyQueryResponder()
output = b""
deadline = time.time() + 10
try:
while proc.isalive() and time.time() < deadline:
try:
chunk = proc.read(4096)
except EOFError:
break
if not chunk:
continue
out, replies = responder.process(chunk)
output += out
if replies:
proc.write(replies)
# Drain anything left after exit.
while True:
try:
chunk = proc.read(4096)
except EOFError:
break
if not chunk:
break
out, replies = responder.process(chunk)
output += out
finally:
if proc.isalive():
proc.terminate(force=True)
pytest.fail(f"subprocess still blocked; output={output!r}")
output += responder.flush()
assert b"alpha" in output
assert b"ok" in output
# The query itself must have been stripped from the captured output,
# and the echoed reply is what dd consumed (not visible w/ -echo).
assert b"\x1b[6n" not in output