aboutsummaryrefslogtreecommitdiff
path: root/tests/tmux-util
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-02 12:16:38 -0500
committerCraig Jennings <c@cjennings.net>2026-06-02 12:16:38 -0500
commitb10cba594db836c0747066addad48bda4d30cd02 (patch)
tree063119a623fa3f7139feda4ef302896d8f5f934c /tests/tmux-util
parent49c2ba9c4510bf6e1acd306687473bc8ba9ad8dd (diff)
downloadarchsetup-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-xtests/tmux-util/fake-fzf30
-rwxr-xr-xtests/tmux-util/fake-kill11
-rwxr-xr-xtests/tmux-util/fake-sleep4
-rwxr-xr-xtests/tmux-util/fake-tmux205
-rw-r--r--tests/tmux-util/test_tmux_util.py712
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()