use super::super::architect_pane_probe::{Listed, architect_marks, classify_listing, parse_panes};
use super::super::architect_pane_verbs::DENY_VERBS;
use super::*;
struct Fake {
live: Result<bool, String>,
panes: Result<Vec<Pane>, String>,
current: Option<String>,
}
fn pane(n: u8, session: u8, name: &str, architect: bool) -> Pane {
Pane {
pane: format!("%{n}"),
window: format!("@{n}"),
session: format!("${session}"),
name: name.into(),
architect,
marked: false,
}
}
fn fake() -> Fake {
Fake {
live: Ok(true),
panes: Ok(vec![
pane(1, 1, "tm-architect", true),
pane(2, 2, "pm", false),
pane(3, 3, "tm-architect-poll", false),
]),
current: Some("%2".into()),
}
}
impl PaneProbe for Fake {
fn architect_live(&self) -> Result<bool, String> {
self.live.clone()
}
fn panes(&self, server: &[String]) -> Result<Vec<Pane>, String> {
if server.iter().any(|w| w == "other" || w == "scratch") {
return Ok(vec![pane(9, 9, "tm-architect-test", false)]);
}
if server.iter().any(|w| w == "arch") {
return fake().panes;
}
self.panes.clone()
}
fn current_pane(&self) -> Option<String> {
self.current.clone()
}
}
fn denied(probe: &Fake, command: &str) -> bool {
evaluate_architect_pane(command, probe).is_some_and(|r| r.contains("#8902"))
}
const ARCHITECT_TARGETS: &[&str] = &[
"=tm-architect",
"=tm-architect:0.0",
"tm-architect:0.0",
"tm-arch",
"%1",
"@1",
"'$1'",
"'$1:0'",
"=pm:%1",
];
#[test]
fn every_deny_verb_is_denied_in_each_target_form() {
let probe = fake();
for verb in DENY_VERBS.iter().filter(|v| v.name != "kill-server") {
let extra = if verb.name == "new-window" { " -k" } else { "" };
for target in ARCHITECT_TARGETS {
let command = format!("tmux {}{extra} -t {target}", verb.name);
assert!(denied(&probe, &command), "{command}");
}
if verb.src {
let command = format!("tmux {} -s %1 -t %2", verb.name);
assert!(denied(&probe, &command), "{command}");
}
}
for command in [
"tmux kill-server",
"tmux kill-session -a -t =pm",
"tmux send -t %1 x",
"tmux killp -t %1",
"tmux respawn-p -k -t %1",
"tmux neww -k -t =tm-architect:0",
"tmux send-keys -lt %1 x",
] {
assert!(denied(&probe, command), "{command}");
}
}
#[test]
fn an_unresolvable_target_denies_while_an_architect_is_live() {
let probe = fake();
for command in [
"tmux send-keys -t \"$T\" x",
"tmux send-keys -t $T x",
"tmux kill-pane -t \"$(tmux display -p -t =pm: '#{pane_id}')\"",
"tmux kill-pane -t `cat pane.txt`",
"tmux kill-pane -t '{marked}'",
"tmux kill-pane -t '~'",
"tmux kill-pane -t '!'",
"tmux kill-pane -t '+1'",
"tmux kill-session -t 'tm-*'",
"tmux kill-pane -t '#{pane_id}'",
"tmux kill-pane -t '%1x'",
"tmux send-keys -Q -t =pm x",
"tmux send-keys -t",
"tmux send-keys -c /dev/ttys001 -t =pm x",
"tmux zap -t =pm",
"tmux source-file ~/.tmux.conf",
"tmux -C attach",
"tmux $VERB -t =pm",
"tmux send-keys -t 'x",
"TMUX_PANE=%1 tmux send-keys x",
"tmux has-session -t =pm ';' zap",
r"$'\x74mux' kill-session -t =tm-architect",
] {
assert!(denied(&probe, command), "{command}");
}
let lost = Fake {
current: None,
..fake()
};
assert!(denied(&lost, "tmux send-keys x Enter"));
let elsewhere = Fake {
current: Some("%9".into()),
..fake()
};
assert!(denied(&elsewhere, "tmux send-keys x Enter"));
let broken = Fake {
panes: Err("server exited unexpectedly".into()),
..fake()
};
assert!(denied(&broken, "tmux send-keys -t =pm x"));
}
#[test]
fn no_live_architect_means_the_rule_does_not_apply() {
let none = Fake {
live: Ok(false),
..fake()
};
assert_eq!(
evaluate_architect_pane("tmux kill-session -t =tm-architect", &none),
None
);
let unreadable = Fake {
live: Err("records unreadable".into()),
..fake()
};
assert!(denied(&unreadable, "tmux kill-session -t =tm-architect"));
}
#[test]
fn a_non_architect_target_and_a_read_verb_pass() {
let probe = fake();
for command in [
"tmux send-keys -t =pm:0 'Run the gates, then git push your branch' Enter",
"tmux send-keys -t %2 'y' Enter",
"tmux send-keys -t @2 n Enter",
"tmux send-keys -t '$2' j",
"tmux send-keys 'hello' Enter",
"tmux kill-session -t =tm-architect-poll",
"tmux capture-pane -p -t %2",
"tmux capture-pane -p -t =tm-architect: -S -200",
"tmux resize-pane -Z -t =tm-architect",
"tmux select-layout -t %1 tiled",
"tmux new-window -t =tm-architect:",
"tmux -L other kill-server",
"tmux has-session -t =tm-architect",
"tmux n",
"echo tmux kill-session",
"tmux send-keys -t =pm x \\; send-keys -t =pm Enter",
] {
assert_eq!(evaluate_architect_pane(command, &probe), None, "{command}");
}
}
#[test]
fn a_tmux_command_reached_through_a_wrapper_or_argument_is_found() {
let probe = fake();
for command in [
"sh -c 'tmux kill-pane -t %1'",
"echo $(tmux kill-pane -t %1)",
"timeout 5 tmux killp -t %1",
"/opt/homebrew/bin/tmux send -t %1 x",
"tmux -c 'tmux kill-pane -t %1'",
"tmux run-shell 'tmux kill-pane -t %1'",
"tmux if-shell true 'kill-pane -t %1'",
"tmux bind-key X kill-pane -t %1",
"tmux new-window 'tmux kill-pane -t %1'",
"tmux new-window \\; kill-pane -t %1",
"tmux set -s command-alias[9] zap=kill-server",
"tmux send-keys -t %2 'tmux kill-session -t =tm-architect' Enter",
"setsid tmux killp -t %1",
"noglob tmux killp -t %1",
"nocorrect tmux killp -t %1",
"chrt 10 tmux killp -t %1",
"taskset 1 tmux killp -t %1",
"flock /tmp/l tmux killp -t %1",
"unbuffer tmux killp -t %1",
"gtimeout 5 tmux killp -t %1",
"timeout 60 bash -c 'tmux kill-pane -t %1'",
"nice -n 5 sh -c 'tmux kill-pane -t %1'",
"env tmux killp -t %1",
"command tmux killp -t %1",
"nice tmux killp -t %1",
"nice -n 5 tmux killp -t %1",
"sudo -u x tmux killp -t %1",
] {
assert!(denied(&probe, command), "{command}");
}
}
#[test]
fn a_server_the_command_selects_differently_denies() {
let probe = fake();
for command in [
"tmux -S default kill-session -t =pm",
"tmux -Sdefault kill-session -t =pm",
"cd /private/tmp/tmux-501 && tmux -S default kill-session -t =tm-architect",
"TMUX= tmux kill-session -t =pm",
"TMUX=/tmp/tmux-501/default,1,0 tmux kill-session -t =pm",
"env -u TMUX tmux kill-session -t =pm",
"env -uTMUX tmux kill-session -t =pm",
"env --unset=TMUX tmux kill-session -t =pm",
"unset TMUX; tmux kill-session -t =pm",
"export TMUX_TMPDIR=/tmp/elsewhere; tmux kill-session -t =pm",
"TMUX_TMPDIR=/tmp/elsewhere tmux kill-session -t =pm",
"env -i tmux kill-session -t =pm",
"env - tmux kill-session -t =pm",
"sudo tmux kill-session -t =pm",
"doas tmux kill-session -t =pm",
"sh -c 'unset TMUX; tmux kill-session -t =pm'",
"exec -c tmux kill-session -t =pm:",
": \"${TMUX:=/tmp/x/default,1,0}\"; tmux kill-session -t =pm:",
": ${TMUX=/tmp/x/default,1,0}; tmux kill-session -t =pm:",
] {
assert!(denied(&probe, command), "{command}");
}
for command in [
"tmux -S /tmp/tmux-501/default capture-pane -p -t =tm-architect:",
"echo \"$TMUX\"; tmux send-keys -t =pm: x",
"TMUX= tmux ls",
"sudo tmux ls",
] {
assert_eq!(evaluate_architect_pane(command, &probe), None, "{command}");
}
}
#[test]
fn a_target_the_same_command_retargets_denies() {
let probe = fake();
for command in [
"tmux rename-session -t =tm-architect x \\; send-keys -t =x hi Enter",
"tmux new-session -d -s g -t =tm-architect \\; send-keys -t =g hi Enter",
"tmux set-hook -t =tm-architect after-resize-pane 'send-keys hi Enter' \\; \
resize-pane -t =tm-architect -x 80",
"tmux rename -t =tm-architect x; tmux send-keys -t =x hi Enter",
"tmux new -d -s g -t =tm-architect && tmux kill-session -t =g",
"tmux bind-key X kill-pane",
"tmux run-shell 'tmux send-keys hi Enter'",
] {
assert!(denied(&probe, command), "{command}");
}
for command in [
"tmux new-session -d -s work",
"tmux rename-session -t =pm pm2",
"tmux send-keys -t =pm 'please send the report' Enter",
] {
assert_eq!(evaluate_architect_pane(command, &probe), None, "{command}");
}
}
#[test]
fn a_nested_invocation_inherits_the_outer_server() {
let probe = Fake {
panes: Ok(vec![pane(2, 2, "pm", false), pane(3, 3, "w", false)]),
..fake()
};
for command in [
"tmux -L arch run-shell 'tmux kill-pane -t %1'",
"tmux -L arch send-keys -t =pm 'tmux kill-session -t =tm-architect' Enter",
] {
assert!(denied(&probe, command), "{command}");
}
for command in [
"tmux run-shell 'tmux kill-pane -t %1'",
"tmux -L arch run-shell 'tmux -L other kill-server'",
"tmux -L arch -c 'tmux kill-pane -t %1'",
] {
assert_eq!(evaluate_architect_pane(command, &probe), None, "{command}");
}
}
#[test]
fn a_nested_tmux_in_typed_keys_is_judged_on_the_default_server_too() {
let probe = fake();
let deny = [
"tmux -L other send-keys -t %9 'tmux kill-session -t =tm-architect' Enter",
"tmux -L other send-keys -t %9 'tmux kill-server' Enter",
"tmux -L other send-keys -t %9 'tmux kill-pane' Enter",
"tmux send-keys -t =pm 'tmux -L arch kill-pane' Enter",
"tmux -L arch send-keys -t =pm 'tmux kill-pane' Enter",
];
let pass = [
"tmux -L other send-keys -t %9 'tmux kill-session -t =pm' Enter",
"tmux -L other send-keys -t %9 'tmux -L other kill-server' Enter",
"tmux -L other run-shell 'tmux kill-session -t =tm-architect'",
"tmux send-keys -t =pm 'tmux kill-pane' Enter",
"tmux send-keys -t =pm 'tmux -L other kill-pane' Enter",
];
assert_eq!(misjudged(&probe, &deny, &pass), Vec::<String>::new());
let reason = evaluate_architect_pane(deny[3], &probe);
assert!(
reason
.as_deref()
.is_some_and(|r| r.contains("another server")),
"{reason:?}"
);
}
#[test]
fn an_unreadable_expansion_or_dynamic_name_counts_as_a_server_move() {
use super::super::architect_pane_env::{assigns_dynamic_name, moves_server_env};
assert!(moves_server_env(": ${n"));
assert!(moves_server_env(": ${(P)n::=x}"));
assert!(!moves_server_env(": ${n} ${(P)n} ${!n}"));
let argv = |s: &str| -> Vec<String> { s.split(' ').map(str::to_owned).collect() };
assert!(assigns_dynamic_name(&argv("export ${n}UX=x")));
assert!(assigns_dynamic_name(&argv("eval $cmd")));
assert!(!assigns_dynamic_name(&argv("export PATH=$PATH:/x")));
assert!(!assigns_dynamic_name(&argv("printf %s $x")));
assert!(!assigns_dynamic_name(&[]));
}
fn misjudged(probe: &Fake, deny: &[&str], pass: &[&str]) -> Vec<String> {
let wrong_deny = deny.iter().filter(|c| !denied(probe, c));
let wrong_pass = pass
.iter()
.filter(|c| evaluate_architect_pane(c, probe).is_some());
wrong_deny
.map(|c| format!("allowed: {c}"))
.chain(wrong_pass.map(|c| format!("denied: {c}")))
.collect()
}
#[test]
fn a_tmux_assignment_inside_a_word_or_through_an_unread_name_denies() {
let deny = [
": ${TMUX::=/tmp/x/default,1,0}; tmux kill-session -t =pm:",
": \"${TMUX_TMPDIR::=/tmp/x}\"; tmux kill-session -t =pm:",
"export TM''UX=/tmp/x/default,1,0; tmux kill-session -t =pm:",
"export \"TM\"UX=/tmp/x/default,1,0; tmux kill-session -t =pm:",
"n=TM; n+=UX; : ${(P)n::=/tmp/x/d,1,0}; tmux kill-session -t =pm:",
"n=TM; : ${(P)${n}UX:=/tmp/x/d,1,0}; tmux kill-session -t =pm:",
"n=TM; n+=UX; : ${!n:=/tmp/x/d,1,0}; tmux kill-session -t =pm:",
"n=TM; export \"${n}UX=/tmp/x/d,1,0\"; tmux kill-session -t =pm:",
"n=TM; typeset $n'UX'=/tmp/x/d,1,0; tmux kill-session -t =pm:",
"n=TM; unset \"${n}UX\"; tmux kill-session -t =pm:",
"n=TM; printf -v \"${n}UX\" x; tmux kill-session -t =pm:",
];
let pass = [
"echo \"${TMUX:-none}\"; tmux send-keys -t =pm: x",
": ${(P)n}; tmux send-keys -t =pm: x",
": ${HOME::=/tmp}; tmux send-keys -t =pm: x",
"export PATH=\"$PATH:/x\"; tmux send-keys -t =pm: x",
"printf '%s' \"$MSG\"; tmux send-keys -t =pm: x",
];
assert_eq!(misjudged(&fake(), &deny, &pass), Vec::<String>::new());
}
#[test]
fn a_retarget_denies_only_on_a_server_holding_the_architect() {
let probe = fake();
let scratch = "tmux -L scratch new -d -s w \\; send-keys -t =w x";
assert_eq!(evaluate_architect_pane(scratch, &probe), None);
for command in [
"tmux -L arch new -d -s w \\; send-keys -t =w x",
"tmux new -d -s w \\; send-keys -t =w x",
] {
let reason = evaluate_architect_pane(command, &probe);
assert!(
reason
.as_deref()
.is_some_and(|r| r.contains("renames or creates")),
"{command}: {reason:?}"
);
}
}
#[test]
fn a_marked_or_linked_architect_pane_is_reached() {
let mut marked = fake();
if let Ok(panes) = marked.panes.as_mut() {
panes[0].marked = true;
}
assert!(denied(&marked, "tmux swap-pane -t %2"));
assert_eq!(
evaluate_architect_pane("tmux swap-pane -t %2", &fake()),
None
);
let mut linked = fake();
if let Ok(panes) = linked.panes.as_mut() {
panes.push(pane(1, 2, "pm", true));
}
assert!(denied(&linked, "tmux send-keys -t =pm:9 x"));
}
#[test]
fn a_pane_listing_marks_the_architect_by_lineage_and_session() {
let text =
"%1\t@1\t$1\t100\t0\ttm-architect\n%2\t@2\t$2\t200\t1\tpm\n%3\t@3\t$3\t300\t0\tw x\n";
let panes = parse_panes(text, &[300], &[]).expect("parse");
let marks: Vec<(bool, bool)> = panes.iter().map(|p| (p.architect, p.marked)).collect();
assert_eq!(marks, [(true, false), (false, true), (true, false)]);
assert_eq!(panes[2].name, "w x");
let by_name = parse_panes(text, &[], &["pm".into()]).expect("parse");
let marks: Vec<bool> = by_name.iter().map(|p| p.architect).collect();
assert_eq!(marks, [true, true, false]);
assert!(parse_panes("%1\t@1\n", &[], &[]).is_err());
}
#[test]
fn a_pane_listing_run_is_classified() {
type Lineage = Result<Vec<u32>, String>;
type Sidecars = Result<Vec<String>, String>;
type Marks = Result<Vec<bool>, String>;
let text = "%1\t@1\t$1\t100\t0\ttm-architect\n%2\t@2\t$2\t300\t0\tpm\n";
let custom = "%1\t@1\t$1\t100\t0\ttm-supervisor\n%2\t@2\t$2\t300\t0\tpm\n";
let marks = |r: Result<Vec<Pane>, String>| -> Marks {
r.map(|panes| panes.iter().map(|p| p.architect).collect())
};
let ran = |ok, stdout, stderr| Listed::Ran { ok, stdout, stderr };
let unreadable: Lineage = Err("records unreadable".into());
let none: Sidecars = Ok(vec![]);
let supervisor: Sidecars = Ok(vec!["tm-supervisor".into()]);
let stale_pm: Sidecars = Ok(vec!["pm".into()]);
let no_sidecars: Sidecars = Err("sidecars unreadable".into());
let neither = "the Architect launch records do not read (records unreadable; \
sidecars unreadable)";
let cases: [(Listed<'_>, &Lineage, &Sidecars, Marks); 11] = [
(Listed::NotFound, &Ok(vec![]), &none, Ok(vec![])),
(
Listed::Failed("spawn".into()),
&Ok(vec![]),
&none,
Err("spawn".into()),
),
(
ran(false, "", "no server running on /tmp/tmux-501/default\n"),
&Ok(vec![]),
&none,
Ok(vec![]),
),
(
ran(
false,
"",
"error connecting to /tmp/tmux-501/x (No such file)",
),
&Ok(vec![]),
&none,
Ok(vec![]),
),
(
ran(false, "", "server exited unexpectedly\n"),
&Ok(vec![]),
&none,
Err("server exited unexpectedly".into()),
),
(
ran(true, text, ""),
&Ok(vec![300]),
&none,
Ok(vec![true, true]),
),
(
ran(true, text, ""),
&unreadable,
&none,
Ok(vec![true, false]),
),
(
ran(true, text, ""),
&Ok(vec![]),
&stale_pm,
Ok(vec![true, false]),
),
(
ran(true, custom, ""),
&unreadable,
&no_sidecars,
Err(neither.into()),
),
(
ran(true, custom, ""),
&unreadable,
&supervisor,
Ok(vec![true, false]),
),
(
ran(true, text, ""),
&unreadable,
&no_sidecars,
Err(neither.into()),
),
];
for (row, (listed, lineage, sidecars, want)) in cases.into_iter().enumerate() {
let found = architect_marks(lineage, || sidecars.clone());
assert_eq!(marks(classify_listing(listed, &found)), want, "row {row}");
}
}
#[test]
fn a_deeply_nested_expansion_is_read_in_linear_time() {
let depth = 50_000;
let nested = format!("{}T{}", "${a".repeat(depth), "}".repeat(depth));
let closed = format!("echo {nested}; tmux kill-session -t =pm:");
let unclosed = format!("echo {}; tmux kill-session -t =pm:", "${a".repeat(depth));
let started = std::time::Instant::now();
assert_eq!(
misjudged(&fake(), &[&unclosed], &[&closed]),
Vec::<String>::new()
);
let elapsed = started.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(3),
"took {elapsed:?}"
);
}
#[test]
fn an_ansi_c_quote_in_a_command_without_tmux_is_not_a_tmux_command_9001() {
for command in [r"grep -c $'\x1b' /tmp/x.log", r"printf $'a\tb' | wc -c"] {
assert_eq!(evaluate_architect_pane(command, &fake()), None, "{command}");
}
}
#[test]
fn an_ansi_c_quote_that_hides_tmux_or_will_not_decode_still_denies_9001() {
for command in [
r"$'\x74mux' kill-session -t =tm-architect",
r"$'\164mux' kill-server",
r"t$'mux' send-keys -t %1 x Enter",
r"grep -c $'\u001b' log",
r#"echo $"x" ; tmux ls"#,
] {
assert!(denied(&fake(), command), "{command}");
}
}
#[test]
fn an_undecodable_quote_refusal_names_the_token_9001() {
let reason = evaluate_architect_pane(r"grep -c $'\u001b' log", &fake()).expect("denied");
assert!(reason.contains(r"`$'\u001b'`"), "{reason}");
assert!(reason.contains("ordinary `'…'`"), "{reason}");
}