aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-19 13:17:06 -0500
committerCraig Jennings <c@cjennings.net>2026-05-19 13:17:06 -0500
commitf91586248f467a2ba7a62f76caee1f40927655d9 (patch)
tree39ff6ae73fa53f8164c0a8ad104dc550313cad44 /tests
parent95e1e0be4905ff5d37b52f889f5dda65543e2b51 (diff)
downloadarchsetup-f91586248f467a2ba7a62f76caee1f40927655d9.tar.gz
archsetup-f91586248f467a2ba7a62f76caee1f40927655d9.zip
feat(tmux-util): add rename subcommand (fzf pick + prompt)
tmux-util rename closes out the original six-subcommand plan. The flow: 1. fzf-pick a session from the list. 2. Prompt for a new name on stdin. 3. Bail with a useful message on empty input, same-as-old, or conflict with an existing session. 4. Otherwise `tmux rename-session -t <old> <new>` and confirm. The conflict check uses `tmux has-session -t =<new>` with the same `=`-prefix exact-match guard as the go subcommand. Without it, tmux's default prefix matching would let `rename foo` succeed even when a session named `foobar` already exists, then surprise the user later. 5 new tests cover Normal cases (pick + rename happy path) and Boundary cases (no sessions, fzf cancel, empty new name, same-as-old no-op, conflict with existing session). The test harness's run_script grew an `stdin=` param so tests can feed the prompt input. fake-tmux picked up a rename-session handler that mutates the state file. Total suite: 48 tests, all green. Six subcommands shipped: go, pick, ls, find, reap, rename. The original "no args prints help" requirement still holds, and the stub-test for unimplemented subcommands got removed since everything's wired now.
Diffstat (limited to 'tests')
-rwxr-xr-xtests/tmux-util/fake-tmux28
-rw-r--r--tests/tmux-util/test_tmux_util.py105
2 files changed, 126 insertions, 7 deletions
diff --git a/tests/tmux-util/fake-tmux b/tests/tmux-util/fake-tmux
index b5a1e61..1b84956 100755
--- a/tests/tmux-util/fake-tmux
+++ b/tests/tmux-util/fake-tmux
@@ -151,6 +151,34 @@ case "$cmd" in
attach-session|switch-client)
# No state mutation needed — the call log already records intent.
;;
+ rename-session)
+ # Forms: rename-session -t <old> <new> OR rename-session <new>
+ old=""
+ new=""
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ -t) shift; old="$1"; shift ;;
+ *) new="$1"; shift ;;
+ esac
+ done
+ if [ -z "$old" ] || [ -z "$new" ]; then
+ echo "fake-tmux rename-session: need both -t <old> and <new>" >&2
+ exit 1
+ fi
+ tmp="$STATE.tmp"
+ : > "$tmp"
+ while IFS= read -r line; do
+ [ -n "$line" ] || continue
+ first="${line%% *}"
+ rest="${line#* }"
+ if [ "$first" = "$old" ]; then
+ printf '%s %s\n' "$new" "$rest" >> "$tmp"
+ else
+ printf '%s\n' "$line" >> "$tmp"
+ fi
+ done < "$STATE"
+ mv "$tmp" "$STATE"
+ ;;
kill-session)
session=""
while [ "$#" -gt 0 ]; do
diff --git a/tests/tmux-util/test_tmux_util.py b/tests/tmux-util/test_tmux_util.py
index afc1ebc..b3eab8d 100644
--- a/tests/tmux-util/test_tmux_util.py
+++ b/tests/tmux-util/test_tmux_util.py
@@ -66,7 +66,7 @@ class TmuxUtilHarness(unittest.TestCase):
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):
+ 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", "")
@@ -76,6 +76,7 @@ class TmuxUtilHarness(unittest.TestCase):
return subprocess.run(
[SCRIPT] + list(args),
env=env,
+ input=stdin,
capture_output=True,
text=True,
timeout=10,
@@ -127,12 +128,6 @@ class TestDispatch(TmuxUtilHarness):
self.assertIn("unknown subcommand", result.stderr)
self.assertIn("Usage: tmux-util", result.stderr)
- def test_unimplemented_subcommand_exits_nonzero(self):
- # rename stubs out for now; pick one to confirm the stub path.
- result = self.run_script("rename")
- self.assertNotEqual(result.returncode, 0)
- self.assertIn("not implemented yet", result.stderr)
-
# -----------------------------------------------------------------------------
# Reap — Normal cases
@@ -617,5 +612,101 @@ class TestPickBoundary(TmuxUtilHarness):
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()