Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 83 additions & 16 deletions crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,24 +673,30 @@ fn resolve_slash_name(cmd: &str) -> String {
///
/// List-only commands (`/sessions`, `/mcp`, `/plugin list`, …) stay on the
/// captured path so their output lands in the modern transcript.
/// Commands that take over the terminal (pickers, pagers, `$EDITOR`).
///
/// Exposed so tests can assert every one of them is actually registered
/// in `COMMANDS` — a restated copy of this list would drift the moment a
/// name is added here, which is the drift the test exists to catch.
pub const INTERACTIVE_SLASH_NAMES: &[&str] = &[
// Session picker (not `/sessions`, which only prints a list).
"session",
// Scrollback pager.
"scroll",
// `$EDITOR` owners — must keep a real TTY on stdout (no pipe tee).
"editor",
"open",
// Theme / model / tutorial pickers.
"theme",
"model",
"powerup",
// Full interactive uninstall flow.
"uninstall",
];

pub fn is_interactive_slash(cmd: &str) -> bool {
let name = resolve_slash_name(cmd);
matches!(
name.as_str(),
// Session picker (not `/sessions`, which only prints a list).
"session"
// Scrollback pager.
| "scroll"
// `$EDITOR` owners — must keep a real TTY on stdout (no pipe tee).
| "editor"
| "open"
// Theme / model / tutorial pickers.
| "theme"
| "model"
| "powerup"
// Full interactive uninstall flow.
| "uninstall"
) || slash_needs_stdin_prompt(cmd)
INTERACTIVE_SLASH_NAMES.contains(&name.as_str()) || slash_needs_stdin_prompt(cmd)
}

/// True when the interactive slash must keep a real TTY on stdout (no pipe
Expand Down Expand Up @@ -6265,6 +6271,67 @@ fn execute_settings(args: Option<&str>, engine: &QueryEngine) {

#[cfg(test)]
mod tests {
/// Every `/command` the reference documents must actually exist.
///
/// This repo has shipped several advertised-but-absent features —
/// a theme picker that was unreachable, `/vim` that set a key nothing
/// read, keybindings listed as active that never fired. Docs are the
/// promise; this makes the promise checkable.
///
/// Commands are extracted from the table rows, so prose mentioning a
/// slash command in passing does not count as a claim.
#[test]
fn every_documented_command_exists() {
let doc = include_str!("../../../../docs/reference/commands.mdx");
let mut claimed: Vec<String> = Vec::new();
for line in doc.lines() {
let line = line.trim_start();
// Table rows only: `| `/foo` | description |`
if !line.starts_with("| `/") {
continue;
}
let Some(rest) = line.strip_prefix("| `/") else {
continue;
};
let Some(name) = rest.split('`').next() else {
continue;
};
// `/model <name>` documents the command `model`.
let head = name.split_whitespace().next().unwrap_or("");
if !head.is_empty() && !claimed.iter().any(|c| c == head) {
claimed.push(head.to_string());
}
}
assert!(
claimed.len() > 20,
"extraction found only {} commands — the table format probably changed, \
which would make this test vacuous",
claimed.len()
);

// Registration in COMMANDS is what makes `execute` dispatch a
// command. `is_interactive_slash` only classifies how a name is
// run, and it carries its own hard-coded list — so accepting it
// as proof of implementation let a command stay "documented and
// implemented" after its COMMANDS entry was dropped or misspelled.
let missing: Vec<&String> = claimed.iter().filter(|c| !is_builtin_command(c)).collect();
assert!(
missing.is_empty(),
"documented but not registered in COMMANDS: {missing:?}"
);

// Every interactive-classified name must be registered too, or
// `execute` never reaches it. Read the classifier's own list
// rather than restating a subset: a copy here would silently
// stop covering names added there.
for name in INTERACTIVE_SLASH_NAMES {
assert!(
is_builtin_command(name),
"/{name} is classified interactive but is not registered in COMMANDS"
);
}
}

use super::*;

// Serialize tests that mutate the global VISUAL/EDITOR env vars so
Expand Down
Loading