#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::error::Error;
use std::fmt::Write as _;
use std::path::Path;
use tmux_mcp::{Selection, TmuxTools};
type TestResult<T = ()> = Result<T, Box<dyn Error>>;
fn generated_reference() -> TestResult<String> {
let mut output = String::from(concat!(
"# tmux MCP tool reference\n\n",
"Generated from the authoritative native registry. Do not edit this file by hand.\n",
"Refresh it with `LIBTMUX_RERECORD=1 cargo test --locked -p tmux-mcp --test docs generated_tool_reference_is_current`.\n",
"\n## Reading retained pane output\n\n",
"Reading old output does not require copy mode. `capture_pane` reads the visible\n",
"screen by default; set `history: true` to include retained scrollback. Use its\n",
"`start` and `end` bounds when only a known range is relevant.\n\n",
"`snapshot_pane` returns terminal content with cursor, geometry, and mode metadata\n",
"in one MCP call. The state query and capture are separate tmux operations, so the\n",
"result is not an atomic view of a pane that changes during the call.\n\n",
"`search_panes` searches visible screens by default and retained scrollback with\n",
"`history: true`. It is the bounded way to find which pane contains known text\n",
"without capturing every pane into the client's context.\n\n",
"Use `capture_since` for later output. Keep its opaque cursor and check `missed`;\n",
"a gap can mean the pane outran the buffer, its live tail was evicted, or the\n",
"server restarted. The current text is then not complete history. Cursors observe\n",
"output without changing pane state.\n",
"\n## Respecting human-owned pane modes\n\n",
"`snapshot_pane` reports `in_mode`, `mode`, and `scroll_position` when tmux owns\n",
"the pane's input. These fields are observations, not permission to change the\n",
"attached person's view, selection, key table, or clipboard state.\n\n",
"The MCP does not enter or cancel pane modes. Enter/exit pairs have unclear\n",
"ownership when a client disconnects, and cancelling can discard a person's\n",
"selection or close a different mode than the caller assumed.\n\n",
"Keep observing with capture, snapshot, search, `capture_since`, or\n",
"`wait_for_text`. Pane-input tools refuse dead, input-disabled, mode-owned,\n",
"terminal-attended, or possible caller recipients. `send_keys_batch` repeats\n",
"that check for each executed row, while `paste_text` checks only its named\n",
"target before buffer setup and again before paste.\n\n",
"`run_shell_command` requires one configured recipient and checks its cohort,\n",
"mode, liveness, input-off state, terminal attention, inherited-caller relation,\n",
"and foreground command before watcher setup and before dispatch. Its resolved\n",
"tmux executable and socket path must contain no ASCII terminal-control bytes.\n",
"These are observations, not locks: state can still change before tmux processes\n",
"input, and returned pane IDs do not confirm delivery.\n\n",
"The `libtmux` crate retains `Pane::copy_mode` and `Pane::exit_mode` for\n",
"applications that own the complete interaction. Library parity does not require\n",
"the detached MCP surface to expose modal human-client operations.\n",
));
append_tools(&mut output)?;
Ok(output)
}
fn append_tools(output: &mut String) -> TestResult {
let tools = TmuxTools::builder(libtmux::Server::new()?)
.selection(Selection::parse(
Some("inspect,manage,execute,teardown"),
None,
None,
)?)
.build();
for tool in tools.offered() {
let row = tool
.meta
.as_ref()
.and_then(|meta| meta.0.get("com.git-pull.libtmux-mcp/capability"))
.expect("capability row");
writeln!(output, "\n## `{}`\n", tool.name)?;
writeln!(
output,
"{}\n",
tool.description.as_deref().expect("controlled description")
)?;
writeln!(output, "- Toolset: `{}`", row["toolset"].as_str().unwrap())?;
writeln!(
output,
"- Process reach: `{}`",
row["processReach"].as_str().unwrap()
)?;
writeln!(
output,
"- Tmux effects: `{}`",
serde_json::to_string(&row["tmuxEffects"])?
)?;
writeln!(
output,
"- Output classes: `{}`",
serde_json::to_string(&row["outputClasses"])?
)?;
writeln!(
output,
"- May expose secrets: `{}`",
row["mayExposeSecrets"].as_bool().unwrap()
)?;
writeln!(
output,
"- May return untrusted content: `{}`",
row["mayReturnUntrustedContent"].as_bool().unwrap()
)?;
let mut hints = serde_json::to_value(tool.annotations.as_ref().expect("annotations"))?;
hints.as_object_mut().expect("hint object").remove("title");
writeln!(
output,
"- Whole-call annotations: `{}`",
serde_json::to_string(&hints)?
)?;
writeln!(
output,
"- Input literalization: `{}`",
serde_json::to_string(&row["inputLiteralization"])?
)?;
writeln!(
output,
"- Nested authority: `{}`",
serde_json::to_string(&row["nestedAuthority"])?
)?;
writeln!(
output,
"- Amplifies future input: `{}`",
row["amplifiesFutureInput"].as_bool().unwrap()
)?;
}
Ok(())
}
#[test]
fn generated_tool_reference_is_current() -> TestResult {
let expected = generated_reference()?;
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("TOOLS.md");
if std::env::var_os("LIBTMUX_RERECORD").is_some() {
std::fs::write(&path, &expected)?;
}
let actual = std::fs::read_to_string(path).unwrap_or_default();
assert_eq!(
actual, expected,
"set LIBTMUX_RERECORD=1 to refresh TOOLS.md"
);
Ok(())
}
#[test]
fn readme_links_to_the_generated_reference_without_copying_the_inventory() -> TestResult {
let readme = std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("README.md"))?;
assert!(readme.contains("[generated tool reference](TOOLS.md)"));
assert!(!readme.contains("**Inspect (18):**"));
assert!(readme.contains("configured recipient cohort"));
assert!(readme.contains("trusted POSIX-compatible pane shell"));
assert!(readme.contains("may be the inherited caller"));
assert!(readme.contains("do not prove delivery"));
Ok(())
}