Files
hermes-agent/tests/tools/test_pty_query_responder.py
ethernet 59624b7ace tests: replace every bare host skipif with platforms()
The OS lanes are marker-driven: list_os_marked_tests.py picks the files
a lane imports from their platforms() specs and the lane selects with
-m platforms. A test gated with skipif(sys.platform != "win32") is
therefore never imported on the Windows lane and skipped everywhere else
— it runs on no host. skipif(sys.platform == "win32") tests were merely
invisible to the lane bookkeeping, but the rule the tree now follows is
one host marker, never a bare skipif.

Mechanical mapping, semantics preserved: skip-on-Windows → "posix",
skip-off-Windows → "windows", skip-off-Linux → "linux", skip-on-macOS →
"not macos". The former skip reasons stay as trailing comments. A
non-host condition (os.geteuid() == 0) stays a separate skipif beside
the marker, spelled getattr(os, "geteuid", ...) so the decorator still
imports on Windows.

Where the conversion would stack two platforms() marks on one test (the
conftest rejects that at collection) the narrower mark wins:
- test_update_wedged_gateway: the class is already platforms("linux");
  its per-test "needs UNIX sockets" marks were redundant and are gone.
- test_process_registry.TestSystemdCgroupIsolation: the class-level
  skip-on-Windows moves onto the 11 methods that had no host mark; the
  11 platforms("linux") methods keep theirs.
- test_file_ops_single_roundtrip: the two fifo tests drop their
  platforms("linux") in favour of the module's "posix" (mkfifo exists on
  macOS; both tests already skip when it does not).
- test_linux_desktop_entry / test_gateway_job_teardown_live: duplicate
  or wider marks removed.
2026-09-21 19:18:15 -04: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.platforms("posix") # 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