1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Pure-logic tests for `gwm::multiplexer`. The module's command builders
//! and env-var probes are deliberately decoupled from any process spawn, so
//! these tests do not require `tmux` or `zellij` to be installed on the
//! runner — they assert against the produced argv vectors.
use gwm::multiplexer::{build_tmux_command, build_zellij_command, detect_tmux, detect_zellij, Multiplexer, SpawnMode};
use std::path::Path;
// --------------------------------------------------------------------------
// tmux command builder
// --------------------------------------------------------------------------
#[test]
fn tmux_new_window_uses_new_window_subverb() {
// `tmux new-window -n <name> -c <path>` is the canonical incantation for
// "open this directory in a new window of the current session". The `-n`
// labels the window (so it's discoverable in tmux's status bar) and
// `-c` sets the new window's cwd, which is what the user expects when
// running `gwm tmux <pattern>`.
let argv = build_tmux_command("feat-99-auth", Path::new("/tmp/wt/feat-99-auth"), SpawnMode::Window);
assert_eq!(argv[0], "tmux");
assert_eq!(argv[1], "new-window");
// `-n <name>` and `-c <path>` are both required.
let has_n = argv.windows(2).any(|w| w[0] == "-n" && w[1] == "feat-99-auth");
let has_c = argv.windows(2).any(|w| w[0] == "-c" && w[1] == "/tmp/wt/feat-99-auth");
assert!(has_n, "expected `-n feat-99-auth` in argv, got: {:?}", argv);
assert!(has_c, "expected `-c /tmp/wt/feat-99-auth` in argv, got: {:?}", argv);
}
#[test]
fn tmux_split_pane_uses_split_window_subverb() {
// With `-p` (SpawnMode::Split) we want a horizontal/vertical split in the
// current window, not a new window. `tmux split-window -c <path>` is the
// shape — no `-n` because tmux panes don't carry a name attribute.
let argv = build_tmux_command("feat-12-x", Path::new("/tmp/wt/feat-12-x"), SpawnMode::Split);
assert_eq!(argv[0], "tmux");
assert_eq!(argv[1], "split-window");
let has_c = argv.windows(2).any(|w| w[0] == "-c" && w[1] == "/tmp/wt/feat-12-x");
assert!(
has_c,
"split-window must also set `-c` so the new pane lands in the worktree, got: {:?}",
argv
);
// No `-n`: tmux split-window doesn't accept `-n`. A buggy build that
// forwarded the window name here would error out at spawn time on
// every invocation.
assert!(
!argv.iter().any(|a| a == "-n"),
"split-window must NOT carry `-n` (tmux rejects it), got: {:?}",
argv
);
}
// --------------------------------------------------------------------------
// zellij command builder
// --------------------------------------------------------------------------
#[test]
fn zellij_new_tab_uses_action_new_tab() {
// Zellij is driven by `zellij action <verb>`. The new-tab verb supports
// both `--name` and `--cwd` since 0.40 — the latter is what makes the
// tab open inside the worktree.
let argv = build_zellij_command("feat-7-foo", Path::new("/tmp/wt/feat-7-foo"), SpawnMode::Window);
assert_eq!(argv[0], "zellij");
assert_eq!(argv[1], "action");
assert_eq!(argv[2], "new-tab");
let has_name = argv.windows(2).any(|w| w[0] == "--name" && w[1] == "feat-7-foo");
let has_cwd = argv.windows(2).any(|w| w[0] == "--cwd" && w[1] == "/tmp/wt/feat-7-foo");
assert!(has_name, "expected `--name feat-7-foo` in argv, got: {:?}", argv);
assert!(has_cwd, "expected `--cwd /tmp/wt/feat-7-foo` in argv, got: {:?}", argv);
}
#[test]
fn zellij_split_pane_uses_action_new_pane() {
// `-p` → split the current tab. `zellij action new-pane --cwd <path>` is
// the shape; no `--name` because zellij panes aren't named at creation.
let argv = build_zellij_command("feat-7-foo", Path::new("/tmp/wt/feat-7-foo"), SpawnMode::Split);
assert_eq!(argv[0], "zellij");
assert_eq!(argv[1], "action");
assert_eq!(argv[2], "new-pane");
let has_cwd = argv.windows(2).any(|w| w[0] == "--cwd" && w[1] == "/tmp/wt/feat-7-foo");
assert!(has_cwd, "new-pane must set `--cwd`, got: {:?}", argv);
assert!(
!argv.iter().any(|a| a == "--name"),
"new-pane must NOT carry `--name` (zellij rejects it on panes), got: {:?}",
argv
);
}
// --------------------------------------------------------------------------
// multiplexer detection
// --------------------------------------------------------------------------
#[test]
fn detect_tmux_true_when_tmux_env_set() {
// Inside a tmux session, `$TMUX` is set to the socket path. Any non-empty
// value counts — gwm should not parse the socket; only the presence of
// the variable matters for the gate.
assert!(detect_tmux(Some("/private/tmp/tmux-501/default,12345,0".to_string())));
assert!(detect_tmux(Some("any-nonempty-string".to_string())));
}
#[test]
fn detect_tmux_false_when_tmux_env_missing_or_empty() {
// No env var → user is not in tmux. `gwm tmux` should refuse with a
// clear error in this case, never silently spawn a server-less tmux
// command.
assert!(!detect_tmux(None));
// Empty string is treated as "not set" — matches what shells emit for
// `unset TMUX; echo "${TMUX-}"`.
assert!(!detect_tmux(Some(String::new())));
}
#[test]
fn detect_zellij_true_when_zellij_env_set() {
// Inside a zellij session, `$ZELLIJ` is set to "0" (or the session
// socket id depending on the version). Presence is the gate, value is
// not parsed.
assert!(detect_zellij(Some("0".to_string())));
assert!(detect_zellij(Some("any-nonempty-string".to_string())));
}
#[test]
fn detect_zellij_false_when_zellij_env_missing_or_empty() {
assert!(!detect_zellij(None));
assert!(!detect_zellij(Some(String::new())));
}
// --------------------------------------------------------------------------
// Multiplexer enum: name + binary
// --------------------------------------------------------------------------
#[test]
fn multiplexer_binary_matches_verb() {
// The `Multiplexer::binary()` helper exists so the spawn site doesn't
// duplicate the string literal — and so the not-running error message
// can name the right multiplexer in one line.
assert_eq!(Multiplexer::Tmux.binary(), "tmux");
assert_eq!(Multiplexer::Zellij.binary(), "zellij");
}