use super::*;
const FAKE_TOKEN: &str = "sk-ant-oat01-super-secret-value";
#[test]
fn redact_oauth_token_masks_quoted_value() {
let input = format!(
"export TM_MANAGED_SESSION_ID='id'; env -u ANTHROPIC_API_KEY \
CLAUDE_CODE_OAUTH_TOKEN='{FAKE_TOKEN}' /usr/bin/claude --resume x"
);
let out = redact_oauth_token(&input);
assert!(
!out.contains(FAKE_TOKEN),
"token value must not survive redaction: {out}"
);
assert!(
out.contains("CLAUDE_CODE_OAUTH_TOKEN='<redacted>'"),
"redacted marker must replace the value: {out}"
);
assert!(out.contains("env -u ANTHROPIC_API_KEY"));
assert!(out.contains("/usr/bin/claude --resume x"));
}
#[test]
fn redact_oauth_token_masks_unquoted_value() {
let input = format!("prefix CLAUDE_CODE_OAUTH_TOKEN={FAKE_TOKEN} suffix");
let out = redact_oauth_token(&input);
assert!(
!out.contains(FAKE_TOKEN),
"unquoted value must be masked: {out}"
);
assert_eq!(out, "prefix CLAUDE_CODE_OAUTH_TOKEN=<redacted> suffix");
}
#[test]
fn redact_oauth_token_leaves_unrelated_text_untouched() {
let input = "tmux [\"send-keys\", \"-t\", \"tmpm-x\", \"-l\", \"echo hi\"] failed: bad";
assert_eq!(redact_oauth_token(input), input);
}
#[test]
fn run_style_error_message_does_not_leak_oauth_token() {
let keys = format!(
"export TM_MANAGED_SESSION_ID='abc'; env -u ANTHROPIC_API_KEY \
CLAUDE_CODE_OAUTH_TOKEN='{FAKE_TOKEN}' claude --dangerously-skip-permissions"
);
let argv = tmux_argv(&TmuxCommand::SendKeys {
target: TmuxTarget::session("tmpm-test"),
keys,
literal: true,
});
let stderr = "can't find pane";
let raw = format!("tmux {argv:?} failed: {stderr}");
assert!(
raw.contains(FAKE_TOKEN),
"precondition: raw message leaks the token"
);
let redacted = redact_oauth_token(&raw);
assert!(
!redacted.contains(FAKE_TOKEN),
"the error string for a failed spawn must not contain the token: {redacted}"
);
assert!(
redacted.contains("CLAUDE_CODE_OAUTH_TOKEN='<redacted>'"),
"the token env var must be present but redacted: {redacted}"
);
}
#[test]
fn find_quoted_value_end_stops_at_bare_closer() {
assert_eq!(find_quoted_value_end("abcdef' rest"), 7);
}
#[test]
fn find_quoted_value_end_skips_escaped_embedded_quote() {
let after_open = "abc'\\''def' trailing";
let end = find_quoted_value_end(after_open);
assert_eq!(
&after_open[..end],
"abc'\\''def'",
"must consume the full escaped value through the true closer"
);
assert_eq!(&after_open[end..], " trailing");
}
#[test]
fn redact_oauth_token_masks_value_with_embedded_single_quote() {
let secret_with_quote = "abc'def";
let input = "CLAUDE_CODE_OAUTH_TOKEN='abc'\\''def' claude --resume x";
let redacted = redact_oauth_token(input);
assert!(
!redacted.contains("abc"),
"no fragment of the secret must survive: {redacted}"
);
assert!(
!redacted.contains("def"),
"the tail after the embedded quote must not leak: {redacted}"
);
assert!(
!redacted.contains(secret_with_quote),
"the full secret must not survive redaction: {redacted}"
);
assert_eq!(
redacted, "CLAUDE_CODE_OAUTH_TOKEN='<redacted>' claude --resume x",
"the entire escaped quoted value must collapse to one redacted marker: {redacted}"
);
}
#[test]
fn parses_session_row() {
let info = SessionInfo::parse("trusty-mpm-abc:1700000000:1").unwrap();
assert_eq!(info.name, "trusty-mpm-abc");
assert_eq!(info.created, 1_700_000_000);
assert!(info.attached);
let detached = SessionInfo::parse("s:1:0").unwrap();
assert!(!detached.attached);
}
#[test]
fn parses_multi_client_session_row_as_attached() {
for clients in ["1", "2", "3", "17"] {
let info = SessionInfo::parse(&format!("tm-dogfood-relaunch-01:1785443731:{clients}"))
.unwrap_or_else(|e| panic!("row with {clients} client(s) must parse: {e}"));
assert!(
info.attached,
"session_attached={clients} is a client COUNT — any non-zero value \
means a client is connected, so the row must read attached"
);
}
assert!(
!SessionInfo::parse("tm-detached-01:1785443731:0")
.unwrap()
.attached,
"zero clients is the only value that means detached"
);
assert!(
!SessionInfo::parse("tm-garbage-01:1785443731:banana")
.unwrap()
.attached,
"an unparseable count degrades to detached, never to attached"
);
}
#[test]
fn attachment_flips_to_detached_when_client_count_drops_to_zero() {
let name = "tm-dogfood-relaunch-01";
let while_attached = SessionInfo::parse(&format!("{name}:1785443731:2")).unwrap();
let after_detach = SessionInfo::parse(&format!("{name}:1785443731:0")).unwrap();
assert_eq!(while_attached.name, after_detach.name);
assert!(while_attached.attached);
assert!(
!after_detach.attached,
"once the last client detaches the very next listing must report detached"
);
}
#[test]
fn parses_managed_pane_row() {
let row = TmuxDriver::parse_managed_pane_row("tmpm-brave-otter\tclaude\t12345\t%7\t/work/repo")
.unwrap();
assert_eq!(row.session_name, "tmpm-brave-otter");
assert_eq!(row.pane_current_command, "claude");
assert_eq!(row.pane_pid, Some(12345));
assert_eq!(row.pane_id.as_deref(), Some("%7"));
assert_eq!(row.pane_current_path.as_deref(), Some("/work/repo"));
assert!(TmuxDriver::parse_managed_pane_row("tmpm-x\tzsh\t\t%9\t/w").is_none());
assert!(TmuxDriver::parse_managed_pane_row("tmpm-y\tclaude\t222").is_none());
assert!(TmuxDriver::parse_managed_pane_row("tmpm-z\tzsh\t1\t%1\t/w\textra").is_none());
assert!(TmuxDriver::parse_managed_pane_row("\tzsh\t1\t%1\t/w").is_none());
assert!(TmuxDriver::parse_managed_pane_row("tmpm-q\t\t1\t%1\t/w").is_none());
}
#[test]
fn parses_pane_row_with_empty_current_path() {
let row = TmuxDriver::parse_managed_pane_row("tm-a\tclaude\t12\t%1\t").expect("row parses");
assert_eq!(row.pane_current_path, None);
let row = TmuxDriver::parse_managed_pane_row("tm-a\tclaude\t12\t%1\t ").expect("row parses");
assert_eq!(row.pane_current_path, None);
}
#[test]
fn four_column_pane_row_is_dropped() {
assert!(TmuxDriver::parse_managed_pane_row("tm-a\tclaude\t12\t%1").is_none());
}
#[test]
fn parses_a_real_tab_delimited_listing() {
let captured = "tm-00b14f3b-dd31-4474-9\tzsh\t74149\t%1860\t/tmp/gone-worktree\n\
tm-trusty-tools-01\tzsh\t10302\t%2306\t/Users/masa/trusty-tools\n\
tm-trusty-tools-02\ttrusty-mpm\t10738\t%2307\t/Users/masa/trusty-tools\n\
tm-writing-03\tzsh\t53398\t%2105\t/Users/masa/writing\n";
let rows = TmuxDriver::parse_managed_pane_rows(captured);
assert_eq!(rows.len(), 4, "every captured row must parse: {rows:?}");
assert_eq!(rows[0].session_name, "tm-00b14f3b-dd31-4474-9");
assert_eq!(rows[0].pane_current_command, "zsh");
assert_eq!(rows[0].pane_pid, Some(74149));
assert_eq!(rows[0].pane_id.as_deref(), Some("%1860"));
assert_eq!(
rows[0].pane_current_path.as_deref(),
Some("/tmp/gone-worktree")
);
assert_eq!(rows[2].session_name, "tm-trusty-tools-02");
assert_eq!(rows[2].pane_current_command, "trusty-mpm");
assert_eq!(rows[2].pane_id.as_deref(), Some("%2307"));
}
#[test]
fn sanitized_pane_row_is_dropped_not_coerced() {
let sanitized = "tm-trusty-tools-01_zsh_10302_%2306\n\
tm-trusty-tools-02_trusty-mpm_10738_%2307\n\
tm-writing-03_zsh_53398_%2105\n";
let rows = TmuxDriver::parse_managed_pane_rows(sanitized);
assert!(
rows.is_empty(),
"a row with no tab columns must be dropped, never coerced into a \
session name: {rows:?}"
);
}
#[test]
#[ignore = "needs the host's live tmux server"]
fn live_listing_keeps_its_columns() {
let Ok(driver) = TmuxDriver::discover() else {
eprintln!("no tmux binary — skipping");
return;
};
let panes = driver
.list_managed_panes()
.expect("list_managed_panes must not error against a live server");
if panes.is_empty() {
eprintln!("no tmux server running — skipping");
return;
}
for pane in &panes {
assert!(
!pane.pane_current_command.is_empty(),
"a parsed pane always carries its command: {pane:?}"
);
assert!(
!pane.session_name.contains('_') || !pane.session_name.contains('%'),
"session_name must never hold a joined row (#6529): {pane:?}"
);
assert!(
pane.pane_id
.as_deref()
.is_some_and(|id| id.starts_with('%')),
"a parsed pane always carries its tmux pane id: {pane:?}"
);
}
}
#[test]
fn rejects_malformed_session_row() {
assert!(SessionInfo::parse("").is_err());
assert!(SessionInfo::parse("name:not-a-number:0").is_err());
}
#[serial_test::serial]
#[test]
fn driver_reports_availability() {
let available = TmuxDriver::is_available();
if !available {
assert!(TmuxDriver::discover().is_err());
}
}
fn write_fake_tmux(dir: &std::path::Path, name: &str, body: &str) -> String {
use std::os::unix::fs::PermissionsExt;
let path = dir.join(name);
std::fs::write(&path, body).expect("write fake tmux script");
let mut perms = std::fs::metadata(&path)
.expect("stat fake tmux script")
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&path, perms).expect("chmod fake tmux script");
path.to_string_lossy().into_owned()
}
#[serial_test::serial]
#[test]
fn ensure_server_up_issues_start_server_on_a_fresh_socket() {
let dir = tempfile::tempdir().unwrap();
let log = dir.path().join("calls.log");
let script = format!(
"#!/bin/sh\necho \"$1\" >> '{log}'\nexit 0\n",
log = log.display()
);
let bin = write_fake_tmux(dir.path(), "fake-tmux-fresh-socket", &script);
let driver = TmuxDriver { tmux_path: bin };
driver
.ensure_server_up()
.expect("a succeeding fake tmux must report the server as up");
let calls = std::fs::read_to_string(&log).unwrap();
assert_eq!(
calls.trim(),
"start-server",
"ensure_server_up must issue exactly one start-server call: {calls:?}"
);
}
#[serial_test::serial]
#[test]
fn ensure_server_up_fails_loudly_when_the_server_never_comes_up() {
let dir = tempfile::tempdir().unwrap();
let script = "#!/bin/sh\necho 'error connecting to /tmp/tmux-502/default (No such file or directory)' >&2\nexit 1\n";
let bin = write_fake_tmux(dir.path(), "fake-tmux-always-fails", script);
let driver = TmuxDriver { tmux_path: bin };
let err = driver
.ensure_server_up()
.expect_err("an always-failing tmux must error loudly, never report success");
assert!(
matches!(err, Error::Protocol(_)),
"must surface as a Protocol error: {err:?}"
);
}
#[test]
fn empty_server_stderr_is_an_empty_list() {
assert!(stderr_means_empty_server(
"no server running on /tmp/tmux-501/default\n"
));
assert!(stderr_means_empty_server("no sessions\n"));
}
#[test]
fn unreachable_socket_stderr_is_not_an_empty_list() {
assert!(!stderr_means_empty_server(
"error connecting to /tmp/tmux-501/default (No such file or directory)"
));
assert!(!stderr_means_empty_server("lost server"));
}
#[test]
fn duplicate_session_stderr_is_recognised() {
assert!(stderr_means_duplicate_session(
"duplicate session: tm-proj-01\n"
));
}
#[test]
fn unrelated_stderr_is_not_a_name_collision() {
assert!(!stderr_means_duplicate_session(
"error connecting to /tmp/tmux-501/default (No such file or directory)"
));
assert!(!stderr_means_duplicate_session("no server running"));
assert!(!stderr_means_duplicate_session("can't create socket"));
}
#[test]
fn list_sessions_errors_on_a_host_whose_tmux_server_never_ran() {
let dir = tempfile::tempdir().unwrap();
let script = "#!/bin/sh\necho 'error connecting to /tmp/tmux-501/default (No such file or directory)' >&2\nexit 1\n";
let bin = write_fake_tmux(dir.path(), "fake-tmux-no-server", script);
let driver = TmuxDriver { tmux_path: bin };
let err = driver
.list_sessions()
.expect_err("an unreachable tmux server must not read as an empty session list");
assert!(
matches!(err, Error::Protocol(_)),
"must surface as a Protocol error: {err:?}"
);
}
#[test]
fn driver_send_command_line_refuses_an_oversized_line() {
let driver = TmuxDriver {
tmux_path: "/nonexistent/definitely-not-tmux".to_owned(),
};
let target = crate::core::tmux::TmuxTarget::session("tm-sess");
let too_long = "x".repeat(crate::core::tmux::MAX_PANE_COMMAND_BYTES + 1);
let err = driver
.send_command_line(&target, &too_long)
.expect_err("an over-length command must be refused, never truncated");
assert!(err.to_string().contains("#8233"), "{err}");
let at_limit = "x".repeat(crate::core::tmux::MAX_PANE_COMMAND_BYTES);
let err = driver
.send_command_line(&target, &at_limit)
.expect_err("the nonexistent tmux path cannot be spawned");
assert!(
!err.to_string().contains("#8233"),
"a line at the limit must pass the guard: {err}"
);
}