Initial pre-release

tmux-party: share a tmux session with people on the same UNIX host.
Single-file POSIX shell (party) with a filesystem + tmux server-access
trust model. See README.md.
This commit is contained in:
veg 2026-06-01 10:45:32 +00:00
commit 6be0ac1877
20 changed files with 3869 additions and 0 deletions

136
tests/helpers.bash Normal file
View file

@ -0,0 +1,136 @@
# Bats helpers shared by every test file.
# `load 'helpers'` from a .bats file pulls these in.
# helpers.bash lives at <repo>/tests/helpers.bash, so repo root is one level
# up from its own directory. This avoids depending on .git, which may not
# be present on remote test hosts (e.g. rsynced without history).
PARTY_BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/party"
# Per-test scratch dir. Bats sets BATS_TEST_TMPDIR but we centralize so
# party's overridable env vars all point under one tree.
setup_party_sandbox() {
# Pretend the test runner is NOT inside tmux. iTerm2's tmux -CC mode
# (or any nested tmux) sets $TMUX, which several party subcommands
# treat as "you're already attached somewhere." Tests that need
# $TMUX set re-export it themselves.
unset TMUX TMUX_PANE TMUX_TMPDIR
# USER may not be exported in all environments (e.g. some CI shells,
# zsh with clean env). party uses $USER under set -eu, so guarantee it.
export USER="${USER:-$(id -un)}"
export PARTY_TMP="$BATS_TEST_TMPDIR/party"
export PARTY_SOCKET_DIR="$PARTY_TMP/sockets"
mkdir -p "$PARTY_SOCKET_DIR"
chmod 0700 "$PARTY_SOCKET_DIR"
# No shared roster directory in this layout. Each per-party dir under
# PARTY_SOCKET_DIR carries its own roster file. validate_socket_dir_parent
# accepts an owner-only-writable parent like this one.
}
# Tear down any tmux servers we spawned under PARTY_SOCKET_DIR.
# Sockets live at ${PARTY_SOCKET_DIR}/party-USER:NAME.d/sock under the
# per-party-private-dir layout. The old glob (party-*) matched the
# *directory* and the [ -S ] filter dropped everything, so servers
# leaked across tests.
teardown_party_sandbox() {
if [ -d "${PARTY_SOCKET_DIR:-}" ]; then
for d in "$PARTY_SOCKET_DIR"/party-*.d; do
[ -d "$d" ] || continue
sock="$d/sock"
[ -S "$sock" ] || continue
tmux -S "$sock" kill-server 2>/dev/null || true
done
fi
}
# Pre-create the per-party private dir so roster_write can land its
# record inside. Mirrors cmd_host's mkdir -m 0700 + chmod 0750. Also
# creates a placeholder sock file so roster_read (which requires
# $d/sock to exist and be owned by the basename's HOST_USER) accepts
# the dir.
ensure_party_dir() {
local d="$PARTY_SOCKET_DIR/party-$1:$2.d"
mkdir -p "$d"
chmod 0750 "$d"
: > "$d/sock"
}
# Skip the test if the running user is not a member of TMUX_PARTY_GROUP.
require_party_group() {
local g="${TMUX_PARTY_GROUP:-party}"
id -nG "$USER" 2>/dev/null | tr ' ' '\n' | grep -qx "$g" \
|| skip "user not in group '$g'; run: sudo usermod -aG $g $USER && relog"
}
# Print one username per line for users in group $1 — both supplementary
# members (4th /etc/group field) and primary-group members (passwd gid
# match). Linux / illumos / FreeBSD / OpenBSD use getent(1); macOS has
# no getent and stores users in Directory Services, so fall back to
# dscl(1) there. Returns silently if neither resolver is available or
# the group is unknown.
list_users_in_group() {
local g="$1" gid
if command -v getent >/dev/null 2>&1; then
gid=$(getent group "$g" 2>/dev/null | awk -F: '{print $3}')
[ -n "$gid" ] || return 0
{
getent group "$g" | awk -F: '{n=split($4,m,","); for(i=1;i<=n;i++) if(m[i]!="") print m[i]}'
getent passwd | awk -F: -v gid="$gid" '$4==gid {print $1}'
}
elif command -v dscl >/dev/null 2>&1; then
gid=$(dscl . -read "/Groups/$g" PrimaryGroupID 2>/dev/null \
| awk '/^PrimaryGroupID:/ {print $2}')
[ -n "$gid" ] || return 0
{
# Supplementary members. macOS records gid 20 (staff) members as
# primary-only, so this branch is usually empty for staff but
# populates for groups created with `dseditgroup -o edit -a`.
dscl . -read "/Groups/$g" GroupMembership 2>/dev/null \
| sed -n 's/^GroupMembership: //p' | tr ' ' '\n'
# Primary members: every user whose PrimaryGroupID matches.
dscl . -list /Users PrimaryGroupID | awk -v gid="$gid" '$2==gid {print $1}'
} | grep -v '^$' || true
fi
}
# Print the login shell of user $1, or nothing if unknown. Mirrors
# list_users_in_group's portability split: getent(1) where present,
# dscl(1) on macOS.
lookup_user_shell() {
local u="$1"
if command -v getent >/dev/null 2>&1; then
getent passwd "$u" 2>/dev/null | awk -F: '{print $7}'
elif command -v dscl >/dev/null 2>&1; then
dscl . -read "/Users/$u" UserShell 2>/dev/null | sed -n 's/^UserShell: //p'
fi
}
# Pick another invitable user on this box: a member of TMUX_PARTY_GROUP
# (supplementary or primary) other than $USER. Empty output if none.
# Uses group membership rather than a UID floor so it works across
# Linux (uid>=1000), illumos/Solaris (uid>=100), and BSD conventions.
# Excludes root and no-login system accounts (tmux server-access refuses
# to manage root, and system pseudo-users aren't sensible test peers).
pick_other_user() {
local g="${TMUX_PARTY_GROUP:-party}"
local candidates u shell
candidates=$(list_users_in_group "$g")
for u in $candidates; do
[ "$u" = "$USER" ] && continue
[ "$u" = "root" ] && continue
shell=$(lookup_user_shell "$u")
case "$shell" in
*false|*nologin|"") continue ;;
esac
printf '%s\n' "$u"
return 0
done
}
# Source ./party in library mode for pure-function tests.
load_party_lib() {
__PARTY_LIB_ONLY=1
# shellcheck disable=SC1090
. "$PARTY_BIN"
}