"""Tests for dotfiles/common/.local/bin/tmux-util. The script is a bash wrapper around `tmux` with subcommand dispatch. Tests invoke the real script with a faked `tmux`, `kill`, and `sleep` on PATH. The fakes read canned state from a temp dir and append a one-line record to a call log on each invocation. Assertions compare the call log to the expected sequence for the scenario — we test behavior (what the script causes tmux / kill to do), not implementation. Run from repo root: python3 -m unittest tests.tmux-util.test_tmux_util """ import os import shutil import stat import subprocess import tempfile import unittest REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) SCRIPT = os.path.join(REPO_ROOT, "dotfiles/common/.local/bin/tmux-util") FAKES_DIR = os.path.dirname(__file__) class TmuxUtilHarness(unittest.TestCase): """Shared harness: run tmux-util with faked tmux/kill/sleep on PATH.""" def setUp(self): self.tmp = tempfile.mkdtemp(prefix="tmux-util-test-") # bin dir with the fakes symlinked under their canonical names self.bin_dir = os.path.join(self.tmp, "bin") os.makedirs(self.bin_dir) for name in ("tmux", "kill", "sleep", "fzf"): fake = os.path.join(FAKES_DIR, f"fake-{name}") os.chmod(fake, os.stat(fake).st_mode | stat.S_IEXEC) os.symlink(fake, os.path.join(self.bin_dir, name)) # Pre-create empty state + log files so the fakes never have to self.state_file = os.path.join(self.tmp, "sessions.txt") self.calls_log = os.path.join(self.tmp, "calls.log") self.kill_log = os.path.join(self.tmp, "kill.log") open(self.state_file, "w").close() open(self.calls_log, "w").close() open(self.kill_log, "w").close() def tearDown(self): shutil.rmtree(self.tmp, ignore_errors=True) def set_sessions(self, sessions): """sessions: list of tuples (name, attached, pids[, activity, windows, cwd]). Defaults for trailing fields: activity=0, windows=1, cwd='/tmp'. cwd must not contain spaces (the fake's state file is space-delimited). """ with open(self.state_file, "w") as f: for s in sessions: name = s[0] attached = s[1] pids = s[2] activity = s[3] if len(s) > 3 else 0 windows = s[4] if len(s) > 4 else 1 cwd = s[5] if len(s) > 5 else "/tmp" pids_csv = ",".join(str(p) for p in pids) if pids else "-" f.write(f"{name} {attached} {pids_csv} {activity} {windows} {cwd}\n") def run_script(self, *args, env_extra=None, stdin=None): env = os.environ.copy() # Prepend the bin dir so the fakes win env["PATH"] = self.bin_dir + os.pathsep + env.get("PATH", "") env["FAKE_TMUX_DIR"] = self.tmp if env_extra: env.update(env_extra) return subprocess.run( [SCRIPT] + list(args), env=env, input=stdin, capture_output=True, text=True, timeout=10, ) def tmux_calls(self): with open(self.calls_log) as f: return [line.rstrip("\n") for line in f if line.strip()] def kill_calls(self): with open(self.kill_log) as f: return [line.rstrip("\n") for line in f if line.strip()] def remaining_sessions(self): with open(self.state_file) as f: return [line.split()[0] for line in f if line.strip()] # ----------------------------------------------------------------------------- # Dispatch + usage # ----------------------------------------------------------------------------- class TestDispatch(TmuxUtilHarness): def test_no_args_prints_usage_to_stdout_and_exits_zero(self): result = self.run_script() self.assertEqual(result.returncode, 0) self.assertIn("Usage: tmux-util", result.stdout) self.assertIn("reap", result.stdout) def test_dash_h_prints_usage(self): result = self.run_script("-h") self.assertEqual(result.returncode, 0) self.assertIn("Usage: tmux-util", result.stdout) def test_double_dash_help_prints_usage(self): result = self.run_script("--help") self.assertEqual(result.returncode, 0) self.assertIn("Usage: tmux-util", result.stdout) def test_help_subcommand_prints_usage(self): result = self.run_script("help") self.assertEqual(result.returncode, 0) self.assertIn("Usage: tmux-util", result.stdout) def test_unknown_subcommand_exits_nonzero_and_prints_usage_to_stderr(self): result = self.run_script("frobnicate") self.assertNotEqual(result.returncode, 0) self.assertIn("unknown subcommand", result.stderr) self.assertIn("Usage: tmux-util", result.stderr) # ----------------------------------------------------------------------------- # Reap — Normal cases # ----------------------------------------------------------------------------- class TestReapNormal(TmuxUtilHarness): def test_reap_unattached_sends_sighup_then_kill_session(self): # bar is unattached, foo is attached. Only bar should be reaped. self.set_sessions([ ("foo", 1, [101, 102]), ("bar", 0, [201]), ]) result = self.run_script("reap") self.assertEqual(result.returncode, 0, msg=result.stderr) self.assertIn("Reaping: bar", result.stdout) self.assertNotIn("Reaping: foo", result.stdout) # SIGHUP went to bar's pid only, never to foo's self.assertIn("kill -HUP 201", self.kill_calls()) self.assertNotIn("kill -HUP 101", self.kill_calls()) self.assertNotIn("kill -HUP 102", self.kill_calls()) # Fallback kill-session ran (our fake-kill is a no-op, so the session # stays "alive" and the force-kill branch fires) tmux = self.tmux_calls() self.assertTrue( any("kill-session -t bar" in c for c in tmux), f"expected kill-session -t bar in {tmux!r}", ) def test_reap_skips_attached_sessions(self): self.set_sessions([ ("foo", 1, [101]), ("bar", 1, [201]), ]) result = self.run_script("reap") self.assertEqual(result.returncode, 0) self.assertIn("No unattached sessions", result.stdout) self.assertEqual(self.kill_calls(), []) def test_reap_skips_aiv_prefix_by_default(self): self.set_sessions([ ("aiv-claude", 0, [301]), ("aiv-gemini", 0, [302]), ("worker", 0, [401]), ]) result = self.run_script("reap") self.assertEqual(result.returncode, 0) self.assertIn("Reaping: worker", result.stdout) self.assertNotIn("Reaping: aiv-claude", result.stdout) self.assertNotIn("Reaping: aiv-gemini", result.stdout) self.assertIn("kill -HUP 401", self.kill_calls()) self.assertNotIn("kill -HUP 301", self.kill_calls()) self.assertNotIn("kill -HUP 302", self.kill_calls()) def test_reap_skip_pattern_overridable_via_env(self): # Override to match nothing → aiv-claude should now be reaped. self.set_sessions([ ("aiv-claude", 0, [301]), ]) result = self.run_script("reap", env_extra={"TMUX_UTIL_REAP_SKIP": "^never-matches-anything$"}) self.assertEqual(result.returncode, 0) self.assertIn("Reaping: aiv-claude", result.stdout) self.assertIn("kill -HUP 301", self.kill_calls()) def test_reap_session_with_multiple_panes_sighups_each(self): self.set_sessions([ ("multi", 0, [501, 502, 503]), ]) result = self.run_script("reap") self.assertEqual(result.returncode, 0) # xargs may collapse into one call; verify each PID is mentioned kc = "\n".join(self.kill_calls()) for pid in ("501", "502", "503"): self.assertIn(pid, kc, f"expected pid {pid} in kill calls: {kc!r}") # ----------------------------------------------------------------------------- # Reap — Boundary cases # ----------------------------------------------------------------------------- class TestReapBoundary(TmuxUtilHarness): def test_reap_no_sessions_at_all(self): self.set_sessions([]) result = self.run_script("reap") self.assertEqual(result.returncode, 0) self.assertIn("No unattached sessions", result.stdout) self.assertEqual(self.kill_calls(), []) def test_reap_session_with_no_panes_does_not_invoke_kill(self): # A session listed but with no PIDs — should still get kill-session. self.set_sessions([ ("ghost", 0, []), ]) result = self.run_script("reap") self.assertEqual(result.returncode, 0) self.assertIn("Reaping: ghost", result.stdout) # No kill -HUP because no PIDs self.assertEqual(self.kill_calls(), []) # But kill-session still ran (force kill, since fake-tmux didn't # auto-remove the session on its own) tmux = self.tmux_calls() self.assertTrue( any("kill-session -t ghost" in c for c in tmux), f"expected kill-session -t ghost in {tmux!r}", ) def test_reap_only_attached_and_skipped_sessions(self): # Mix that leaves no candidates after filtering. self.set_sessions([ ("foo", 1, [101]), # attached ("aiv-bar", 0, [201]), # skipped by pattern ]) result = self.run_script("reap") self.assertEqual(result.returncode, 0) self.assertIn("No unattached sessions", result.stdout) self.assertEqual(self.kill_calls(), []) # ----------------------------------------------------------------------------- # ls — Normal cases # ----------------------------------------------------------------------------- class TestLsNormal(TmuxUtilHarness): def test_ls_prints_header_and_columns_for_attached_session(self): # activity=950, now=1000 → idle 50s. cwd=/tmp/work → unchanged (not under HOME). self.set_sessions([ ("work", 1, [101], 950, 3, "/tmp/work"), ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) self.assertEqual(result.returncode, 0, msg=result.stderr) out = result.stdout self.assertIn("STATE", out) self.assertIn("NAME", out) self.assertIn("IDLE", out) self.assertIn("WINDOWS", out) self.assertIn("CWD", out) self.assertIn("attached", out) self.assertIn("work", out) self.assertIn("50s", out) self.assertIn("3", out) self.assertIn("/tmp/work", out) def test_ls_shows_detached_state_for_unattached_session(self): self.set_sessions([ ("idle", 0, [201], 900, 1, "/tmp/idle"), ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) self.assertEqual(result.returncode, 0) self.assertIn("detached", result.stdout) self.assertIn("idle", result.stdout) def test_ls_lists_multiple_sessions(self): self.set_sessions([ ("foo", 1, [101], 990, 2, "/tmp/foo"), ("bar", 0, [201], 900, 1, "/tmp/bar"), ("baz", 0, [301], 800, 4, "/tmp/baz"), ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) self.assertEqual(result.returncode, 0) for name in ("foo", "bar", "baz"): self.assertIn(name, result.stdout) # ----------------------------------------------------------------------------- # ls — Boundary cases # ----------------------------------------------------------------------------- class TestLsBoundary(TmuxUtilHarness): def test_ls_no_sessions_prints_message(self): self.set_sessions([]) result = self.run_script("ls") self.assertEqual(result.returncode, 0) self.assertIn("No tmux sessions", result.stdout) def test_ls_idle_zero_seconds(self): self.set_sessions([ ("now", 1, [101], 1000, 1, "/tmp/now"), ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) self.assertEqual(result.returncode, 0) self.assertIn("0s", result.stdout) def test_ls_idle_under_minute_shows_seconds(self): self.set_sessions([ ("s59", 0, [201], 941, 1, "/tmp/s"), # 59s ago ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) self.assertIn("59s", result.stdout) def test_ls_idle_exactly_one_minute_shows_minutes(self): self.set_sessions([ ("m1", 0, [201], 940, 1, "/tmp/m"), # 60s ago ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) self.assertIn("1m", result.stdout) def test_ls_idle_under_hour_shows_minutes(self): self.set_sessions([ ("m45", 0, [201], 0, 1, "/tmp/m"), # 2700s = 45m ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "2700"}) self.assertIn("45m", result.stdout) def test_ls_idle_exactly_one_hour_shows_hours(self): self.set_sessions([ ("h1", 0, [201], 0, 1, "/tmp/h"), # 3600s = 1h ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "3600"}) self.assertIn("1h", result.stdout) def test_ls_idle_under_day_shows_hours(self): self.set_sessions([ ("h23", 0, [201], 0, 1, "/tmp/h"), # 23 * 3600 = 82800s ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "82800"}) self.assertIn("23h", result.stdout) def test_ls_idle_exactly_one_day_shows_days(self): self.set_sessions([ ("d1", 0, [201], 0, 1, "/tmp/d"), # 86400s = 1d ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "86400"}) self.assertIn("1d", result.stdout) def test_ls_tilde_fies_home_paths(self): # cwd inside $HOME should render as ~/... home = os.environ.get("HOME", "/home/cjennings") cwd_under_home = home + "/code/foo" self.set_sessions([ ("h", 1, [101], 990, 1, cwd_under_home), ]) result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) self.assertEqual(result.returncode, 0) self.assertIn("~/code/foo", result.stdout) # The absolute path under HOME should NOT appear in raw form self.assertNotIn(cwd_under_home, result.stdout) # ----------------------------------------------------------------------------- # go — Normal cases # ----------------------------------------------------------------------------- class TestGoNormal(TmuxUtilHarness): def test_go_existing_session_outside_tmux_attaches(self): self.set_sessions([ ("work", 0, [201], 900, 1, "/tmp/work"), ]) # Outside tmux: TMUX env should be unset. env = {k: v for k, v in os.environ.items() if k != "TMUX"} result = subprocess.run( [SCRIPT, "go", "work"], env={**env, "PATH": self.bin_dir + os.pathsep + env.get("PATH", ""), "FAKE_TMUX_DIR": self.tmp}, capture_output=True, text=True, timeout=10, ) self.assertEqual(result.returncode, 0, msg=result.stderr) calls = self.tmux_calls() # has-session should be the first existence check (exact-match form) self.assertTrue(any("has-session" in c and "work" in c for c in calls), f"expected has-session probe in {calls!r}") self.assertTrue(any("attach-session -t work" in c for c in calls), f"expected attach-session -t work in {calls!r}") # Must NOT create a new session self.assertFalse(any("new-session" in c for c in calls), f"unexpected new-session in {calls!r}") def test_go_existing_session_inside_tmux_switches_client(self): self.set_sessions([ ("work", 0, [201], 900, 1, "/tmp/work"), ]) result = self.run_script("go", "work", env_extra={"TMUX": "/tmp/fake-tmux-socket,1234,0"}) self.assertEqual(result.returncode, 0, msg=result.stderr) calls = self.tmux_calls() self.assertTrue(any("switch-client -t work" in c for c in calls), f"expected switch-client in {calls!r}") # attach-session shouldn't be called inside tmux self.assertFalse(any("attach-session" in c for c in calls), f"unexpected attach-session in {calls!r}") def test_go_missing_session_outside_tmux_creates(self): self.set_sessions([]) env = {k: v for k, v in os.environ.items() if k != "TMUX"} result = subprocess.run( [SCRIPT, "go", "fresh"], env={**env, "PATH": self.bin_dir + os.pathsep + env.get("PATH", ""), "FAKE_TMUX_DIR": self.tmp}, cwd="/tmp", capture_output=True, text=True, timeout=10, ) self.assertEqual(result.returncode, 0, msg=result.stderr) calls = self.tmux_calls() # new-session with -s and -c pointing at the subprocess's cwd self.assertTrue( any("new-session" in c and "-s fresh" in c and "-c /tmp" in c for c in calls), f"expected new-session -s fresh -c /tmp in {calls!r}", ) def test_go_missing_session_inside_tmux_creates_detached_then_switches(self): self.set_sessions([]) env = os.environ.copy() env.update({ "PATH": self.bin_dir + os.pathsep + env.get("PATH", ""), "FAKE_TMUX_DIR": self.tmp, "TMUX": "/tmp/fake-tmux-socket,1234,0", }) result = subprocess.run( [SCRIPT, "go", "fresh"], env=env, cwd="/tmp", capture_output=True, text=True, timeout=10, ) self.assertEqual(result.returncode, 0, msg=result.stderr) calls = self.tmux_calls() self.assertTrue( any("new-session" in c and "-d" in c and "-s fresh" in c and "-c /tmp" in c for c in calls), f"expected detached new-session in {calls!r}", ) self.assertTrue(any("switch-client -t fresh" in c for c in calls), f"expected switch-client after create in {calls!r}") # ----------------------------------------------------------------------------- # go — Error / Boundary cases # ----------------------------------------------------------------------------- class TestGoErrors(TmuxUtilHarness): def test_go_with_no_name_exits_nonzero(self): result = self.run_script("go") self.assertNotEqual(result.returncode, 0) self.assertIn("missing session name", result.stderr) def test_go_with_empty_string_name_exits_nonzero(self): result = self.run_script("go", "") self.assertNotEqual(result.returncode, 0) self.assertIn("missing session name", result.stderr) # ----------------------------------------------------------------------------- # find — Normal cases # ----------------------------------------------------------------------------- class TestFindNormal(TmuxUtilHarness): def test_find_matches_pane_running_named_command(self): # Two sessions, one has a pane running vim; find should locate it. self.set_sessions([ ("work", 1, ["101:zsh", "102:vim"], 950, 2, "/tmp/work"), ("idle", 0, ["201:zsh"], 900, 1, "/tmp/idle"), ]) result = self.run_script("find", "vim") self.assertEqual(result.returncode, 0, msg=result.stderr) self.assertIn("work:", result.stdout) self.assertIn("vim", result.stdout) # idle session pane shouldn't be in the output self.assertNotIn("idle:", result.stdout) def test_find_matches_multiple_panes(self): self.set_sessions([ ("a", 1, ["101:zsh", "102:vim"], 950, 1, "/tmp/a"), ("b", 1, ["201:vim"], 900, 1, "/tmp/b"), ]) result = self.run_script("find", "vim") self.assertEqual(result.returncode, 0) self.assertIn("a:", result.stdout) self.assertIn("b:", result.stdout) # Two matches, two output lines lines = [ln for ln in result.stdout.strip().split("\n") if ln] self.assertEqual(len(lines), 2, msg=f"expected 2 matching lines, got {lines!r}") def test_find_output_includes_session_window_pane_location(self): self.set_sessions([ ("work", 1, ["101:zsh", "102:vim"], 950, 1, "/tmp/work"), ]) result = self.run_script("find", "vim") self.assertEqual(result.returncode, 0) # Expected format: work:0.1 vim (window 0, pane index 1) self.assertRegex(result.stdout, r"work:0\.1\s+vim") # ----------------------------------------------------------------------------- # find — Boundary / Error cases # ----------------------------------------------------------------------------- class TestFindBoundary(TmuxUtilHarness): def test_find_no_matches_exits_nonzero(self): self.set_sessions([ ("work", 1, ["101:zsh"], 950, 1, "/tmp/work"), ]) result = self.run_script("find", "nothing-matches-this") self.assertNotEqual(result.returncode, 0) self.assertEqual(result.stdout.strip(), "") def test_find_no_sessions_exits_nonzero(self): self.set_sessions([]) result = self.run_script("find", "vim") self.assertNotEqual(result.returncode, 0) def test_find_no_pattern_exits_nonzero(self): result = self.run_script("find") self.assertNotEqual(result.returncode, 0) self.assertIn("missing pattern", result.stderr) def test_find_empty_pattern_exits_nonzero(self): result = self.run_script("find", "") self.assertNotEqual(result.returncode, 0) self.assertIn("missing pattern", result.stderr) # ----------------------------------------------------------------------------- # pick — Normal cases # ----------------------------------------------------------------------------- class TestPickNormal(TmuxUtilHarness): def test_pick_attaches_chosen_session_outside_tmux(self): self.set_sessions([ ("foo", 1, ["101:zsh"], 950, 1, "/tmp/foo"), ("bar", 0, ["201:zsh"], 900, 1, "/tmp/bar"), ]) # Pick the second line (bar) env = {k: v for k, v in os.environ.items() if k != "TMUX"} result = subprocess.run( [SCRIPT, "pick"], env={**env, "PATH": self.bin_dir + os.pathsep + env.get("PATH", ""), "FAKE_TMUX_DIR": self.tmp, "FAKE_FZF_CHOICE_LINE": "2"}, capture_output=True, text=True, timeout=10, ) self.assertEqual(result.returncode, 0, msg=result.stderr) calls = self.tmux_calls() self.assertTrue(any("attach-session -t bar" in c for c in calls), f"expected attach-session -t bar in {calls!r}") def test_pick_switches_client_inside_tmux(self): self.set_sessions([ ("foo", 1, ["101:zsh"], 950, 1, "/tmp/foo"), ("bar", 0, ["201:zsh"], 900, 1, "/tmp/bar"), ]) result = self.run_script("pick", env_extra={ "TMUX": "/tmp/fake-tmux-socket,1234,0", "FAKE_FZF_CHOICE_LINE": "1", # first line → foo }) self.assertEqual(result.returncode, 0, msg=result.stderr) calls = self.tmux_calls() self.assertTrue(any("switch-client -t foo" in c for c in calls), f"expected switch-client -t foo in {calls!r}") self.assertFalse(any("attach-session" in c for c in calls), f"unexpected attach-session in {calls!r}") # ----------------------------------------------------------------------------- # pick — Boundary cases # ----------------------------------------------------------------------------- class TestPickBoundary(TmuxUtilHarness): def test_pick_no_sessions_prints_message(self): self.set_sessions([]) result = self.run_script("pick") self.assertEqual(result.returncode, 0) self.assertIn("No tmux sessions", result.stdout) # fzf should never have been invoked self.assertFalse(any(c.startswith("fzf ") for c in self.tmux_calls()), f"unexpected fzf call in {self.tmux_calls()!r}") def test_pick_user_cancels_fzf_returns_zero_no_attach(self): self.set_sessions([ ("foo", 1, ["101:zsh"], 950, 1, "/tmp/foo"), ]) # No FAKE_FZF_CHOICE / FAKE_FZF_CHOICE_LINE → fzf exits 130 (cancelled) result = self.run_script("pick") self.assertEqual(result.returncode, 0, msg=result.stderr) calls = self.tmux_calls() self.assertFalse(any("attach-session" in c or "switch-client" in c for c in calls), f"unexpected attach/switch in {calls!r}") # ----------------------------------------------------------------------------- # rename — Normal cases # ----------------------------------------------------------------------------- class TestRenameNormal(TmuxUtilHarness): def test_rename_picks_session_and_renames_it(self): self.set_sessions([ ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), ("other", 0, ["201:zsh"], 900, 1, "/tmp/other"), ]) result = self.run_script( "rename", env_extra={"FAKE_FZF_CHOICE_LINE": "1"}, # picks "old" stdin="new\n", ) self.assertEqual(result.returncode, 0, msg=result.stderr) calls = self.tmux_calls() self.assertTrue( any("rename-session -t old new" in c for c in calls), f"expected rename-session -t old new in {calls!r}", ) # State should reflect the rename names = self.remaining_sessions() self.assertIn("new", names) self.assertIn("other", names) self.assertNotIn("old", names) self.assertIn("Renamed: old", result.stdout) # ----------------------------------------------------------------------------- # rename — Boundary / Error cases # ----------------------------------------------------------------------------- class TestRenameBoundary(TmuxUtilHarness): def test_rename_no_sessions_prints_message(self): self.set_sessions([]) result = self.run_script("rename") self.assertEqual(result.returncode, 0) self.assertIn("No tmux sessions", result.stdout) # fzf should never have been invoked self.assertFalse(any(c.startswith("fzf ") for c in self.tmux_calls())) def test_rename_user_cancels_fzf_no_action(self): self.set_sessions([ ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), ]) # No FAKE_FZF_CHOICE → fzf exits 130 result = self.run_script("rename") self.assertEqual(result.returncode, 0, msg=result.stderr) self.assertFalse(any("rename-session" in c for c in self.tmux_calls())) # State unchanged self.assertEqual(self.remaining_sessions(), ["old"]) def test_rename_empty_new_name_exits_nonzero(self): self.set_sessions([ ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), ]) result = self.run_script( "rename", env_extra={"FAKE_FZF_CHOICE_LINE": "1"}, stdin="\n", # empty new name ) self.assertNotEqual(result.returncode, 0) self.assertIn("empty new name", result.stderr) self.assertFalse(any("rename-session" in c for c in self.tmux_calls())) def test_rename_same_name_is_noop(self): self.set_sessions([ ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), ]) result = self.run_script( "rename", env_extra={"FAKE_FZF_CHOICE_LINE": "1"}, stdin="old\n", # same as current name ) self.assertEqual(result.returncode, 0, msg=result.stderr) self.assertIn("same as old", result.stdout) self.assertFalse(any("rename-session" in c for c in self.tmux_calls())) def test_rename_conflict_with_existing_exits_nonzero(self): self.set_sessions([ ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), ("taken", 0, ["201:zsh"], 900, 1, "/tmp/taken"), ]) result = self.run_script( "rename", env_extra={"FAKE_FZF_CHOICE_LINE": "1"}, # picks "old" stdin="taken\n", # collides with existing ) self.assertNotEqual(result.returncode, 0) self.assertIn("already exists", result.stderr) self.assertFalse(any("rename-session" in c for c in self.tmux_calls())) if __name__ == "__main__": unittest.main()