From 25ca14a8a2b96f158fbdb4e71be121e6c0ddbac2 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 23 Jun 2026 01:53:14 +0000 Subject: [PATCH 1/4] fix(macos-container): forward TERM env var in container exec --tty Without TERM, Claude Code inside the container cannot determine which modifier-key protocol to enable (modifyOtherKeys / kitty). The inner PTY session has no terminal-type context, so Shift+Enter and Enter produce identical byte sequences (\r), making them indistinguishable. Pass the host TERM via --env TERM= on every container exec --interactive --tty call, falling back to xterm-256color when TERM is not set on the host. Non-TTY exec paths are unaffected. Closes #245 --- bot_bottle/backend/macos_container/bottle.py | 6 ++++ tests/unit/test_macos_container_bottle.py | 31 ++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/bot_bottle/backend/macos_container/bottle.py b/bot_bottle/backend/macos_container/bottle.py index 6919046..aa50cc0 100644 --- a/bot_bottle/backend/macos_container/bottle.py +++ b/bot_bottle/backend/macos_container/bottle.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os import subprocess from typing import Callable, cast @@ -47,6 +48,11 @@ class MacosContainerBottle(Bottle): cmd = ["container", "exec"] if tty: cmd.extend(["--interactive", "--tty"]) + # Forward TERM so Claude Code can enable modifier-key protocols + # (e.g. modifyOtherKeys). Without it the inner PTY session has no + # TERM and Shift+Enter is indistinguishable from plain Enter. + term = os.environ.get("TERM", "xterm-256color") + cmd.extend(["--env", f"TERM={term}"]) if self.agent_workdir and self.agent_workdir != "/home/node": cmd.extend(["--workdir", self.agent_workdir]) cmd.extend([self.name, self.agent_command, *full_argv]) diff --git a/tests/unit/test_macos_container_bottle.py b/tests/unit/test_macos_container_bottle.py index 04e1315..d650814 100644 --- a/tests/unit/test_macos_container_bottle.py +++ b/tests/unit/test_macos_container_bottle.py @@ -5,6 +5,7 @@ from __future__ import annotations import unittest from unittest.mock import patch +from bot_bottle.backend.macos_container import bottle as bottle_mod from bot_bottle.backend.macos_container.bottle import MacosContainerBottle @@ -16,12 +17,15 @@ class TestMacosContainerBottle(unittest.TestCase): None, agent_command="codex", ) + with patch.dict(bottle_mod.os.environ, {"TERM": "xterm-256color"}, clear=False): + argv = bottle.agent_argv(["run"]) self.assertEqual( [ "container", "exec", "--interactive", "--tty", + "--env", "TERM=xterm-256color", "bot-bottle-dev-abc", "codex", "run", ], - bottle.agent_argv(["run"]), + argv, ) def test_agent_argv_includes_workdir(self): @@ -31,15 +35,38 @@ class TestMacosContainerBottle(unittest.TestCase): None, agent_workdir="/home/node/workspace", ) + with patch.dict(bottle_mod.os.environ, {"TERM": "xterm-256color"}, clear=False): + argv = bottle.agent_argv([]) self.assertEqual( [ "container", "exec", "--interactive", "--tty", + "--env", "TERM=xterm-256color", "--workdir", "/home/node/workspace", "bot-bottle-dev-abc", "claude", ], - bottle.agent_argv([]), + argv, ) + def test_agent_argv_uses_host_term_value(self): + bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) + with patch.dict(bottle_mod.os.environ, {"TERM": "screen-256color"}, clear=False): + argv = bottle.agent_argv([]) + self.assertIn("--env", argv) + self.assertIn("TERM=screen-256color", argv) + + def test_agent_argv_term_falls_back_to_xterm_256color(self): + bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) + env_without_term = {k: v for k, v in bottle_mod.os.environ.items() if k != "TERM"} + with patch.object(bottle_mod.os, "environ", env_without_term): + argv = bottle.agent_argv([]) + self.assertIn("TERM=xterm-256color", argv) + + def test_agent_argv_no_tty_omits_term(self): + bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) + argv = bottle.agent_argv([], tty=False) + self.assertNotIn("--tty", argv) + self.assertNotIn("--env", argv) + def test_exec_pipes_script_to_shell(self): bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) with patch("bot_bottle.backend.macos_container.bottle.subprocess.run") as run: -- 2.52.0 From 1c11110da529846b991561a50761ed1b4cbc785f Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 23 Jun 2026 02:30:46 +0000 Subject: [PATCH 2/4] fix(macos-container): set host terminal to raw mode for container exec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apple's container exec --interactive --tty does not put the host terminal into raw mode before starting its I/O relay. In cooked (canonical) mode the kernel line discipline buffers modifier-key escape sequences — e.g. Shift+Enter in modifyOtherKeys mode generates \x1b[13;2~ — until a carriage-return arrives, so they never reach Claude Code inside the container. Add pty_forward.py, a stdlib-only wrapper (modelled on the existing smolmachines pty_resize.py) that sets the host terminal to raw mode via tty.setraw(), spawns the container exec command, and restores the original terminal attributes on exit. Falls back to a bare subprocess.run when stdin is not a TTY (piped invocations, CI) or when termios operations fail. Also retain the --env TERM= forwarding from the previous commit: without TERM inside the container session, Claude Code cannot determine which modifier-key protocol to enable even with raw mode correctly set. Non-TTY exec paths (bottle.exec, cp_in) are unaffected. --- bot_bottle/backend/macos_container/bottle.py | 28 ++-- .../backend/macos_container/pty_forward.py | 60 ++++++++ tests/unit/test_macos_container_bottle.py | 12 +- .../unit/test_macos_container_pty_forward.py | 136 ++++++++++++++++++ 4 files changed, 224 insertions(+), 12 deletions(-) create mode 100644 bot_bottle/backend/macos_container/pty_forward.py create mode 100644 tests/unit/test_macos_container_pty_forward.py diff --git a/bot_bottle/backend/macos_container/bottle.py b/bot_bottle/backend/macos_container/bottle.py index aa50cc0..1d6c79c 100644 --- a/bot_bottle/backend/macos_container/bottle.py +++ b/bot_bottle/backend/macos_container/bottle.py @@ -4,11 +4,16 @@ from __future__ import annotations import os import subprocess +import sys from typing import Callable, cast from ...agent_provider import PromptMode, prompt_args from .. import Bottle, ExecResult from ..terminal import exec_shell_script +from . import pty_forward as _pty_forward + + +_PTY_FORWARD_SCRIPT = _pty_forward.__file__ class MacosContainerBottle(Bottle): @@ -45,18 +50,25 @@ class MacosContainerBottle(Bottle): argv=full_argv, ) ) - cmd = ["container", "exec"] + container_exec = ["container", "exec"] if tty: - cmd.extend(["--interactive", "--tty"]) + container_exec.extend(["--interactive", "--tty"]) # Forward TERM so Claude Code can enable modifier-key protocols - # (e.g. modifyOtherKeys). Without it the inner PTY session has no - # TERM and Shift+Enter is indistinguishable from plain Enter. + # (e.g. modifyOtherKeys / kitty protocol). Without it the inner + # PTY session has no terminal-type context and Shift+Enter is + # indistinguishable from plain Enter. term = os.environ.get("TERM", "xterm-256color") - cmd.extend(["--env", f"TERM={term}"]) + container_exec.extend(["--env", f"TERM={term}"]) if self.agent_workdir and self.agent_workdir != "/home/node": - cmd.extend(["--workdir", self.agent_workdir]) - cmd.extend([self.name, self.agent_command, *full_argv]) - return cmd + container_exec.extend(["--workdir", self.agent_workdir]) + container_exec.extend([self.name, self.agent_command, *full_argv]) + if tty: + # Wrap with the raw-mode forwarder: container exec does not put + # the host terminal into raw mode itself, so the line discipline + # buffers modifier-key sequences until CR. The wrapper sets raw + # mode before exec and restores it on exit. + return [sys.executable, _PTY_FORWARD_SCRIPT, "--", *container_exec] + return container_exec def exec_agent(self, argv: list[str], *, tty: bool = True) -> int: agent_argv = self.agent_argv(argv, tty=tty) diff --git a/bot_bottle/backend/macos_container/pty_forward.py b/bot_bottle/backend/macos_container/pty_forward.py new file mode 100644 index 0000000..63d96fc --- /dev/null +++ b/bot_bottle/backend/macos_container/pty_forward.py @@ -0,0 +1,60 @@ +"""Host-side raw-mode wrapper for `container exec --interactive --tty`. + +Apple's `container exec --interactive --tty` does not set the host terminal to +raw mode before starting its I/O relay. Without raw mode the kernel line +discipline buffers modifier-key escape sequences (e.g. Shift+Enter in +modifyOtherKeys mode produces \\x1b[13;2~) until a carriage-return arrives, so +they never reach Claude Code inside the container. + +This module sets the host terminal to raw mode, spawns the inner argv (the +container exec command), and restores the original terminal attributes on +exit. When stdin is not a TTY (piped invocations, CI) it falls through to a +bare subprocess.run so callers do not need to special-case non-interactive +contexts. + +Usage (the `--` separator is the API contract — everything after it is the +inner command): + + python pty_forward.py -- container exec --interactive --tty +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import termios +import tty + + +def main(argv: list[str]) -> int: + """Entry point. ``argv`` shape: ``-- ``.""" + if len(argv) < 2 or argv[0] != "--": + sys.stderr.write( + "usage: python pty_forward.py -- \n" + ) + return 2 + inner = argv[1:] + + try: + fd = sys.stdin.fileno() + except OSError: + return subprocess.run(inner, check=False).returncode + + if not os.isatty(fd): + return subprocess.run(inner, check=False).returncode + + try: + old = termios.tcgetattr(fd) + except termios.error: + return subprocess.run(inner, check=False).returncode + + try: + tty.setraw(fd) + return subprocess.run(inner, check=False).returncode + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tests/unit/test_macos_container_bottle.py b/tests/unit/test_macos_container_bottle.py index d650814..e5ceb82 100644 --- a/tests/unit/test_macos_container_bottle.py +++ b/tests/unit/test_macos_container_bottle.py @@ -2,15 +2,16 @@ from __future__ import annotations +import sys import unittest from unittest.mock import patch from bot_bottle.backend.macos_container import bottle as bottle_mod -from bot_bottle.backend.macos_container.bottle import MacosContainerBottle +from bot_bottle.backend.macos_container.bottle import MacosContainerBottle, _PTY_FORWARD_SCRIPT class TestMacosContainerBottle(unittest.TestCase): - def test_agent_argv_uses_container_exec(self): + def test_agent_argv_uses_pty_forward_and_container_exec(self): bottle = MacosContainerBottle( "bot-bottle-dev-abc", lambda: None, @@ -21,6 +22,7 @@ class TestMacosContainerBottle(unittest.TestCase): argv = bottle.agent_argv(["run"]) self.assertEqual( [ + sys.executable, _PTY_FORWARD_SCRIPT, "--", "container", "exec", "--interactive", "--tty", "--env", "TERM=xterm-256color", "bot-bottle-dev-abc", "codex", "run", @@ -39,6 +41,7 @@ class TestMacosContainerBottle(unittest.TestCase): argv = bottle.agent_argv([]) self.assertEqual( [ + sys.executable, _PTY_FORWARD_SCRIPT, "--", "container", "exec", "--interactive", "--tty", "--env", "TERM=xterm-256color", "--workdir", "/home/node/workspace", @@ -51,7 +54,6 @@ class TestMacosContainerBottle(unittest.TestCase): bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) with patch.dict(bottle_mod.os.environ, {"TERM": "screen-256color"}, clear=False): argv = bottle.agent_argv([]) - self.assertIn("--env", argv) self.assertIn("TERM=screen-256color", argv) def test_agent_argv_term_falls_back_to_xterm_256color(self): @@ -61,11 +63,13 @@ class TestMacosContainerBottle(unittest.TestCase): argv = bottle.agent_argv([]) self.assertIn("TERM=xterm-256color", argv) - def test_agent_argv_no_tty_omits_term(self): + def test_agent_argv_no_tty_omits_wrapper_and_tty_flags(self): bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) argv = bottle.agent_argv([], tty=False) self.assertNotIn("--tty", argv) self.assertNotIn("--env", argv) + self.assertNotIn(_PTY_FORWARD_SCRIPT, argv) + self.assertEqual(["container", "exec", "bot-bottle-dev-abc", "claude"], argv) def test_exec_pipes_script_to_shell(self): bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) diff --git a/tests/unit/test_macos_container_pty_forward.py b/tests/unit/test_macos_container_pty_forward.py new file mode 100644 index 0000000..0fc22ca --- /dev/null +++ b/tests/unit/test_macos_container_pty_forward.py @@ -0,0 +1,136 @@ +"""Unit: macos-container pty_forward raw-mode wrapper (issue #245). + +Tests argument parsing, non-TTY fallback, and the raw-mode +setup/restore sequence without requiring a real terminal. +""" + +from __future__ import annotations + +import io +import termios +import unittest +from unittest.mock import MagicMock, patch + +from bot_bottle.backend.macos_container import pty_forward + + +def _fake_stdin(fd: int = 0) -> MagicMock: + """Return a mock stdin whose fileno() returns *fd*.""" + m = MagicMock() + m.fileno.return_value = fd + return m + + +class TestArgvParsing(unittest.TestCase): + def test_missing_separator_returns_error_exit_code(self): + with patch.object(pty_forward.sys, "stderr", new=io.StringIO()) as err: + rc = pty_forward.main(["container", "exec"]) + self.assertEqual(2, rc) + self.assertIn("usage:", err.getvalue()) + + def test_too_few_args_returns_error_exit_code(self): + with patch.object(pty_forward.sys, "stderr", new=io.StringIO()): + self.assertEqual(2, pty_forward.main([])) + self.assertEqual(2, pty_forward.main(["--"])) + + def test_separator_at_start_with_inner_is_valid(self): + with ( + patch.object(pty_forward.sys, "stdin", _fake_stdin()), + patch.object(pty_forward.os, "isatty", return_value=False), + patch.object(pty_forward.subprocess, "run") as run, + ): + run.return_value.returncode = 0 + rc = pty_forward.main(["--", "container", "exec"]) + self.assertEqual(0, rc) + run.assert_called_once_with(["container", "exec"], check=False) + + +class TestNonTtyFallback(unittest.TestCase): + def test_non_tty_stdin_runs_inner_directly(self): + with ( + patch.object(pty_forward.sys, "stdin", _fake_stdin()), + patch.object(pty_forward.os, "isatty", return_value=False), + patch.object(pty_forward.subprocess, "run") as run, + ): + run.return_value.returncode = 42 + rc = pty_forward.main( + ["--", "container", "exec", "--interactive", "--tty", "c", "claude"] + ) + self.assertEqual(42, rc) + run.assert_called_once_with( + ["container", "exec", "--interactive", "--tty", "c", "claude"], + check=False, + ) + + def test_fileno_error_runs_inner_directly(self): + bad_stdin = MagicMock() + bad_stdin.fileno.side_effect = OSError("pseudofile") + with ( + patch.object(pty_forward.sys, "stdin", bad_stdin), + patch.object(pty_forward.subprocess, "run") as run, + ): + run.return_value.returncode = 0 + rc = pty_forward.main(["--", "container", "exec"]) + run.assert_called_once_with(["container", "exec"], check=False) + self.assertEqual(0, rc) + + +class TestRawModeSetupAndRestore(unittest.TestCase): + def test_tty_stdin_sets_raw_mode_and_restores_on_exit(self): + saved_attrs = object() + with ( + patch.object(pty_forward.sys, "stdin", _fake_stdin()), + patch.object(pty_forward.os, "isatty", return_value=True), + patch.object(pty_forward.termios, "tcgetattr", return_value=saved_attrs), + patch.object(pty_forward.tty, "setraw") as setraw, + patch.object(pty_forward.termios, "tcsetattr") as tcsetattr, + patch.object(pty_forward.subprocess, "run") as run, + ): + run.return_value.returncode = 0 + rc = pty_forward.main(["--", "container", "exec"]) + + self.assertEqual(0, rc) + setraw.assert_called_once() + tcsetattr.assert_called_once_with( + unittest.mock.ANY, termios.TCSADRAIN, saved_attrs, + ) + + def test_tty_restores_on_subprocess_nonzero_exit(self): + saved_attrs = object() + with ( + patch.object(pty_forward.sys, "stdin", _fake_stdin()), + patch.object(pty_forward.os, "isatty", return_value=True), + patch.object(pty_forward.termios, "tcgetattr", return_value=saved_attrs), + patch.object(pty_forward.tty, "setraw"), + patch.object(pty_forward.termios, "tcsetattr") as tcsetattr, + patch.object(pty_forward.subprocess, "run") as run, + ): + run.return_value.returncode = 1 + rc = pty_forward.main(["--", "container", "exec"]) + + self.assertEqual(1, rc) + tcsetattr.assert_called_once_with( + unittest.mock.ANY, termios.TCSADRAIN, saved_attrs, + ) + + def test_tcgetattr_error_falls_back_to_bare_run(self): + with ( + patch.object(pty_forward.sys, "stdin", _fake_stdin()), + patch.object(pty_forward.os, "isatty", return_value=True), + patch.object( + pty_forward.termios, "tcgetattr", + side_effect=termios.error("not a tty"), + ), + patch.object(pty_forward.tty, "setraw") as setraw, + patch.object(pty_forward.subprocess, "run") as run, + ): + run.return_value.returncode = 0 + rc = pty_forward.main(["--", "container", "exec"]) + + setraw.assert_not_called() + run.assert_called_once_with(["container", "exec"], check=False) + self.assertEqual(0, rc) + + +if __name__ == "__main__": + unittest.main() -- 2.52.0 From 31b29631b62ca1ce4a0c39d1fab513d261aa162f Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 22 Jun 2026 22:57:16 -0400 Subject: [PATCH 3/4] fix(macos-container): forward terminal capability env --- bot_bottle/backend/macos_container/bottle.py | 34 ++++++++++++++---- .../backend/macos_container/pty_forward.py | 18 +++++++--- tests/unit/test_macos_container_bottle.py | 36 +++++++++++++------ .../unit/test_macos_container_pty_forward.py | 33 ++++++++++++++--- 4 files changed, 95 insertions(+), 26 deletions(-) diff --git a/bot_bottle/backend/macos_container/bottle.py b/bot_bottle/backend/macos_container/bottle.py index 1d6c79c..1a6f8c7 100644 --- a/bot_bottle/backend/macos_container/bottle.py +++ b/bot_bottle/backend/macos_container/bottle.py @@ -14,6 +14,29 @@ from . import pty_forward as _pty_forward _PTY_FORWARD_SCRIPT = _pty_forward.__file__ +_TERMINAL_ENV_NAMES = ( + "TERM", + "COLORTERM", + "TERM_PROGRAM", + "TERM_PROGRAM_VERSION", + "KITTY_WINDOW_ID", + "KITTY_PID", + "WEZTERM_PANE", + "WEZTERM_UNIX_SOCKET", + "GHOSTTY_BIN_DIR", + "GHOSTTY_RESOURCES_DIR", + "ITERM_SESSION_ID", + "VTE_VERSION", + "KONSOLE_VERSION", + "ALACRITTY_WINDOW_ID", +) + + +def _terminal_env_names() -> tuple[str, ...]: + return tuple( + name for name in _TERMINAL_ENV_NAMES + if name == "TERM" or os.environ.get(name) + ) class MacosContainerBottle(Bottle): @@ -53,12 +76,11 @@ class MacosContainerBottle(Bottle): container_exec = ["container", "exec"] if tty: container_exec.extend(["--interactive", "--tty"]) - # Forward TERM so Claude Code can enable modifier-key protocols - # (e.g. modifyOtherKeys / kitty protocol). Without it the inner - # PTY session has no terminal-type context and Shift+Enter is - # indistinguishable from plain Enter. - term = os.environ.get("TERM", "xterm-256color") - container_exec.extend(["--env", f"TERM={term}"]) + # Forward terminal capability hints so TUIs can enable modified-key + # protocols. Use bare env names: values stay in the child env, not + # on argv, and pty_forward supplies a TERM fallback when needed. + for name in _terminal_env_names(): + container_exec.extend(["--env", name]) if self.agent_workdir and self.agent_workdir != "/home/node": container_exec.extend(["--workdir", self.agent_workdir]) container_exec.extend([self.name, self.agent_command, *full_argv]) diff --git a/bot_bottle/backend/macos_container/pty_forward.py b/bot_bottle/backend/macos_container/pty_forward.py index 63d96fc..193ca7f 100644 --- a/bot_bottle/backend/macos_container/pty_forward.py +++ b/bot_bottle/backend/macos_container/pty_forward.py @@ -27,6 +27,16 @@ import termios import tty +def _inner_env() -> dict[str, str]: + env = dict(os.environ) + env.setdefault("TERM", "xterm-256color") + return env + + +def _run_inner(inner: list[str]) -> int: + return subprocess.run(inner, check=False, env=_inner_env()).returncode + + def main(argv: list[str]) -> int: """Entry point. ``argv`` shape: ``-- ``.""" if len(argv) < 2 or argv[0] != "--": @@ -39,19 +49,19 @@ def main(argv: list[str]) -> int: try: fd = sys.stdin.fileno() except OSError: - return subprocess.run(inner, check=False).returncode + return _run_inner(inner) if not os.isatty(fd): - return subprocess.run(inner, check=False).returncode + return _run_inner(inner) try: old = termios.tcgetattr(fd) except termios.error: - return subprocess.run(inner, check=False).returncode + return _run_inner(inner) try: tty.setraw(fd) - return subprocess.run(inner, check=False).returncode + return _run_inner(inner) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old) diff --git a/tests/unit/test_macos_container_bottle.py b/tests/unit/test_macos_container_bottle.py index e5ceb82..543cd74 100644 --- a/tests/unit/test_macos_container_bottle.py +++ b/tests/unit/test_macos_container_bottle.py @@ -18,13 +18,13 @@ class TestMacosContainerBottle(unittest.TestCase): None, agent_command="codex", ) - with patch.dict(bottle_mod.os.environ, {"TERM": "xterm-256color"}, clear=False): + with patch.dict(bottle_mod.os.environ, {}, clear=True): argv = bottle.agent_argv(["run"]) self.assertEqual( [ sys.executable, _PTY_FORWARD_SCRIPT, "--", "container", "exec", "--interactive", "--tty", - "--env", "TERM=xterm-256color", + "--env", "TERM", "bot-bottle-dev-abc", "codex", "run", ], argv, @@ -37,31 +37,45 @@ class TestMacosContainerBottle(unittest.TestCase): None, agent_workdir="/home/node/workspace", ) - with patch.dict(bottle_mod.os.environ, {"TERM": "xterm-256color"}, clear=False): + with patch.dict(bottle_mod.os.environ, {}, clear=True): argv = bottle.agent_argv([]) self.assertEqual( [ sys.executable, _PTY_FORWARD_SCRIPT, "--", "container", "exec", "--interactive", "--tty", - "--env", "TERM=xterm-256color", + "--env", "TERM", "--workdir", "/home/node/workspace", "bot-bottle-dev-abc", "claude", ], argv, ) - def test_agent_argv_uses_host_term_value(self): + def test_agent_argv_forwards_terminal_env_names_without_values(self): bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) - with patch.dict(bottle_mod.os.environ, {"TERM": "screen-256color"}, clear=False): + with patch.dict( + bottle_mod.os.environ, + { + "TERM": "screen-256color", + "TERM_PROGRAM": "WezTerm", + "WEZTERM_PANE": "pane-id", + "SHELL": "/bin/zsh", + }, + clear=True, + ): argv = bottle.agent_argv([]) - self.assertIn("TERM=screen-256color", argv) + self.assertIn("TERM", argv) + self.assertIn("TERM_PROGRAM", argv) + self.assertIn("WEZTERM_PANE", argv) + self.assertNotIn("SHELL", argv) + self.assertNotIn("TERM=screen-256color", argv) + self.assertNotIn("TERM_PROGRAM=WezTerm", argv) + self.assertNotIn("WEZTERM_PANE=pane-id", argv) - def test_agent_argv_term_falls_back_to_xterm_256color(self): + def test_agent_argv_always_forwards_term_name(self): bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) - env_without_term = {k: v for k, v in bottle_mod.os.environ.items() if k != "TERM"} - with patch.object(bottle_mod.os, "environ", env_without_term): + with patch.dict(bottle_mod.os.environ, {}, clear=True): argv = bottle.agent_argv([]) - self.assertIn("TERM=xterm-256color", argv) + self.assertIn("TERM", argv) def test_agent_argv_no_tty_omits_wrapper_and_tty_flags(self): bottle = MacosContainerBottle("bot-bottle-dev-abc", lambda: None, None) diff --git a/tests/unit/test_macos_container_pty_forward.py b/tests/unit/test_macos_container_pty_forward.py index 0fc22ca..e4b9ca2 100644 --- a/tests/unit/test_macos_container_pty_forward.py +++ b/tests/unit/test_macos_container_pty_forward.py @@ -42,7 +42,9 @@ class TestArgvParsing(unittest.TestCase): run.return_value.returncode = 0 rc = pty_forward.main(["--", "container", "exec"]) self.assertEqual(0, rc) - run.assert_called_once_with(["container", "exec"], check=False) + run.assert_called_once() + self.assertEqual(["container", "exec"], run.call_args.args[0]) + self.assertFalse(run.call_args.kwargs["check"]) class TestNonTtyFallback(unittest.TestCase): @@ -57,10 +59,12 @@ class TestNonTtyFallback(unittest.TestCase): ["--", "container", "exec", "--interactive", "--tty", "c", "claude"] ) self.assertEqual(42, rc) - run.assert_called_once_with( + run.assert_called_once() + self.assertEqual( ["container", "exec", "--interactive", "--tty", "c", "claude"], - check=False, + run.call_args.args[0], ) + self.assertFalse(run.call_args.kwargs["check"]) def test_fileno_error_runs_inner_directly(self): bad_stdin = MagicMock() @@ -71,7 +75,9 @@ class TestNonTtyFallback(unittest.TestCase): ): run.return_value.returncode = 0 rc = pty_forward.main(["--", "container", "exec"]) - run.assert_called_once_with(["container", "exec"], check=False) + run.assert_called_once() + self.assertEqual(["container", "exec"], run.call_args.args[0]) + self.assertFalse(run.call_args.kwargs["check"]) self.assertEqual(0, rc) @@ -128,9 +134,26 @@ class TestRawModeSetupAndRestore(unittest.TestCase): rc = pty_forward.main(["--", "container", "exec"]) setraw.assert_not_called() - run.assert_called_once_with(["container", "exec"], check=False) + run.assert_called_once() + self.assertEqual(["container", "exec"], run.call_args.args[0]) + self.assertFalse(run.call_args.kwargs["check"]) self.assertEqual(0, rc) + def test_inner_run_sets_term_default_without_mutating_process_env(self): + with ( + patch.dict(pty_forward.os.environ, {}, clear=True), + patch.object(pty_forward.subprocess, "run") as run, + ): + run.return_value.returncode = 0 + rc = pty_forward._run_inner(["container", "exec"]) + + self.assertNotIn("TERM", pty_forward.os.environ) + + self.assertEqual(0, rc) + child_env = run.call_args.kwargs["env"] + self.assertEqual(["TERM"], sorted(child_env.keys())) + self.assertEqual("xterm-256color", child_env["TERM"]) + if __name__ == "__main__": unittest.main() -- 2.52.0 From d314ccf455468c5ef2b35f428c712790897500e8 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 23 Jun 2026 03:02:03 +0000 Subject: [PATCH 4/4] test(macos-container): satisfy pyright mock typing --- tests/unit/test_macos_container_pty_forward.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_macos_container_pty_forward.py b/tests/unit/test_macos_container_pty_forward.py index e4b9ca2..bebb047 100644 --- a/tests/unit/test_macos_container_pty_forward.py +++ b/tests/unit/test_macos_container_pty_forward.py @@ -9,7 +9,7 @@ from __future__ import annotations import io import termios import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import ANY, MagicMock, patch from bot_bottle.backend.macos_container import pty_forward @@ -98,7 +98,7 @@ class TestRawModeSetupAndRestore(unittest.TestCase): self.assertEqual(0, rc) setraw.assert_called_once() tcsetattr.assert_called_once_with( - unittest.mock.ANY, termios.TCSADRAIN, saved_attrs, + ANY, termios.TCSADRAIN, saved_attrs, ) def test_tty_restores_on_subprocess_nonzero_exit(self): @@ -116,7 +116,7 @@ class TestRawModeSetupAndRestore(unittest.TestCase): self.assertEqual(1, rc) tcsetattr.assert_called_once_with( - unittest.mock.ANY, termios.TCSADRAIN, saved_attrs, + ANY, termios.TCSADRAIN, saved_attrs, ) def test_tcgetattr_error_falls_back_to_bare_run(self): -- 2.52.0