fix(tui): handle Windows PTY stdin and detached WS frames (#41953)
Two narrow Windows desktop fixes:
1. tools/process_registry.py — PTY stdin writes are now platform-aware.
pywinpty (Windows) expects str; ptyprocess (POSIX) expects bytes.
Previously bytes was unconditionally passed, producing a TypeError on
Windows ("'bytes' object cannot be converted to 'PyString'").
2. tui_gateway/server.py + ws.py — Detached WebSocket sessions now park on
a _DropTransport sink instead of _stdio_transport. In the desktop the
gateway runs in-process and stdout is captured by Electron into
desktop.log, so falling back to stdio leaked raw JSON-RPC frames into
the desktop log after WS disconnects. Orphan-reap semantics are
preserved via _ws_session_is_orphaned.
Verified on a Windows desktop install:
- pywinpty 2.0.15 rejects bytes / accepts str — reproduced exactly
- Focused suite green (write_stdin × 2, write_json_drops_detached_ws_frames,
ws_orphan_reap × 2)
- All 6 CI test shards green, e2e green, nix (ubuntu/macos) green
Salvage commit (21be7ca) fixes the new test referencing an undefined
_ThreadUnsafeStdout — uses the existing _ChunkyStdout helper.
This commit is contained in:
@@ -152,6 +152,21 @@ def test_write_json_returns_false_on_broken_pipe(monkeypatch):
|
||||
assert server.write_json({"ok": True}) is False
|
||||
|
||||
|
||||
def test_write_json_drops_detached_ws_frames(monkeypatch):
|
||||
out = _ChunkyStdout()
|
||||
monkeypatch.setattr(server, "_real_stdout", out)
|
||||
server._sessions["detached-sid"] = {"transport": server._detached_ws_transport}
|
||||
try:
|
||||
assert server.write_json({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event",
|
||||
"params": {"session_id": "detached-sid", "type": "message.delta"},
|
||||
}) is False
|
||||
assert out.parts == []
|
||||
finally:
|
||||
server._sessions.pop("detached-sid", None)
|
||||
|
||||
|
||||
def test_tui_verbose_tool_details_fail_closed_when_redaction_fails(monkeypatch):
|
||||
redact_module = types.ModuleType("agent.redact")
|
||||
|
||||
@@ -933,7 +948,7 @@ def test_ws_orphan_reap_closes_worker_when_session_stays_detached(monkeypatch):
|
||||
closed["worker"] = True
|
||||
|
||||
server._sessions["orphan-sid"] = _session(
|
||||
transport=server._stdio_transport,
|
||||
transport=server._detached_ws_transport,
|
||||
slash_worker=_FakeWorker(),
|
||||
running=False,
|
||||
)
|
||||
@@ -992,11 +1007,15 @@ def test_ws_orphan_reap_spares_reattached_session(monkeypatch):
|
||||
assert server._ws_session_is_orphaned(reattached) is False
|
||||
|
||||
# Mid-turn sessions are also spared even if detached.
|
||||
mid_turn = _session(transport=server._stdio_transport, running=True)
|
||||
mid_turn = _session(transport=server._detached_ws_transport, running=True)
|
||||
assert server._ws_session_is_orphaned(mid_turn) is False
|
||||
|
||||
# Already finalized sessions are spared (idempotency).
|
||||
done = _session(transport=server._stdio_transport, running=False, _finalized=True)
|
||||
done = _session(
|
||||
transport=server._detached_ws_transport,
|
||||
running=False,
|
||||
_finalized=True,
|
||||
)
|
||||
assert server._ws_session_is_orphaned(done) is False
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,44 @@ def _wait_until(predicate, timeout: float = 5.0, interval: float = 0.05) -> bool
|
||||
return False
|
||||
|
||||
|
||||
def test_write_stdin_uses_str_for_windows_pty(monkeypatch, registry):
|
||||
"""pywinpty expects str input; bytes raises a PyString conversion error."""
|
||||
written = []
|
||||
|
||||
class _FakePty:
|
||||
def write(self, value):
|
||||
written.append(value)
|
||||
|
||||
session = _make_session(sid="pty-win")
|
||||
session._pty = _FakePty()
|
||||
registry._running[session.id] = session
|
||||
monkeypatch.setattr("tools.process_registry._IS_WINDOWS", True)
|
||||
|
||||
result = registry.write_stdin(session.id, "hello\n")
|
||||
|
||||
assert result == {"status": "ok", "bytes_written": 6}
|
||||
assert written == ["hello\n"]
|
||||
assert isinstance(written[0], str)
|
||||
|
||||
|
||||
def test_write_stdin_uses_bytes_for_posix_pty(monkeypatch, registry):
|
||||
written = []
|
||||
|
||||
class _FakePty:
|
||||
def write(self, value):
|
||||
written.append(value)
|
||||
|
||||
session = _make_session(sid="pty-posix")
|
||||
session._pty = _FakePty()
|
||||
registry._running[session.id] = session
|
||||
monkeypatch.setattr("tools.process_registry._IS_WINDOWS", False)
|
||||
|
||||
result = registry.write_stdin(session.id, "hello\n")
|
||||
|
||||
assert result == {"status": "ok", "bytes_written": 6}
|
||||
assert written == [b"hello\n"]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Get / Poll
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user