Files
Kigi-CLI/crates/codegen/kigi-pager-render/src/terminal/test.rs
T
ZacharyZhang-NY d6c20fc13f M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
2026-07-17 05:31:01 -04:00

1903 lines
56 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
// -- terminal_name_from_term_program (existing coverage) ------------------
#[test]
fn test_terminal_name_from_term_program() {
assert_eq!(
terminal_name_from_term_program("iTerm.app"),
Some(TerminalName::Iterm2)
);
assert_eq!(
terminal_name_from_term_program("WarpTerminal"),
Some(TerminalName::WarpTerminal)
);
assert_eq!(
terminal_name_from_term_program("Ghostty"),
Some(TerminalName::Ghostty)
);
assert_eq!(
terminal_name_from_term_program("Apple_Terminal"),
Some(TerminalName::AppleTerminal)
);
assert_eq!(
terminal_name_from_term_program("zed"),
Some(TerminalName::Zed)
);
assert_eq!(
terminal_name_from_term_program("WindowsTerminal"),
Some(TerminalName::WindowsTerminal)
);
assert_eq!(
terminal_name_from_term_program("rio"),
Some(TerminalName::Rio)
);
assert_eq!(
terminal_name_from_term_program("otty"),
Some(TerminalName::Otty)
);
assert_eq!(
terminal_name_from_term_program("Otty"),
Some(TerminalName::Otty)
);
assert_eq!(terminal_name_from_term_program("unknown-term"), None);
}
#[test]
fn brand_otty_from_term_program() {
let env = env_from(&[("TERM_PROGRAM", "otty")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Otty);
let env = env_from(&[("TERM_PROGRAM", "Otty")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Otty);
}
#[test]
fn otty_delivers_ime_as_bracketed_paste_only() {
assert!(TerminalName::Otty.delivers_ime_as_bracketed_paste());
for brand in [
TerminalName::AppleTerminal,
TerminalName::Ghostty,
TerminalName::Iterm2,
TerminalName::Unknown,
TerminalName::WezTerm,
TerminalName::Kitty,
] {
assert!(
!brand.delivers_ime_as_bracketed_paste(),
"{brand:?} must not gate IME bracketed-paste origin"
);
}
}
#[test]
fn otty_is_capability_unclassified_like_unknown() {
assert!(TerminalName::Otty.is_capability_unclassified());
assert!(TerminalName::Unknown.is_capability_unclassified());
assert!(!TerminalName::Ghostty.is_capability_unclassified());
assert!(!TerminalName::AppleTerminal.is_capability_unclassified());
}
#[test]
fn otty_skips_kitty_keyboard_like_unknown() {
let ctx = TerminalContext {
brand: TerminalName::Otty,
env_brand: TerminalName::Otty,
multiplexer: MultiplexerKind::Undetected,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("unknown_no_multiplexer"));
assert!(ctx.shift_enter_unavailable());
}
// -- detect_terminal_brand_from_env (pure) --------------------------------
#[test]
fn brand_ghostty_from_term_program() {
let env = env_from(&[("TERM_PROGRAM", "Ghostty")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Ghostty);
}
#[test]
fn brand_wezterm_from_version_var() {
let env = env_from(&[("WEZTERM_VERSION", "20240203")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::WezTerm);
}
#[test]
fn brand_iterm2_from_session_id() {
let env = env_from(&[("ITERM_SESSION_ID", "w0t0p0")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Iterm2);
}
#[test]
fn brand_iterm2_from_lc_terminal_over_ssh() {
// Only LC_TERMINAL survives the SSH hop; ITERM_SESSION_ID / TERM_PROGRAM do not.
let env = env_from(&[("LC_TERMINAL", "iTerm2")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Iterm2);
}
#[test]
fn brand_not_iterm2_for_foreign_lc_terminal() {
// Value-exact gate: a non-iTerm2 LC_TERMINAL value must NOT match
// (matches on value, not bare presence).
let env = env_from(&[("LC_TERMINAL", "not-iterm2")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Unknown);
}
#[test]
fn brand_kitty_from_term() {
let env = env_from(&[("TERM", "xterm-kitty")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Kitty);
}
#[test]
fn brand_alacritty_from_term() {
let env = env_from(&[("TERM", "alacritty")]);
assert_eq!(
detect_terminal_brand_from_env(&env),
TerminalName::Alacritty
);
}
#[test]
fn brand_rio_from_term() {
let env = env_from(&[("TERM", "rio")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Rio);
}
#[test]
fn brand_foot_from_term() {
let env = env_from(&[("TERM", "foot")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Foot);
}
#[test]
fn brand_foot_extra_from_term() {
let env = env_from(&[("TERM", "foot-extra")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Foot);
}
#[test]
fn brand_foot_direct_from_term() {
let env = env_from(&[("TERM", "foot-direct")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Foot);
}
#[test]
fn brand_not_foot_for_unrelated_term() {
// Over-match guard: a TERM merely containing "foot" is not foot.
let env = env_from(&[("TERM", "xterm-footbar")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Unknown);
}
#[test]
fn brand_not_foot_for_foot_prefixed_out_of_set_term() {
// Inverse boundary: a `foot`-prefixed value outside the exact set
// stays Unknown (pins the exact-set decision over `starts_with`).
let env = env_from(&[("TERM", "foot-extra-256color")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Unknown);
}
#[test]
fn brand_vte_from_vte_version() {
let env = env_from(&[("VTE_VERSION", "7402")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Vte);
}
#[test]
fn brand_terminator_from_term_program() {
let env = env_from(&[("TERM_PROGRAM", "terminator")]);
assert_eq!(
detect_terminal_brand_from_env(&env),
TerminalName::Terminator
); // WHY: canonical per detect-terminal
}
#[test]
fn terminator_vte_version_interaction() {
let env = env_from(&[("TERM_PROGRAM", "terminator"), ("VTE_VERSION", "8200")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.brand, TerminalName::Terminator);
assert!(ctx.is_vte_based()); // WHY: helper covers version + brand
}
#[test]
fn terminator_with_tmux() {
let env = env_from(&[("TERM_PROGRAM", "terminator"), ("TMUX", "/tmp/tmux")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.brand, TerminalName::Terminator);
assert_eq!(ctx.multiplexer, MultiplexerKind::Tmux);
}
#[test]
fn terminator_over_ssh() {
let env = env_from(&[("TERM_PROGRAM", "terminator"), ("SSH_TTY", "/dev/pts/0")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.brand, TerminalName::Terminator);
assert!(ctx.is_ssh);
}
#[test]
fn terminator_focus_tracking() {
let env = env_from(&[("TERM_PROGRAM", "terminator")]);
let ctx = build_terminal_context_from_env(&env);
assert!(!matches!(
ctx.brand,
TerminalName::AppleTerminal | TerminalName::Unknown
)); // supports focus like VTE
assert!(ctx.is_vte_based());
}
#[test]
fn brand_windows_terminal_from_wt_session() {
let env = env_from(&[("WT_SESSION", "a1b2c3d4-e5f6-7890-abcd-ef1234567890")]);
assert_eq!(
detect_terminal_brand_from_env(&env),
TerminalName::WindowsTerminal
);
}
#[test]
fn brand_unknown_empty_env() {
let env = env_from(&[]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Unknown);
}
// -- refine_unknown_brand_for_host ---------------------------------------
#[test]
fn refine_unknown_brand_defaults_to_wt_only_on_windows() {
use super::TerminalName::{Unknown, VsCode, WindowsTerminal};
use crate::host::HostOs::{Linux, Windows};
let cases = [
(Unknown, Windows, WindowsTerminal), // DefTerm handoff: no WT_SESSION
(Unknown, Linux, Unknown),
(VsCode, Windows, VsCode), // never override a positively detected brand
];
for (brand, host, expected) in cases {
assert_eq!(refine_unknown_brand_for_host(brand, host), expected);
}
}
#[test]
fn env_brand_holds_raw_detection_independent_of_refinement() {
// `build_*` records the raw detection in both fields.
let ctx = build_terminal_context_from_env(&env_from(&[]));
assert_eq!(ctx.brand, TerminalName::Unknown);
assert_eq!(ctx.env_brand, TerminalName::Unknown);
// The fallback refines only `brand`; `env_brand` stays raw so the glyphs
// legacy-console check and shift_enter_unavailable still treat a bare
// ConHost conservatively.
let effective = refine_unknown_brand_for_host(ctx.brand, crate::host::HostOs::Windows);
assert_eq!(effective, TerminalName::WindowsTerminal);
assert_eq!(ctx.env_brand, TerminalName::Unknown);
}
#[test]
fn mouse_reporting_leaks_only_for_jetbrains_on_windows() {
use crate::host::HostOs;
// The reported failure: JediTerm on Windows leaks raw X10 mouse bytes.
assert!(mouse_reporting_leaks(
TerminalName::JetBrains,
HostOs::Windows
));
// JetBrains on macOS/Linux is fine — crossterm's unix parser decodes the
// reports, so we must NOT downgrade those to minimal.
assert!(!mouse_reporting_leaks(
TerminalName::JetBrains,
HostOs::Macos
));
assert!(!mouse_reporting_leaks(
TerminalName::JetBrains,
HostOs::Linux
));
// Other Windows terminals deliver native console mouse records — no leak.
assert!(!mouse_reporting_leaks(
TerminalName::WindowsTerminal,
HostOs::Windows
));
assert!(!mouse_reporting_leaks(TerminalName::Kitty, HostOs::Windows));
}
// -- detect_byobu_from_env ------------------------------------------------
#[test]
fn byobu_tmux_explicit_backend() {
let env = env_from(&[("BYOBU_BACKEND", "tmux"), ("TMUX", "/tmp/tmux")]);
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Tmux));
}
#[test]
fn byobu_screen_explicit_backend() {
let env = env_from(&[("BYOBU_BACKEND", "screen"), ("STY", "12345.pts-0")]);
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Screen));
}
#[test]
fn byobu_inferred_tmux_from_config_dir() {
let env = env_from(&[
("BYOBU_CONFIG_DIR", "/home/user/.byobu"),
("TMUX", "/tmp/tmux"),
]);
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Tmux));
}
#[test]
fn byobu_inferred_screen_from_distro() {
let env = env_from(&[("BYOBU_DISTRO", "Ubuntu"), ("STY", "12345.pts-0")]);
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Screen));
}
#[test]
fn byobu_markers_but_no_mux_markers_returns_none() {
let env = env_from(&[("BYOBU_CONFIG_DIR", "/home/user/.byobu")]);
assert_eq!(detect_byobu_from_env(&env), None);
}
#[test]
fn no_byobu_markers_returns_none() {
let env = env_from(&[("TMUX", "/tmp/tmux")]);
assert_eq!(detect_byobu_from_env(&env), None);
}
// -- detect_multiplexer_from_env ------------------------------------------
#[test]
fn mux_plain_tmux() {
let env = env_from(&[("TMUX", "/tmp/tmux-501/default,12345,0")]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
}
#[test]
fn mux_plain_screen() {
let env = env_from(&[("STY", "12345.pts-0.host")]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Screen);
}
#[test]
fn mux_zellij() {
let env = env_from(&[("ZELLIJ", "0")]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Zellij);
}
#[test]
fn mux_zellij_from_session_name() {
let env = env_from(&[("ZELLIJ_SESSION_NAME", "my-session")]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Zellij);
}
#[test]
fn mux_none_empty_env() {
let env = env_from(&[]);
assert_eq!(
detect_multiplexer_from_env(&env),
MultiplexerKind::Undetected
);
}
#[test]
fn mux_cmux_from_socket_path() {
let env = env_from(&[("CMUX_SOCKET_PATH", "/tmp/cmux.sock")]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Cmux);
}
#[test]
fn mux_cmux_from_panel_id() {
let env = env_from(&[("CMUX_PANEL_ID", "1")]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Cmux);
}
#[test]
fn mux_cmux_from_bundle_id() {
let env = env_from(&[("CMUX_BUNDLE_ID", "com.cmuxterm.app")]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Cmux);
}
#[test]
fn mux_empty_cmux_socket_alone_is_undetected() {
// CMUX_SOCKET may be present but empty under cmux; env_get filters empties.
// Without a non-empty CMUX_* marker, do not classify as Cmux.
let env = env_from(&[("CMUX_SOCKET", "")]);
assert_eq!(
detect_multiplexer_from_env(&env),
MultiplexerKind::Undetected
);
}
#[test]
fn mux_tmux_nested_inside_cmux_wins() {
let env = env_from(&[
("TMUX", "/tmp/tmux-501/default,12345,0"),
("CMUX_SOCKET_PATH", "/tmp/cmux.sock"),
("CMUX_PANEL_ID", "1"),
]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
}
// -- ambiguous marker precedence ------------------------------------------
#[test]
fn tmux_beats_zellij_when_both_set() {
// tmux pane launched from inside Zellij leaves inherited ZELLIJ var.
let env = env_from(&[("TMUX", "/tmp/tmux-501/default,12345,0"), ("ZELLIJ", "0")]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
}
#[test]
fn byobu_screen_beats_tmux_marker() {
// BYOBU_BACKEND=screen with a stale TMUX var — Byobu backend wins.
let env = env_from(&[
("BYOBU_BACKEND", "screen"),
("TMUX", "/tmp/tmux-old"),
("STY", "12345.pts-0"),
]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Screen);
}
#[test]
fn byobu_tmux_explicit_with_sty_stays_tmux() {
// BYOBU_BACKEND=tmux + stale STY — Byobu backend wins.
let env = env_from(&[
("BYOBU_BACKEND", "tmux"),
("STY", "old-screen"),
("TMUX", "/tmp/tmux"),
]);
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
}
// -- detect_tmux_meta_from_env --------------------------------------------
#[test]
fn tmux_meta_populated() {
let env = env_from(&[
("TMUX", "/tmp/tmux-501/default,12345,0"),
("TMUX_PANE", "%3"),
]);
let meta = detect_tmux_meta_from_env(&env);
assert_eq!(
meta.tmux_env.as_deref(),
Some("/tmp/tmux-501/default,12345,0")
);
assert_eq!(meta.tmux_pane.as_deref(), Some("%3"));
}
#[test]
fn tmux_meta_empty_outside_tmux() {
let env = env_from(&[]);
let meta = detect_tmux_meta_from_env(&env);
assert_eq!(meta, TmuxClientMeta::default());
}
// -- build_terminal_context_from_env (integration) ------------------------
#[test]
fn context_plain_terminal() {
let env = env_from(&[("TERM_PROGRAM", "Ghostty")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.brand, TerminalName::Ghostty);
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
assert_eq!(ctx.byobu, None);
assert!(!ctx.is_tmux_backed());
assert!(!ctx.is_byobu());
}
#[test]
fn context_iterm2_lc_terminal_version_fallback_over_ssh() {
// Over SSH, TERM_PROGRAM_VERSION is stripped; version gating falls back to
// iTerm2's SSH-surviving LC_TERMINAL_VERSION.
let env = env_from(&[("LC_TERMINAL", "iTerm2"), ("LC_TERMINAL_VERSION", "3.5.6")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.brand, TerminalName::Iterm2);
assert_eq!(ctx.term_program_version.as_deref(), Some("3.5.6"));
}
#[test]
fn context_plain_tmux() {
let env = env_from(&[
("TERM_PROGRAM", "iTerm2"),
("TMUX", "/tmp/tmux-501/default,12345,0"),
("TMUX_PANE", "%0"),
]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.brand, TerminalName::Iterm2);
assert_eq!(ctx.multiplexer, MultiplexerKind::Tmux);
assert_eq!(ctx.byobu, None);
assert!(ctx.is_tmux_backed());
assert!(!ctx.is_byobu());
assert_eq!(ctx.tmux_config_path(), "~/.tmux.conf");
assert_eq!(
ctx.tmux_meta.tmux_env.as_deref(),
Some("/tmp/tmux-501/default,12345,0")
);
}
#[test]
fn context_byobu_tmux() {
let env = env_from(&[
("BYOBU_BACKEND", "tmux"),
("TMUX", "/tmp/tmux-501/default,12345,0"),
("TMUX_PANE", "%1"),
]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.multiplexer, MultiplexerKind::Tmux);
assert_eq!(ctx.byobu, Some(ByobuBackend::Tmux));
assert!(ctx.is_tmux_backed());
assert!(ctx.is_byobu());
assert_eq!(ctx.tmux_config_path(), "~/.byobu/.tmux.conf");
}
#[test]
fn context_byobu_screen() {
let env = env_from(&[("BYOBU_BACKEND", "screen"), ("STY", "12345.pts-0.host")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
assert_eq!(ctx.byobu, Some(ByobuBackend::Screen));
assert!(!ctx.is_tmux_backed());
assert!(ctx.is_byobu());
}
#[test]
fn context_plain_screen_no_byobu() {
let env = env_from(&[("STY", "12345.pts-0.host")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
assert_eq!(ctx.byobu, None);
assert!(!ctx.is_byobu());
}
#[test]
fn context_zellij() {
let env = env_from(&[
("ZELLIJ", "0"),
("ZELLIJ_SESSION_NAME", "my-session"),
("TERM_PROGRAM", "Ghostty"),
]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.brand, TerminalName::Ghostty);
assert_eq!(ctx.multiplexer, MultiplexerKind::Zellij);
assert_eq!(ctx.byobu, None);
assert!(!ctx.is_tmux_backed());
}
#[test]
fn context_tmux_inside_zellij() {
// tmux launched from Zellij: TMUX is set, inherited ZELLIJ var remains.
let env = env_from(&[("TMUX", "/tmp/tmux-501/default,12345,0"), ("ZELLIJ", "0")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.multiplexer, MultiplexerKind::Tmux);
assert!(ctx.is_tmux_backed());
}
#[test]
fn context_tmux_meta_not_populated_for_non_tmux() {
let env = env_from(&[("ZELLIJ", "0")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.tmux_meta, TmuxClientMeta::default());
}
#[test]
fn context_ambiguous_byobu_screen_with_tmux() {
// BYOBU_BACKEND=screen + stale TMUX → screen wins via Byobu backend.
let env = env_from(&[
("BYOBU_BACKEND", "screen"),
("TMUX", "/tmp/tmux-old"),
("STY", "12345.pts-0"),
]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
assert_eq!(ctx.byobu, Some(ByobuBackend::Screen));
}
#[test]
fn context_empty_env_values_ignored() {
// Empty string values should not trigger detection.
let env = env_from(&[("TMUX", ""), ("ZELLIJ", ""), ("STY", "")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
}
// =====================================================================
// determine_alt_screen_policy: fullscreen policy matrix
// =====================================================================
fn plain_ctx() -> TerminalContext {
TerminalContext {
brand: TerminalName::Ghostty,
multiplexer: MultiplexerKind::Undetected,
..Default::default()
}
}
fn tmux_ctx() -> TerminalContext {
TerminalContext {
brand: TerminalName::Iterm2,
multiplexer: MultiplexerKind::Tmux,
tmux_meta: TmuxClientMeta {
tmux_env: Some("/tmp/tmux-501/default,12345,0".to_owned()),
tmux_pane: Some("%0".to_owned()),
},
..Default::default()
}
}
fn zellij_ctx_test() -> TerminalContext {
TerminalContext {
brand: TerminalName::Ghostty,
multiplexer: MultiplexerKind::Zellij,
..Default::default()
}
}
fn screen_ctx() -> TerminalContext {
TerminalContext {
brand: TerminalName::Unknown,
multiplexer: MultiplexerKind::Screen,
..Default::default()
}
}
fn byobu_tmux_ctx() -> TerminalContext {
TerminalContext {
brand: TerminalName::Unknown,
multiplexer: MultiplexerKind::Tmux,
byobu: Some(ByobuBackend::Tmux),
tmux_meta: TmuxClientMeta {
tmux_env: Some("/tmp/tmux".to_owned()),
tmux_pane: Some("%1".to_owned()),
},
..Default::default()
}
}
fn byobu_screen_ctx() -> TerminalContext {
TerminalContext {
brand: TerminalName::Unknown,
multiplexer: MultiplexerKind::Screen,
byobu: Some(ByobuBackend::Screen),
..Default::default()
}
}
// -- Alt-screen policy matrix (all modes × contexts × CLI override) -------
#[derive(Debug)]
struct AltScreenCase {
name: &'static str,
cli_no_alt: bool,
mode: AltScreenMode,
ctx: TerminalContext,
control: bool,
expected: bool,
}
#[test]
fn alt_screen_policy_matrix() {
let cases = [
// Auto mode
AltScreenCase {
name: "auto_plain_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Auto,
ctx: plain_ctx(),
control: false,
expected: true,
},
AltScreenCase {
name: "auto_tmux_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Auto,
ctx: tmux_ctx(),
control: false,
expected: true,
},
AltScreenCase {
name: "auto_tmux_control_inline",
cli_no_alt: false,
mode: AltScreenMode::Auto,
ctx: tmux_ctx(),
control: true,
expected: false,
},
AltScreenCase {
name: "auto_zellij_inline",
cli_no_alt: false,
mode: AltScreenMode::Auto,
ctx: zellij_ctx_test(),
control: false,
expected: false,
},
AltScreenCase {
name: "auto_screen_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Auto,
ctx: screen_ctx(),
control: false,
expected: true,
},
AltScreenCase {
name: "auto_byobu_tmux_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Auto,
ctx: byobu_tmux_ctx(),
control: false,
expected: true,
},
AltScreenCase {
name: "auto_byobu_screen_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Auto,
ctx: byobu_screen_ctx(),
control: false,
expected: true,
},
AltScreenCase {
name: "auto_byobu_tmux_control_inline",
cli_no_alt: false,
mode: AltScreenMode::Auto,
ctx: byobu_tmux_ctx(),
control: true,
expected: false,
},
// Never mode: always inline
AltScreenCase {
name: "never_plain_inline",
cli_no_alt: false,
mode: AltScreenMode::Never,
ctx: plain_ctx(),
control: false,
expected: false,
},
AltScreenCase {
name: "never_tmux_inline",
cli_no_alt: false,
mode: AltScreenMode::Never,
ctx: tmux_ctx(),
control: false,
expected: false,
},
AltScreenCase {
name: "never_screen_inline",
cli_no_alt: false,
mode: AltScreenMode::Never,
ctx: screen_ctx(),
control: false,
expected: false,
},
AltScreenCase {
name: "never_zellij_inline",
cli_no_alt: false,
mode: AltScreenMode::Never,
ctx: zellij_ctx_test(),
control: false,
expected: false,
},
AltScreenCase {
name: "never_byobu_tmux_inline",
cli_no_alt: false,
mode: AltScreenMode::Never,
ctx: byobu_tmux_ctx(),
control: false,
expected: false,
},
AltScreenCase {
name: "never_byobu_screen_inline",
cli_no_alt: false,
mode: AltScreenMode::Never,
ctx: byobu_screen_ctx(),
control: false,
expected: false,
},
// Always mode: forces fullscreen
AltScreenCase {
name: "always_plain_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Always,
ctx: plain_ctx(),
control: false,
expected: true,
},
AltScreenCase {
name: "always_tmux_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Always,
ctx: tmux_ctx(),
control: false,
expected: true,
},
AltScreenCase {
name: "always_tmux_control_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Always,
ctx: tmux_ctx(),
control: true,
expected: true,
},
AltScreenCase {
name: "always_zellij_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Always,
ctx: zellij_ctx_test(),
control: false,
expected: true,
},
AltScreenCase {
name: "always_screen_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Always,
ctx: screen_ctx(),
control: false,
expected: true,
},
AltScreenCase {
name: "always_byobu_screen_fullscreen",
cli_no_alt: false,
mode: AltScreenMode::Always,
ctx: byobu_screen_ctx(),
control: false,
expected: true,
},
// CLI --no-alt-screen overrides everything
AltScreenCase {
name: "cli_no_alt_overrides_auto",
cli_no_alt: true,
mode: AltScreenMode::Auto,
ctx: plain_ctx(),
control: false,
expected: false,
},
AltScreenCase {
name: "cli_no_alt_overrides_always",
cli_no_alt: true,
mode: AltScreenMode::Always,
ctx: plain_ctx(),
control: false,
expected: false,
},
AltScreenCase {
name: "cli_no_alt_overrides_never",
cli_no_alt: true,
mode: AltScreenMode::Never,
ctx: plain_ctx(),
control: false,
expected: false,
},
AltScreenCase {
name: "cli_no_alt_overrides_auto_in_tmux",
cli_no_alt: true,
mode: AltScreenMode::Auto,
ctx: tmux_ctx(),
control: false,
expected: false,
},
AltScreenCase {
name: "cli_no_alt_overrides_always_in_zellij",
cli_no_alt: true,
mode: AltScreenMode::Always,
ctx: zellij_ctx_test(),
control: false,
expected: false,
},
AltScreenCase {
name: "cli_no_alt_overrides_always_in_tmux_control",
cli_no_alt: true,
mode: AltScreenMode::Always,
ctx: tmux_ctx(),
control: true,
expected: false,
},
];
for case in &cases {
let result =
determine_alt_screen_policy(case.cli_no_alt, case.mode, &case.ctx, case.control);
assert_eq!(
result, case.expected,
"alt_screen_policy failed on case '{}'",
case.name
);
}
}
// -- Windows Terminal context integration ---------------------------------
#[test]
fn context_windows_terminal() {
let env = env_from(&[("WT_SESSION", "a1b2c3d4-e5f6-7890-abcd-ef1234567890")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.brand, TerminalName::WindowsTerminal);
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
assert!(!ctx.is_ssh);
}
// -- Terminal brand detection edge cases -----------------------------------
// =====================================================================
// Extended environment matrix (final hardening)
// =====================================================================
// -- Byobu-screen: auto keeps fullscreen (screen is not auto-disabled) ----
#[test]
fn auto_byobu_screen_is_fullscreen() {
let ctx = TerminalContext {
brand: TerminalName::Unknown,
multiplexer: MultiplexerKind::Screen,
byobu: Some(ByobuBackend::Screen),
..Default::default()
};
assert!(determine_alt_screen_policy(
false,
AltScreenMode::Auto,
&ctx,
false
));
}
// -- Terminal brand detection edge cases -----------------------------------
#[test]
fn brand_vscode_from_term_program() {
let env = env_from(&[("TERM_PROGRAM", "vscode")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::VsCode);
}
#[test]
fn brand_kitty_from_window_id() {
let env = env_from(&[("KITTY_WINDOW_ID", "1")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Kitty);
}
#[test]
fn brand_alacritty_from_socket() {
let env = env_from(&[("ALACRITTY_SOCKET", "/tmp/alacritty.sock")]);
assert_eq!(
detect_terminal_brand_from_env(&env),
TerminalName::Alacritty
);
}
#[test]
fn brand_apple_terminal_from_session_id() {
let env = env_from(&[("TERM_SESSION_ID", "w0t0p0:ABC123")]);
assert_eq!(
detect_terminal_brand_from_env(&env),
TerminalName::AppleTerminal
);
}
#[test]
fn brand_term_program_takes_precedence_over_other_vars() {
// TERM_PROGRAM should win even when other brand-specific vars are set.
let env = env_from(&[
("TERM_PROGRAM", "Ghostty"),
("WEZTERM_VERSION", "20240203"),
("ITERM_SESSION_ID", "w0t0p0"),
]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Ghostty);
}
// -- IDE family detection (VS Code forks / xterm.js embeds) ---------------
#[test]
fn brand_cursor_from_cursor_trace_id() {
// Cursor sets a unique CURSOR_TRACE_ID; it also sets TERM_PROGRAM=vscode
// (since it's a VS Code fork), so we must detect it before the
// TERM_PROGRAM lookup.
let env = env_from(&[
("CURSOR_TRACE_ID", "abcdef0123456789"),
("TERM_PROGRAM", "vscode"),
]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Cursor);
}
#[test]
fn brand_cursor_from_askpass_main() {
// Cursor exposes VSCODE_GIT_ASKPASS_MAIN pointing at its install path.
let env = env_from(&[
(
"VSCODE_GIT_ASKPASS_MAIN",
"/Applications/Cursor.app/Contents/Resources/app/extensions/git/dist/askpass-main.js",
),
("TERM_PROGRAM", "vscode"),
]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Cursor);
}
#[test]
fn brand_windsurf_from_askpass_main() {
// Another VS Code fork; same askpass path substring pattern.
let env = env_from(&[
(
"VSCODE_GIT_ASKPASS_MAIN",
"/Applications/Windsurf.app/Contents/Resources/app/extensions/git/dist/askpass-main.js",
),
("TERM_PROGRAM", "vscode"),
]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Windsurf);
}
#[test]
fn brand_zed_from_term_program() {
let env = env_from(&[("TERM_PROGRAM", "zed")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Zed);
}
#[test]
fn brand_vscode_when_no_ide_markers() {
// Pure VS Code: TERM_PROGRAM=vscode and no CURSOR_TRACE_ID / matching
// askpass marker. Should remain VsCode, not collapse to anything else.
let env = env_from(&[("TERM_PROGRAM", "vscode")]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::VsCode);
}
#[test]
fn brand_vscode_when_askpass_does_not_match_ide() {
// Pure VS Code askpass path (no "cursor"/"windsurf") — brand is VsCode
// even when TERM_PROGRAM is also set.
let env = env_from(&[
("VSCODE_GIT_ASKPASS_MAIN", "/usr/local/bin/askpass-main.js"),
("TERM_PROGRAM", "vscode"),
]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::VsCode);
}
#[test]
fn brand_vscode_from_askpass_without_term_program() {
// Remote SSH / tmux: TERM_PROGRAM missing or overwritten, but the VS Code
// remote agent still injects VSCODE_GIT_ASKPASS_MAIN into the pane env.
let env = env_from(&[(
"VSCODE_GIT_ASKPASS_MAIN",
"/home/user/.vscode-server/bin/abc/askpass",
)]);
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::VsCode);
}
// -- Zellij detection from ZELLIJ_VERSION (no ZELLIJ or SESSION_NAME) -----
#[test]
fn mux_zellij_not_from_version_only() {
// ZELLIJ_VERSION alone does not trigger Zellij detection — it must be
// ZELLIJ or ZELLIJ_SESSION_NAME.
let env = env_from(&[("ZELLIJ_VERSION", "0.43.1")]);
assert_eq!(
detect_multiplexer_from_env(&env),
MultiplexerKind::Undetected
);
}
// -- Byobu inference edge cases -------------------------------------------
#[test]
fn byobu_unknown_backend_string_with_tmux() {
// Unknown BYOBU_BACKEND value falls through to mux-marker inference.
let env = env_from(&[("BYOBU_BACKEND", "unknown"), ("TMUX", "/tmp/tmux")]);
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Tmux));
}
#[test]
fn byobu_unknown_backend_string_with_sty() {
let env = env_from(&[("BYOBU_BACKEND", "fish"), ("STY", "12345.pts-0")]);
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Screen));
}
#[test]
fn byobu_unknown_backend_no_mux_returns_none() {
let env = env_from(&[("BYOBU_BACKEND", "unknown")]);
assert_eq!(detect_byobu_from_env(&env), None);
}
// -- Context-level edge cases ---------------------------------------------
#[test]
fn context_sty_takes_screen_when_no_tmux_or_zellij() {
let env = env_from(&[("STY", "12345.pts-0.host")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
assert_eq!(ctx.byobu, None);
assert!(!ctx.is_tmux_backed());
}
#[test]
fn context_screen_does_not_populate_tmux_meta() {
let env = env_from(&[("STY", "12345.pts-0.host"), ("TMUX_PANE", "%stale")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
// tmux_meta should be default since multiplexer is not Tmux.
assert_eq!(ctx.tmux_meta, TmuxClientMeta::default());
}
#[test]
fn context_tmux_config_path_is_standard_for_plain_tmux() {
let env = env_from(&[("TMUX", "/tmp/tmux-501/default,1,0")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.tmux_config_path(), "~/.tmux.conf");
}
#[test]
fn context_tmux_config_path_is_byobu_for_byobu_tmux() {
let env = env_from(&[("BYOBU_BACKEND", "tmux"), ("TMUX", "/tmp/tmux")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.tmux_config_path(), "~/.byobu/.tmux.conf");
}
#[test]
fn context_is_byobu_returns_true_for_both_backends() {
let tmux_env = env_from(&[("BYOBU_BACKEND", "tmux"), ("TMUX", "/tmp/tmux")]);
let screen_env = env_from(&[("BYOBU_BACKEND", "screen"), ("STY", "12345.pts-0")]);
assert!(build_terminal_context_from_env(&tmux_env).is_byobu());
assert!(build_terminal_context_from_env(&screen_env).is_byobu());
}
#[test]
fn context_is_byobu_returns_false_without_byobu_markers() {
let env = env_from(&[("TMUX", "/tmp/tmux")]);
assert!(!build_terminal_context_from_env(&env).is_byobu());
}
// =====================================================================
// parse_tmux_major_minor: version string parsing
// =====================================================================
#[test]
fn parse_tmux_version_standard() {
assert_eq!(parse_tmux_major_minor("tmux 3.4"), Some((3, 4)));
}
#[test]
fn parse_tmux_version_with_letter_suffix() {
assert_eq!(parse_tmux_major_minor("tmux 3.3a"), Some((3, 3)));
}
#[test]
fn parse_tmux_version_old() {
assert_eq!(parse_tmux_major_minor("tmux 2.9"), Some((2, 9)));
}
#[test]
fn parse_tmux_version_next_gen() {
assert_eq!(parse_tmux_major_minor("tmux 4.0"), Some((4, 0)));
}
#[test]
fn parse_tmux_version_missing_prefix() {
assert_eq!(parse_tmux_major_minor("3.4"), None);
}
#[test]
fn parse_tmux_version_empty() {
assert_eq!(parse_tmux_major_minor(""), None);
}
#[test]
fn parse_tmux_version_garbage() {
assert_eq!(parse_tmux_major_minor("not a version"), None);
}
#[test]
fn parse_tmux_version_trailing_dot() {
assert_eq!(parse_tmux_major_minor("tmux 3."), None);
}
#[test]
fn parse_tmux_version_no_minor() {
assert_eq!(parse_tmux_major_minor("tmux 3"), None);
}
// =====================================================================
// parse_semver_major_minor: TERM_PROGRAM_VERSION parsing
// =====================================================================
#[test]
fn parse_semver_standard() {
assert_eq!(parse_semver_major_minor("3.6.0"), Some((3, 6)));
}
#[test]
fn parse_semver_two_part() {
assert_eq!(parse_semver_major_minor("1.2"), Some((1, 2)));
}
#[test]
fn parse_semver_high_version() {
assert_eq!(parse_semver_major_minor("4.0.0"), Some((4, 0)));
}
#[test]
fn parse_semver_empty() {
assert_eq!(parse_semver_major_minor(""), None);
}
#[test]
fn parse_semver_garbage() {
assert_eq!(parse_semver_major_minor("garbage"), None);
}
#[test]
fn parse_semver_major_only() {
assert_eq!(parse_semver_major_minor("3"), None);
}
// =====================================================================
// graphics_protocol_skip_reason
#[test]
fn graphics_protocol_skip_reason_tmux() {
let ctx = TerminalContext {
brand: TerminalName::Kitty,
multiplexer: MultiplexerKind::Tmux,
..Default::default()
};
assert_eq!(ctx.graphics_protocol_skip_reason(), Some("tmux"));
}
#[test]
fn graphics_protocol_skip_reason_plain_kitty() {
let ctx = TerminalContext {
brand: TerminalName::Kitty,
..Default::default()
};
assert_eq!(ctx.graphics_protocol_skip_reason(), None);
}
// kitty_skip_reason: Kitty keyboard protocol skip-reason matrix
// =====================================================================
#[test]
fn kitty_skip_vscode() {
let ctx = TerminalContext {
brand: TerminalName::VsCode,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
}
#[test]
fn kitty_skip_cursor() {
let ctx = TerminalContext {
brand: TerminalName::Cursor,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
}
#[test]
fn kitty_skip_windsurf() {
let ctx = TerminalContext {
brand: TerminalName::Windsurf,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
}
#[test]
fn kitty_skip_zed() {
let ctx = TerminalContext {
brand: TerminalName::Zed,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
}
#[test]
fn kitty_skip_apple_terminal() {
let ctx = TerminalContext {
brand: TerminalName::AppleTerminal,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("apple_terminal"));
}
#[test]
fn kitty_skip_screen() {
let ctx = TerminalContext {
multiplexer: MultiplexerKind::Screen,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("screen"));
}
#[test]
fn kitty_skip_old_tmux() {
let ctx = TerminalContext {
multiplexer: MultiplexerKind::Tmux,
tmux_version: Some("tmux 3.2".to_owned()),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("tmux_old"));
}
#[test]
fn kitty_skip_tmux_no_version() {
let ctx = TerminalContext {
multiplexer: MultiplexerKind::Tmux,
tmux_version: None,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("tmux_old"));
}
#[test]
fn kitty_allowed_modern_tmux() {
let ctx = TerminalContext {
multiplexer: MultiplexerKind::Tmux,
tmux_version: Some("tmux 3.3".to_owned()),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), None);
}
#[test]
fn kitty_allowed_modern_tmux_letter() {
let ctx = TerminalContext {
multiplexer: MultiplexerKind::Tmux,
tmux_version: Some("tmux 3.3a".to_owned()),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), None);
}
#[test]
fn kitty_allowed_tmux_4() {
let ctx = TerminalContext {
multiplexer: MultiplexerKind::Tmux,
tmux_version: Some("tmux 4.0".to_owned()),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), None);
}
#[test]
fn kitty_skip_unknown_terminal_no_multiplexer() {
// Unknown brand with no multiplexer = no positive evidence of KKP
// support. Catches VSCode-over-SSH, bare Docker containers, etc.
let ctx = TerminalContext::default();
assert_eq!(ctx.kitty_skip_reason(), Some("unknown_no_multiplexer"));
}
#[test]
fn kitty_allowed_known_good_terminal() {
let ctx = TerminalContext {
brand: TerminalName::Ghostty,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), None);
}
#[test]
fn kitty_allowed_foot_no_multiplexer() {
// Core fix: a bare foot window negotiates KKP (no skip reason).
let ctx = TerminalContext {
brand: TerminalName::Foot,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), None);
}
#[test]
fn shift_enter_available_on_foot() {
let ctx = TerminalContext {
brand: TerminalName::Foot,
env_brand: TerminalName::Foot,
..Default::default()
};
assert!(!ctx.shift_enter_unavailable());
}
#[test]
fn foot_ctrl_dot_is_reliable() {
let ctx = TerminalContext {
brand: TerminalName::Foot,
..Default::default()
};
assert!(!ctx.ctrl_dot_unreliable());
}
#[test]
fn kitty_skip_foot_over_old_tmux_uses_mux_logic() {
let ctx = TerminalContext {
brand: TerminalName::Foot,
multiplexer: MultiplexerKind::Tmux,
tmux_version: Some("tmux 3.0".to_owned()),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("tmux_old"));
}
#[test]
fn kitty_allowed_zellij() {
let ctx = TerminalContext {
multiplexer: MultiplexerKind::Zellij,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), None);
}
#[test]
fn kitty_skip_byobu_screen() {
let ctx = TerminalContext {
multiplexer: MultiplexerKind::Screen,
byobu: Some(ByobuBackend::Screen),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("screen"));
}
#[test]
fn kitty_skip_windows_terminal() {
let ctx = TerminalContext {
brand: TerminalName::WindowsTerminal,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("windows_terminal"));
}
#[test]
fn kitty_skip_vscode_over_tmux() {
// When both brand and multiplexer would skip, brand wins.
let ctx = TerminalContext {
brand: TerminalName::VsCode,
multiplexer: MultiplexerKind::Tmux,
tmux_version: Some("tmux 3.0".to_owned()),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
}
#[test]
fn kitty_skip_vte_version() {
// VTE does not support Kitty keyboard protocol and crossterm's probe
// can false-positive on it. https://gitlab.gnome.org/GNOME/vte/-/issues/2601
let ctx = TerminalContext {
vte_version: Some("7402".to_owned()),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
}
#[test]
fn kitty_skip_vte_brand() {
let ctx = TerminalContext {
brand: TerminalName::Vte,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
}
// =====================================================================
// shift_enter_unavailable: VTE version gating for Shift+Enter
// =====================================================================
//
// VTE 0.82.0 (= VTE_VERSION 8200) is the first release containing the
// Kitty keyboard protocol; earlier versions cannot distinguish
// Shift+Enter from bare Enter, so the UI should advertise Alt+Enter
// for newline insertion instead.
#[test]
fn shift_enter_unavailable_legacy_vte_version() {
// VTE 0.64.2 (real user report) — well below the KKP cutoff.
let ctx = TerminalContext {
vte_version: Some("6402".to_owned()),
..Default::default()
};
assert!(ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_unavailable_just_below_cutoff() {
// VTE 0.81.99 — anything below 8200 must return true.
let ctx = TerminalContext {
vte_version: Some("8199".to_owned()),
..Default::default()
};
assert!(ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_available_modern_vte() {
// VTE 0.82.0 — first release with KKP.
let ctx = TerminalContext {
vte_version: Some("8200".to_owned()),
..Default::default()
};
assert!(!ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_available_future_vte() {
// VTE 0.84.1 — well above the cutoff.
let ctx = TerminalContext {
vte_version: Some("8401".to_owned()),
..Default::default()
};
assert!(!ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_unavailable_vte_brand_no_version() {
// Brand detected as VTE but VTE_VERSION missing — conservative: old.
let ctx = TerminalContext {
brand: TerminalName::Vte,
vte_version: None,
..Default::default()
};
assert!(ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_unavailable_unparseable_version() {
// Garbage in VTE_VERSION — conservative: old.
let ctx = TerminalContext {
brand: TerminalName::Vte,
vte_version: Some("not-a-number".to_owned()),
..Default::default()
};
assert!(ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_available_kkp_terminals() {
// Terminals that negotiate the Kitty keyboard protocol (or, for Apple
// Terminal, recover the modifier via CoreGraphics) deliver a usable
// Shift+Enter and must never report it as unavailable.
for brand in [
TerminalName::Ghostty,
TerminalName::Kitty,
TerminalName::WezTerm,
TerminalName::AppleTerminal,
] {
let ctx = TerminalContext {
brand,
env_brand: brand, // lockstep with brand (no Windows refinement)
..Default::default()
};
assert!(
!ctx.shift_enter_unavailable(),
"{brand:?} must not report shift_enter_unavailable"
);
}
}
#[test]
fn shift_enter_unavailable_vscode_family() {
// VS Code's xterm.js (and VS Code-family / xterm.js IDE forks) never
// negotiate KKP, so Shift+Enter arrives as a bare CR identical to
// Enter. The UI must advertise Alt+Enter instead.
for brand in [
TerminalName::VsCode,
TerminalName::Cursor,
TerminalName::Windsurf,
TerminalName::Zed,
] {
let ctx = TerminalContext {
brand,
..Default::default()
};
assert!(
ctx.shift_enter_unavailable(),
"{brand:?} (xterm.js) must report shift_enter_unavailable"
);
}
}
#[test]
fn shift_enter_unavailable_unknown_no_multiplexer() {
// The common VS Code-over-SSH shape: TERM_PROGRAM isn't forwarded so
// the brand falls back to Unknown, and the pager skips KKP. Shift+Enter
// is indistinguishable from Enter — advertise Alt+Enter.
let ctx = TerminalContext::default();
assert_eq!(ctx.brand, TerminalName::Unknown);
assert_eq!(ctx.env_brand, TerminalName::Unknown);
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
assert!(ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_unavailable_windows_refined_brand_stays_env_unknown() {
// Native Windows DefTerm / bare ConHost: `brand` is refined to WT for
// capabilities/label, but `env_brand` stays Unknown. KKP is skipped
// (WT is in the skip list; ConHost has no KKP either), so Shift+Enter
// is unreliable — advertise Alt+Enter, same as an unrefined Unknown.
let ctx = TerminalContext {
brand: TerminalName::WindowsTerminal,
env_brand: TerminalName::Unknown,
multiplexer: MultiplexerKind::Undetected,
..Default::default()
};
assert!(ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_available_positively_detected_windows_terminal() {
// WT detected via WT_SESSION/TERM_PROGRAM: both brand fields are WT.
// The Unknown gate must not fire; Shift+Enter follows the WT path
// (KKP is skipped for WT, but that is a separate, pre-existing choice).
let ctx = TerminalContext {
brand: TerminalName::WindowsTerminal,
env_brand: TerminalName::WindowsTerminal,
multiplexer: MultiplexerKind::Undetected,
..Default::default()
};
assert!(!ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_available_unknown_with_multiplexer() {
// Unknown brand but inside modern tmux: KKP is negotiated through the
// multiplexer, so Shift+Enter works and must not be flagged.
let ctx = TerminalContext {
brand: TerminalName::Unknown,
env_brand: TerminalName::Unknown,
multiplexer: MultiplexerKind::Tmux,
tmux_version: Some("tmux 3.4".to_owned()),
..Default::default()
};
assert!(!ctx.shift_enter_unavailable());
}
#[test]
fn ctrl_dot_unreliable_on_vte() {
let ctx = TerminalContext {
brand: TerminalName::Vte,
..Default::default()
};
assert!(ctx.ctrl_dot_unreliable());
}
#[test]
fn ctrl_dot_unreliable_when_vte_version_set() {
let ctx = TerminalContext {
vte_version: Some("7402".to_owned()),
..Default::default()
};
assert!(ctx.ctrl_dot_unreliable());
}
#[test]
fn ctrl_dot_reliable_on_kitty() {
let ctx = TerminalContext {
brand: TerminalName::Kitty,
..Default::default()
};
assert!(!ctx.ctrl_dot_unreliable());
}
#[test]
fn ctrl_dot_unreliable_on_windows_terminal() {
// WT propagates WT_SESSION into WSL via WSLENV, so this also covers
// WSL-inside-WT — `Ctrl+.` can't survive WT's ConPTY pipeline either
// way. (Pure WSL with no WT_SESSION is caught by `is_wsl()` in the
// consumer, not here.)
let ctx = TerminalContext {
brand: TerminalName::WindowsTerminal,
..Default::default()
};
assert!(ctx.ctrl_dot_unreliable());
}
#[test]
fn ctrl_dot_unreliable_on_vscode_forks() {
// VS Code's integrated terminal swallows `Ctrl+.` for its own command
// palette. VS Code-family forks and other xterm.js IDE embeds inherit
// the same behavior (forks share the keymap; others mirror it).
// Preference is keyed off `kitty_skip_reason` ("vscode").
for brand in [
TerminalName::VsCode,
TerminalName::Cursor,
TerminalName::Windsurf,
TerminalName::Zed,
] {
let ctx = TerminalContext {
brand,
..Default::default()
};
assert!(
ctx.ctrl_dot_unreliable(),
"expected ctrl_dot_unreliable on {brand:?}"
);
}
}
#[test]
fn ctrl_dot_unreliable_when_tmux_extended_keys_off() {
// iTerm2+tmux still used to advertise Ctrl+. while KKP was
// skipped for `extended-keys off`. Preference must follow the skip.
let ctx = TerminalContext {
brand: TerminalName::Iterm2,
tmux_version: Some("tmux 3.6a".to_owned()),
tmux_extended_keys: Some("off".to_owned()),
..tmux_ctx()
};
assert_eq!(ctx.kitty_skip_reason(), Some("tmux_extended_keys_off"));
assert!(ctx.ctrl_dot_unreliable());
}
#[test]
fn ctrl_dot_reliable_on_iterm2_tmux_extended_keys_on() {
let ctx = TerminalContext {
brand: TerminalName::Iterm2,
tmux_version: Some("tmux 3.6a".to_owned()),
tmux_extended_keys: Some("on".to_owned()),
..tmux_ctx()
};
assert_eq!(ctx.kitty_skip_reason(), None);
assert!(!ctx.ctrl_dot_unreliable());
}
#[test]
fn ctrl_dot_unreliable_on_unknown_no_multiplexer() {
// Same KKP-skip path as VS Code SSH / unbranded hosts.
let ctx = TerminalContext {
brand: TerminalName::Unknown,
multiplexer: MultiplexerKind::Undetected,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("unknown_no_multiplexer"));
assert!(ctx.ctrl_dot_unreliable());
}
// -- tmux extended-keys interaction with kitty_skip_reason ---------------
fn extended_keys_ctx(version: &str, extended_keys: Option<&str>) -> TerminalContext {
TerminalContext {
tmux_version: Some(version.to_owned()),
tmux_extended_keys: extended_keys.map(str::to_owned),
..tmux_ctx()
}
}
#[test]
fn kitty_skip_modern_tmux_extended_keys_off() {
assert_eq!(
extended_keys_ctx("tmux 3.4", Some("off")).kitty_skip_reason(),
Some("tmux_extended_keys_off"),
);
}
#[test]
fn kitty_allowed_modern_tmux_extended_keys_non_off_values() {
// `on`/`always`/empty/uppercase/None must all NOT trigger the skip:
// the comparison against `"off"` is exact and case-sensitive.
for val in [Some("on"), Some("always"), Some(""), Some("OFF"), None] {
assert_eq!(
extended_keys_ctx("tmux 3.4", val).kitty_skip_reason(),
None,
"value {val:?} must not trigger skip",
);
}
}
#[test]
fn kitty_skip_old_tmux_takes_precedence_over_extended_keys() {
assert_eq!(
extended_keys_ctx("tmux 3.2", Some("off")).kitty_skip_reason(),
Some("tmux_old"),
);
}
#[test]
fn kitty_skip_vte_takes_precedence_over_tmux_extended_keys_off() {
// VTE strips kitty CSI-u so `/terminal-setup` should recommend
// switching terminal emulators rather than editing tmux.conf.
let mut ctx = extended_keys_ctx("tmux 3.4", Some("off"));
ctx.vte_version = Some("7402".to_owned());
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
}
#[test]
fn kitty_skip_vte_takes_precedence_over_screen() {
let ctx = TerminalContext {
multiplexer: MultiplexerKind::Screen,
vte_version: Some("7402".to_owned()),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
}
#[test]
fn kitty_skip_vte_takes_precedence_over_tmux_old() {
let mut ctx = extended_keys_ctx("tmux 3.2", None);
ctx.vte_version = Some("7402".to_owned());
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
}
// =====================================================================
// JetBrains JediTerm detection
// =====================================================================
#[test]
fn brand_jetbrains_from_terminal_emulator() {
let env = env_from(&[("TERMINAL_EMULATOR", "JetBrains-JediTerm")]);
assert_eq!(
detect_terminal_brand_from_env(&env),
TerminalName::JetBrains
);
}
#[test]
fn brand_jetbrains_case_insensitive() {
let env = env_from(&[("TERMINAL_EMULATOR", "jetbrains-jediterm")]);
assert_eq!(
detect_terminal_brand_from_env(&env),
TerminalName::JetBrains
);
}
#[test]
fn brand_jetbrains_beats_term_session_id() {
// JediTerm on some platforms may set TERM_SESSION_ID, which would
// otherwise be detected as Apple Terminal. JetBrains must win.
let env = env_from(&[
("TERMINAL_EMULATOR", "JetBrains-JediTerm"),
("TERM_SESSION_ID", "w0t0p0:ABC123"),
]);
assert_eq!(
detect_terminal_brand_from_env(&env),
TerminalName::JetBrains
);
}
#[test]
fn context_jetbrains_terminal() {
let env = env_from(&[("TERMINAL_EMULATOR", "JetBrains-JediTerm")]);
let ctx = build_terminal_context_from_env(&env);
assert_eq!(ctx.brand, TerminalName::JetBrains);
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
assert!(!ctx.is_ssh);
}
#[test]
fn kitty_skip_jetbrains() {
let ctx = TerminalContext {
brand: TerminalName::JetBrains,
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("jetbrains"));
}
#[test]
fn ctrl_dot_unreliable_on_jetbrains() {
let ctx = TerminalContext {
brand: TerminalName::JetBrains,
..Default::default()
};
assert!(ctx.ctrl_dot_unreliable());
}
#[test]
fn repaints_pane_out_of_band_per_arm() {
// Embedded-editor :terminal (libvterm) can repaint our pane with no PTY resize.
let editor = TerminalContext {
embedded_editor: Some(EmbeddedEditor::Neovim),
multiplexer: MultiplexerKind::Undetected,
..Default::default()
};
assert!(editor.repaints_pane_out_of_band());
// Each multiplexer arm fires independently.
for mux in [
MultiplexerKind::Tmux,
MultiplexerKind::Screen,
MultiplexerKind::Zellij,
] {
let ctx = TerminalContext {
multiplexer: mux,
..Default::default()
};
assert!(
ctx.repaints_pane_out_of_band(),
"multiplexer {mux:?} should report out-of-band repaint"
);
}
// Plain terminal: no editor, no multiplexer -> no heal (no flicker regression).
let plain = TerminalContext {
embedded_editor: None,
multiplexer: MultiplexerKind::Undetected,
..Default::default()
};
assert!(!plain.repaints_pane_out_of_band());
}