diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-02 12:16:38 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-02 12:16:38 -0500 |
| commit | b10cba594db836c0747066addad48bda4d30cd02 (patch) | |
| tree | 063119a623fa3f7139feda4ef302896d8f5f934c /tests/tmux-util | |
| parent | 49c2ba9c4510bf6e1acd306687473bc8ba9ad8dd (diff) | |
| download | archsetup-b10cba594db836c0747066addad48bda4d30cd02.tar.gz archsetup-b10cba594db836c0747066addad48bda4d30cd02.zip | |
refactor: drop in-repo dotfiles/, move stow tooling to the dotfiles repo
Since the installer clones DOTFILES_REPO into ~/.dotfiles and stows from there, the in-repo dotfiles/ tree was dead weight. Nothing reads it at install time. I removed it (831 files) now that both machines are migrated.
The Makefile's stow / restow / reset / unstow / import targets and the dotfile-script unit suites moved to the dotfiles repo. They sit alongside the scripts they manage and run standalone (cd ~/.dotfiles && make ...). This Makefile keeps the VM-integration targets and the installer-helper suite (safe-rm-rf).
I updated CLAUDE.md and README.md so stow operations run from ~/.dotfiles, and the dotfile-management, theme, and unit-test sections point at the standalone repo. The README was already describing the old in-repo model from before the installer switched to cloning. This brings it in line.
Diffstat (limited to 'tests/tmux-util')
| -rwxr-xr-x | tests/tmux-util/fake-fzf | 30 | ||||
| -rwxr-xr-x | tests/tmux-util/fake-kill | 11 | ||||
| -rwxr-xr-x | tests/tmux-util/fake-sleep | 4 | ||||
| -rwxr-xr-x | tests/tmux-util/fake-tmux | 205 | ||||
| -rw-r--r-- | tests/tmux-util/test_tmux_util.py | 712 |
5 files changed, 0 insertions, 962 deletions
diff --git a/tests/tmux-util/fake-fzf b/tests/tmux-util/fake-fzf deleted file mode 100755 index 476bf7f..0000000 --- a/tests/tmux-util/fake-fzf +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# Fake fzf for testing tmux-util. -# -# Control via env vars: -# FAKE_FZF_CHOICE_LINE — 1-based line index into stdin; that line is -# printed as the selection and exit 0. -# FAKE_FZF_CHOICE — print this literal string as the selection and -# exit 0 (ignored if FAKE_FZF_CHOICE_LINE is set). -# (neither set) — exit 130 (user cancelled). -# -# Every invocation is logged to $FAKE_TMUX_DIR/calls.log as one line. - -: "${FAKE_TMUX_DIR:?FAKE_TMUX_DIR must be set}" -printf 'fzf %s\n' "$*" >> "$FAKE_TMUX_DIR/calls.log" - -if [ -n "${FAKE_FZF_CHOICE_LINE:-}" ]; then - awk -v n="$FAKE_FZF_CHOICE_LINE" 'NR == n { print; exit }' - exit 0 -fi - -if [ -n "${FAKE_FZF_CHOICE:-}" ]; then - # Drain stdin so the pipe upstream doesn't SIGPIPE - cat >/dev/null - printf '%s\n' "$FAKE_FZF_CHOICE" - exit 0 -fi - -# Cancelled -cat >/dev/null -exit 130 diff --git a/tests/tmux-util/fake-kill b/tests/tmux-util/fake-kill deleted file mode 100755 index a157e32..0000000 --- a/tests/tmux-util/fake-kill +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# Fake kill for testing tmux-util. -# -# Records every invocation to $FAKE_TMUX_DIR/kill.log as one line per call. -# Format: kill <args> -# Exits 0 unconditionally — tests assert on the log, not the actual signaling. - -: "${FAKE_TMUX_DIR:?FAKE_TMUX_DIR must be set}" - -printf 'kill %s\n' "$*" >> "$FAKE_TMUX_DIR/kill.log" -exit 0 diff --git a/tests/tmux-util/fake-sleep b/tests/tmux-util/fake-sleep deleted file mode 100755 index 2b8a549..0000000 --- a/tests/tmux-util/fake-sleep +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -# Fake sleep — no-op so tests don't actually wait. Tests assert on behavior, -# not timing. -exit 0 diff --git a/tests/tmux-util/fake-tmux b/tests/tmux-util/fake-tmux deleted file mode 100755 index 1b84956..0000000 --- a/tests/tmux-util/fake-tmux +++ /dev/null @@ -1,205 +0,0 @@ -#!/bin/bash -# Fake tmux for testing tmux-util. -# -# State file: $FAKE_TMUX_DIR/sessions.txt -# One line per session, space-separated: -# <name> <attached> <pids_csv> [<activity_epoch> [<windows> [<cwd>]]] -# pids_csv is a comma-separated list of pane PIDs (or '-' for none) -# cwd cannot contain spaces in test data. -# -# Log file: $FAKE_TMUX_DIR/calls.log -# Each invocation appended as a single line: tmux <args> - -: "${FAKE_TMUX_DIR:?FAKE_TMUX_DIR must be set}" - -STATE="$FAKE_TMUX_DIR/sessions.txt" -LOG="$FAKE_TMUX_DIR/calls.log" - -# Log every invocation -printf 'tmux %s\n' "$*" >> "$LOG" - -cmd="$1" -shift - -read_state() { - [ -f "$STATE" ] || return 0 - grep -v '^[[:space:]]*$' "$STATE" || true -} - -# Render a tmux format string against one session's fields. -# Args: format, name, attached, activity, windows, cwd -render_format() { - local out="$1" name="$2" attached="$3" activity="$4" windows="$5" cwd="$6" - out="${out//\#\{session_name\}/$name}" - out="${out//\#\{session_attached\}/$attached}" - out="${out//\#\{session_activity\}/$activity}" - out="${out//\#\{session_windows\}/$windows}" - out="${out//\#\{pane_current_path\}/$cwd}" - echo "$out" -} - -# Render a tmux format string against one pane's fields. -# Args: format, name, pid, cmd, idx, cwd -render_pane_format() { - local out="$1" name="$2" pid="$3" cmd="$4" idx="$5" cwd="$6" - out="${out//\#\{pane_pid\}/$pid}" - out="${out//\#\{pane_current_command\}/$cmd}" - out="${out//\#\{pane_current_path\}/$cwd}" - out="${out//\#\{session_name\}/$name}" - out="${out//\#\{window_index\}/0}" - out="${out//\#\{pane_index\}/$idx}" - echo "$out" -} - -case "$cmd" in - list-sessions) - fmt="" - while [ "$#" -gt 0 ]; do - case "$1" in - -F) shift; fmt="${1:-}"; shift ;; - *) shift ;; - esac - done - [ -n "$fmt" ] || fmt='#{session_name} #{session_attached}' - read_state | while IFS=' ' read -r name attached pids activity windows cwd; do - [ -n "$name" ] || continue - render_format "$fmt" "$name" "$attached" "${activity:-0}" "${windows:-1}" "${cwd:-/tmp}" - done - ;; - list-panes) - all=0 - fmt="" - session="" - while [ "$#" -gt 0 ]; do - case "$1" in - -t) shift; session="$1"; shift ;; - -F) shift; fmt="${1:-}"; shift ;; - -s) shift ;; - -a) all=1; shift ;; - *) shift ;; - esac - done - [ -n "$fmt" ] || fmt='#{pane_pid}' - read_state | while IFS=' ' read -r name attached pids activity windows cwd; do - [ -n "$name" ] || continue - if [ "$all" -eq 0 ] && [ "$name" != "$session" ]; then - continue - fi - [ "$pids" = "-" ] && continue - idx=0 - for entry in $(echo "$pids" | tr ',' ' '); do - pid="${entry%%:*}" - cmd="${entry##*:}" - [ "$cmd" = "$entry" ] && cmd="shell" - render_pane_format "$fmt" "$name" "$pid" "$cmd" "$idx" "${cwd:-/tmp}" - idx=$((idx + 1)) - done - done - ;; - display) - fmt="" - target="" - while [ "$#" -gt 0 ]; do - case "$1" in - -p) shift ;; - -t) shift; target="$1"; shift ;; - *) fmt="$1"; shift ;; - esac - done - while IFS=' ' read -r name attached pids activity windows cwd; do - [ -n "$name" ] || continue - if [ "$name" = "$target" ]; then - render_format "$fmt" "$name" "$attached" "${activity:-0}" "${windows:-1}" "${cwd:-/tmp}" - exit 0 - fi - done < "$STATE" - exit 1 - ;; - has-session) - session="" - while [ "$#" -gt 0 ]; do - case "$1" in - -t) shift; session="$1"; shift ;; - *) shift ;; - esac - done - # tmux accepts a `=name` form to force exact match; strip the prefix. - session="${session#=}" - while IFS=' ' read -r name attached pids _rest; do - if [ "$name" = "$session" ]; then - exit 0 - fi - done < "$STATE" - exit 1 - ;; - new-session) - detached=0 - name="" - cwd="" - while [ "$#" -gt 0 ]; do - case "$1" in - -s) shift; name="$1"; shift ;; - -c) shift; cwd="$1"; shift ;; - -d) detached=1; shift ;; - *) shift ;; - esac - done - attached=1 - [ "$detached" -eq 1 ] && attached=0 - printf '%s %s - 0 1 %s\n' "$name" "$attached" "${cwd:-/tmp}" >> "$STATE" - ;; - 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 - case "$1" in - -t) shift; session="$1"; shift ;; - *) shift ;; - esac - done - tmp="$STATE.tmp" - : > "$tmp" - while IFS= read -r line; do - [ -n "$line" ] || continue - first="${line%% *}" - if [ "$first" != "$session" ]; then - printf '%s\n' "$line" >> "$tmp" - fi - done < "$STATE" - mv "$tmp" "$STATE" - ;; - *) - echo "fake-tmux: unknown command '$cmd'" >&2 - exit 1 - ;; -esac diff --git a/tests/tmux-util/test_tmux_util.py b/tests/tmux-util/test_tmux_util.py deleted file mode 100644 index b3eab8d..0000000 --- a/tests/tmux-util/test_tmux_util.py +++ /dev/null @@ -1,712 +0,0 @@ -"""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() |
