pub mod discovery;
mod groups;
pub mod traits;
pub mod user_commands;
pub mod user_registry;
#[cfg(test)]
#[path = "epic_dispatch_acceptance.rs"]
mod epic_dispatch_acceptance;
#[cfg(test)]
#[path = "epic_discovery_acceptance.rs"]
mod epic_discovery_acceptance;
use std::sync::OnceLock;
pub use traits::CommandInfo;
pub use groups::project::share;
#[cfg(test)]
pub(crate) use groups::session::rename_with_manager as rename_session_with_manager;
pub use groups::core::voice;
use crate::tui::app::{App, AppAction};
#[derive(Debug, Clone)]
pub struct CommandResult {
pub message: Option<String>,
pub action: Option<AppAction>,
pub is_error: bool,
}
impl CommandResult {
pub fn ok() -> Self {
Self {
message: None,
action: None,
is_error: false,
}
}
pub fn message(msg: impl Into<String>) -> Self {
Self {
message: Some(msg.into()),
action: None,
is_error: false,
}
}
pub fn action(action: AppAction) -> Self {
Self {
message: None,
action: Some(action),
is_error: false,
}
}
pub fn with_message_and_action(msg: impl Into<String>, action: AppAction) -> Self {
Self {
message: Some(msg.into()),
action: Some(action),
is_error: false,
}
}
pub fn error(msg: impl Into<String>) -> Self {
Self {
message: Some(format!("Error: {}", msg.into())),
action: None,
is_error: true,
}
}
}
static REGISTRY: OnceLock<traits::CommandRegistry> = OnceLock::new();
fn build_registry() -> traits::CommandRegistry {
let mut registry = traits::CommandRegistry::empty();
for &group in groups::all_command_groups() {
registry.register_group(group);
}
registry
}
pub fn registry() -> &'static traits::CommandRegistry {
REGISTRY.get_or_init(build_registry)
}
pub fn command_infos() -> Vec<&'static CommandInfo> {
registry().infos()
}
pub fn get_command_info(name: &str) -> Option<&'static CommandInfo> {
registry().get_info(name)
}
pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
let dispatch_input = cmd.trim_start();
let command_token_end = dispatch_input
.find(char::is_whitespace)
.unwrap_or(dispatch_input.len());
let raw_remainder = &dispatch_input[command_token_end..];
let trimmed = cmd.trim();
if let Some(skill_input) = trimmed.strip_prefix('$') {
let skill_input = skill_input.trim_start();
if skill_input.is_empty() {
return CommandResult::error(
"Type a skill name after $. For example: $getting-started",
);
}
let parts: Vec<&str> = skill_input.splitn(2, char::is_whitespace).collect();
let skill_name = parts.first().copied().unwrap_or("");
let arg = parts
.get(1)
.map(|value| value.trim())
.filter(|value| !value.is_empty());
if let Some(result) = groups::skills::run_skill_by_name(app, skill_name, arg) {
return result;
}
return CommandResult::error(format!(
"Unknown skill: ${skill_name}. Type /skills to see installed skills."
));
}
let parts: Vec<&str> = trimmed.splitn(2, char::is_whitespace).collect();
let command = parts
.first()
.copied()
.unwrap_or_default()
.trim_start_matches('/')
.to_ascii_lowercase();
let arg = parts
.get(1)
.map(|value| value.trim())
.filter(|value| !value.is_empty());
if let Some(result) = user_registry::try_dispatch(app, trimmed) {
return result;
}
match command.as_str() {
"jihua" => {
return groups::config::dispatch(app, "jihua", arg).unwrap_or_else(|| {
CommandResult::error("The /jihua alias could not be dispatched.")
});
}
"zidong" => {
return groups::config::dispatch(app, "zidong", arg).unwrap_or_else(|| {
CommandResult::error("The /zidong alias could not be dispatched.")
});
}
_ => {}
}
if let Some(command_object) = registry().get(command.as_str()) {
let command_arg = if command_object.info().name == "preview-request" {
Some(raw_remainder)
} else {
arg
};
return command_object.execute(app, command_arg);
}
match command.as_str() {
"set" => CommandResult::error(
"The /set command was retired. Use /config to edit settings and /settings to inspect current values.",
),
"deepseek" => CommandResult::error(
"The /deepseek command was renamed. Use /links (aliases: /dashboard, /api).",
),
"doctor" => CommandResult::error(
"The /doctor command is a CLI diagnostic. Run `codewhale doctor` or `codewhale doctor --json`; use `/setup` in the TUI for readiness and verification.",
),
_ => {
if let Some(result) = groups::skills::run_skill_by_name(app, command.as_str(), arg) {
return result;
}
let suggestions =
user_registry::with_registry_for_workspace(Some(&app.workspace), |user_commands| {
suggest_command_names(command.as_str(), 3, user_commands)
});
if suggestions.is_empty() {
CommandResult::error(format!(
"Unknown command: /{command}. Type /help for available commands."
))
} else {
let list = suggestions
.into_iter()
.map(|name| format!("/{name}"))
.collect::<Vec<_>>()
.join(", ");
CommandResult::error(format!(
"Unknown command: /{command}. Did you mean: {list}? Type /help for available commands."
))
}
}
}
}
pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> CommandResult {
groups::config::config::set_config_value(app, key, value, persist)
}
pub fn switch_mode(app: &mut App, mode: crate::tui::app::AppMode) -> String {
groups::config::config::switch_mode(app, mode)
}
fn edit_distance(a: &str, b: &str) -> usize {
if a == b {
return 0;
}
if a.is_empty() {
return b.chars().count();
}
if b.is_empty() {
return a.chars().count();
}
let b_chars: Vec<char> = b.chars().collect();
let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
let mut current = vec![0usize; b_chars.len() + 1];
for (i, a_ch) in a.chars().enumerate() {
current[0] = i + 1;
for (j, b_ch) in b_chars.iter().enumerate() {
let cost = if a_ch == *b_ch { 0 } else { 1 };
let delete = previous[j + 1] + 1;
let insert = current[j] + 1;
let substitute = previous[j] + cost;
current[j + 1] = delete.min(insert).min(substitute);
}
std::mem::swap(&mut previous, &mut current);
}
previous[b_chars.len()]
}
fn best_suggestion_score<'a>(
query: &str,
candidates: impl IntoIterator<Item = &'a str>,
) -> Option<(u8, usize)> {
let mut best: Option<(u8, usize)> = None;
for candidate in candidates {
let prefix_match = candidate.starts_with(query) || query.starts_with(candidate);
let contains_match = candidate.contains(query) || query.contains(candidate);
let distance = edit_distance(candidate, query);
let close_typo = distance <= 2;
if !(prefix_match || contains_match || close_typo) {
continue;
}
let rank = if prefix_match {
0
} else if contains_match {
1
} else {
2
};
match best {
Some((best_rank, best_distance))
if rank > best_rank || (rank == best_rank && distance >= best_distance) => {}
_ => best = Some((rank, distance)),
}
}
best
}
fn suggest_command_names(
input: &str,
limit: usize,
user_commands: &user_registry::UserCommandRegistry,
) -> Vec<String> {
let query = input.trim().to_ascii_lowercase();
if query.is_empty() || limit == 0 {
return Vec::new();
}
let mut scored: Vec<(u8, usize, String)> = Vec::new();
for command in registry().infos() {
if user_commands.get(command.name).is_some() {
continue;
}
let candidates = std::iter::once(command.name).chain(
command
.aliases
.iter()
.copied()
.filter(|alias| user_commands.get(alias).is_none()),
);
if let Some((rank, distance)) = best_suggestion_score(&query, candidates) {
scored.push((rank, distance, command.name.to_string()));
}
}
for command in user_commands.iter().filter(|command| !command.hidden) {
let candidates = std::iter::once(command.name.as_str()).chain(
command.aliases.iter().map(String::as_str).filter(|alias| {
user_commands
.get(alias)
.is_some_and(|resolved| resolved.name == command.name)
}),
);
if let Some((rank, distance)) = best_suggestion_score(&query, candidates) {
scored.push((rank, distance, command.name.clone()));
}
}
scored.sort_by(|a, b| {
a.0.cmp(&b.0)
.then_with(|| a.1.cmp(&b.1))
.then_with(|| a.2.cmp(&b.2))
});
scored
.into_iter()
.take(limit)
.map(|(_, _, name)| name)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{ApiProvider, Config};
use crate::localization::{Locale, MessageId};
use crate::tools::plan::{PlanItemArg, StepStatus, UpdatePlanArgs};
use crate::tools::todo::TodoStatus;
use crate::tui::app::{App, AppAction, TuiOptions};
use crate::tui::work_surface::{RailPanel, WorkSurfacePlacement};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use tempfile::tempdir;
fn is_palette_safe_command_name(name: &str) -> bool {
let bytes = name.as_bytes();
!bytes.is_empty()
&& bytes.first().is_some_and(u8::is_ascii_alphanumeric)
&& bytes.last().is_some_and(u8::is_ascii_alphanumeric)
&& bytes
.iter()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
&& !name.contains("--")
}
fn create_test_app() -> App {
let options = TuiOptions {
..crate::test_support::test_tui_options(PathBuf::from("."))
};
App::new(options, &Config::default())
}
#[test]
fn user_registry_module_is_compiled() {
super::user_registry::reload(None);
let registry = super::user_registry::current_registry();
assert!(registry.is_valid());
}
#[test]
fn preview_request_dispatch_preserves_prompt_edge_bytes() {
let mut app = create_test_app();
let result = execute("/preview-request --prompt lead\ntrail ", &mut app);
assert!(!result.is_error, "{result:?}");
assert!(matches!(
result.action,
Some(AppAction::PreviewOutboundRequest {
json: false,
base_prompt_only: false,
hypothetical_prompt,
}) if hypothetical_prompt.as_deref() == Some(" lead\ntrail ")
));
}
#[test]
fn user_command_shadows_builtin_before_group_dispatch() {
let temp = tempdir().unwrap();
let commands_dir = temp.path().join(".codewhale").join("commands");
std::fs::create_dir_all(&commands_dir).unwrap();
std::fs::write(
commands_dir.join("help.md"),
"---\ndescription: User help\n---\nuser help $ARGUMENTS",
)
.unwrap();
let mut app = create_test_app();
app.workspace = temp.path().to_path_buf();
super::user_registry::reload(Some(temp.path()));
let result = execute("/help now", &mut app);
assert!(!result.is_error);
match result.action {
Some(AppAction::SendMessage(message)) => assert_eq!(message, "user help now"),
other => panic!("expected user command SendMessage action, got {other:?}"),
}
}
#[test]
fn removed_user_command_reloads_and_falls_back_to_builtin() {
let temp = tempdir().unwrap();
let commands_dir = temp.path().join(".codewhale").join("commands");
std::fs::create_dir_all(&commands_dir).unwrap();
let command_path = commands_dir.join("help.md");
std::fs::write(&command_path, "user help").unwrap();
let mut app = create_test_app();
app.workspace = temp.path().to_path_buf();
super::user_registry::reload(Some(temp.path()));
assert!(matches!(
execute("/help config", &mut app).action,
Some(AppAction::SendMessage(_))
));
std::fs::remove_file(command_path).unwrap();
super::user_registry::reload(Some(temp.path()));
let result = execute("/help config", &mut app);
assert!(!result.is_error);
assert!(
result
.message
.as_deref()
.is_some_and(|message| message.contains("config")),
"built-in /help should handle the command"
);
assert!(result.action.is_none());
}
#[test]
fn command_registry_contains_config_and_links_but_not_set_or_deepseek() {
assert!(command_infos().iter().any(|cmd| cmd.name == "config"));
let rail = command_infos()
.into_iter()
.find(|cmd| cmd.name == "rail")
.expect("rail command should exist");
assert_eq!(rail.aliases, &["sidebar"]);
assert_eq!(rail.description_id, MessageId::CmdSidebarDescription);
assert!(rail.description_for(Locale::En).contains("rail"));
assert!(command_infos().iter().any(|cmd| cmd.name == "links"));
let hf = command_infos()
.into_iter()
.find(|cmd| cmd.name == "hf")
.expect("hf command should exist");
assert_eq!(hf.aliases, &["huggingface"]);
assert_eq!(hf.description_id, MessageId::CmdHfDescription);
assert!(hf.description_for(Locale::En).contains("Hugging Face"));
assert!(command_infos().iter().any(|cmd| cmd.name == "memory"));
assert!(!command_infos().iter().any(|cmd| cmd.name == "set"));
assert!(!command_infos().iter().any(|cmd| cmd.name == "deepseek"));
}
#[test]
fn links_command_has_dashboard_and_api_aliases() {
let links = command_infos()
.into_iter()
.find(|cmd| cmd.name == "links")
.expect("links command should exist");
assert_eq!(links.aliases, &["dashboard", "api", "lianjie"]);
}
#[test]
fn transcript_command_is_discoverable_and_opens_live_overlay() {
let transcript = command_infos()
.into_iter()
.find(|cmd| cmd.name == "transcript")
.expect("transcript command should exist");
assert_eq!(transcript.usage, "/transcript");
assert!(transcript.show_in_empty_discovery());
let mut app = create_test_app();
let result = execute("/transcript", &mut app);
assert!(!result.is_error);
assert!(matches!(result.action, Some(AppAction::OpenLiveTranscript)));
}
#[test]
fn hf_alias_dispatches_to_concepts_helper() {
let mut app = create_test_app();
let result = execute("/huggingface concepts", &mut app);
assert!(!result.is_error);
let message = result.message.expect("concepts message");
assert!(message.contains("Hugging Face provider route"));
assert!(message.contains("Hugging Face MCP"));
assert!(message.contains("Hub workflows"));
}
#[test]
fn xai_device_auth_slash_command_starts_login() {
let mut app = create_test_app();
let result = execute("/auth xai-device", &mut app);
assert!(!result.is_error);
assert!(matches!(
result.action,
Some(AppAction::StartXaiDeviceLogin)
));
}
#[test]
fn rlm_slash_command_routes_to_persistent_tool_instruction() {
let mut app = create_test_app();
let result = execute("/rlm 2 inspect this long corpus", &mut app);
assert!(!result.is_error);
assert!(
result
.message
.as_deref()
.unwrap_or("")
.contains("persistent working context")
);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("session-persistent working context"));
assert!(message.contains("Do not use legacy `rlm` tool actions"));
}
#[test]
fn kernel_is_not_a_command() {
let mut app = create_test_app();
let result = execute("/kernel inspect the fresh corpus", &mut app);
assert!(
result.is_error,
"/kernel must not resolve to a registered command"
);
}
#[test]
fn agent_slash_command_routes_to_persistent_tool_instruction() {
let mut app = create_test_app();
let result = execute("/agent 0 inspect the parser", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("`agent`"));
assert!(message.contains("max_depth: 0"));
}
#[test]
fn relay_slash_command_routes_to_session_relay_instruction() {
let mut app = create_test_app();
app.hunt.quarry = Some("Unify the work surface".to_string());
app.hunt.token_budget = Some(12_000);
{
let mut todos = app.todos.try_lock().expect("todo lock");
todos.add("inspect workspace".to_string(), TodoStatus::Completed);
todos.add("patch relay command".to_string(), TodoStatus::InProgress);
}
{
let mut plan = app.plan_state.try_lock().expect("plan lock");
plan.update(UpdatePlanArgs {
objective: Some("Keep relays grounded".to_string()),
explanation: Some("RLM-style strategy".to_string()),
sources_used: vec!["transcript context".to_string()],
critical_files: vec!["crates/tui/src/commands/mod.rs".to_string()],
constraints: vec!["Do not invent verification".to_string()],
verification_plan: Some("Check relay prompt assertions".to_string()),
handoff_packet: Some("Next thread should read the To-do list".to_string()),
plan: vec![PlanItemArg {
step: "keep To-do primary".to_string(),
status: StepStatus::InProgress,
}],
..UpdatePlanArgs::default()
});
}
let result = execute("/relay verify install", &mut app);
assert!(!result.is_error);
assert!(
result
.message
.as_deref()
.unwrap_or_default()
.contains(".deepseek/handoff.md")
);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("session relay"));
assert!(message.contains("接力"));
assert!(message.contains("Write or update `.deepseek/handoff.md`"));
assert!(message.contains("# Session relay"));
assert!(message.contains("Requested relay focus: verify install"));
assert!(message.contains("Goal objective: Unify the work surface"));
assert!(message.contains("Goal token budget: 12000"));
let expected_body = crate::todo_snapshot::todo_snapshot_body(
&app.todos.try_lock().expect("todo lock").snapshot(),
)
.expect("canonical body");
assert_eq!(
expected_body,
"To-do (50% settled)\n- [x] #1 inspect workspace\n- [~] #2 patch relay command"
);
assert!(
message.contains(&expected_body),
"relay must embed the canonical To-do body: {message}"
);
assert!(message.contains("Conversational strategy notes from update_plan"));
assert!(message.contains("Objective: Keep relays grounded"));
assert!(message.contains("Explanation: RLM-style strategy"));
assert!(message.contains("Source: transcript context"));
assert!(message.contains("Critical file: crates/tui/src/commands/mod.rs"));
assert!(message.contains("Constraint: Do not invent verification"));
assert!(message.contains("Verification plan: Check relay prompt assertions"));
assert!(message.contains("Handoff packet: Next thread should read the To-do list"));
assert!(message.contains("[in_progress] keep To-do primary"));
assert!(
!message.contains("Work checklist"),
"relay copy should use To-do vocabulary: {message}"
);
}
#[test]
fn relay_does_not_present_plan_only_state_as_work_state() {
let mut app = create_test_app();
{
let mut plan = app.plan_state.try_lock().expect("plan lock");
plan.update(UpdatePlanArgs {
objective: Some("Ship the To-do seam".to_string()),
plan: vec![PlanItemArg {
step: "draft the renderer".to_string(),
status: StepStatus::InProgress,
}],
..UpdatePlanArgs::default()
});
}
let result = execute("/relay", &mut app);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(
!message.contains("Current To-do:"),
"plan-only state must not render as a To-do: {message}"
);
assert!(
!message.contains("To-do ("),
"plan-only state must not synthesize a To-do list: {message}"
);
assert!(message.contains("Conversational strategy notes from update_plan"));
}
#[tokio::test]
async fn relay_reads_same_turn_graph_backed_work_update() {
use crate::tools::spec::ToolSpec as _;
let mut app = create_test_app();
let work =
crate::work_graph::new_shared_work_runtime(app.todos.clone(), app.plan_state.clone());
app.runtime_services.work = Some(work.clone());
let mut context = crate::tools::spec::ToolContext::new(app.workspace.clone());
context.runtime.work = Some(work);
crate::tools::todo::TodoWriteTool::new(app.todos.clone())
.execute(
serde_json::json!({
"todos": [{"content": "relay the staged graph", "status": "in_progress"}]
}),
&context,
)
.await
.expect("graph-backed todo_write");
assert!(
app.todos.lock().await.snapshot().is_empty(),
"precondition: legacy projection has not published yet"
);
let result = execute("/relay", &mut app);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(
message.contains("[~] #1 relay the staged graph"),
"{message}"
);
}
#[test]
fn relay_command_has_bilingual_aliases() {
let relay = command_infos()
.into_iter()
.find(|cmd| cmd.name == "relay")
.expect("relay command should exist");
assert_eq!(relay.aliases, &["batonpass", "接力"]);
assert!(relay.description_for(Locale::ZhHans).contains("接力"));
assert!(relay.description_for(Locale::ZhHant).contains("接力"));
let mut app = create_test_app();
let result = execute("/接力 next hand", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("Requested relay focus: next hand"));
}
#[test]
fn command_registry_has_unique_names_and_aliases() {
let mut names = std::collections::BTreeSet::new();
for command in command_infos() {
assert!(
names.insert(command.name),
"duplicate command name /{}",
command.name
);
}
let mut aliases = std::collections::BTreeSet::new();
for command in command_infos() {
for alias in command.aliases {
assert!(
!names.contains(alias),
"alias /{alias} collides with a command name"
);
assert!(aliases.insert(*alias), "duplicate command alias /{alias}");
}
}
}
#[test]
fn command_ownership_contract_is_enforced() {
let groups = groups::all_command_groups();
assert_eq!(
groups.len(),
9,
"expected exactly 9 command groups (core, session, config, debug, \
project, skills, memory, plugins, utility), got {}",
groups.len()
);
let mut total_commands = 0;
let mut has_config = false;
let mut has_debug = false;
for &group in groups {
let commands = group.commands();
assert!(
!commands.is_empty(),
"each group must have at least one command"
);
for cmd in commands {
let info = cmd.info();
assert!(!info.name.is_empty(), "command name must not be empty");
assert!(
is_palette_safe_command_name(info.name),
"/{} command names must be lowercase ASCII kebab-case",
info.name
);
let usage_prefix = format!("/{}", info.name);
assert!(
info.usage.starts_with(&usage_prefix),
"/{} usage must start with /{{name}}, got {:?}",
info.name,
info.usage
);
}
total_commands += commands.len();
if commands.iter().any(|c| c.info().name == "config") {
has_config = true;
assert_eq!(
commands.len(),
12,
"config group (group-local metadata exception) expected \
exactly 12 commands, got {}",
commands.len()
);
}
if commands.iter().any(|c| c.info().name == "tokens") {
has_debug = true;
assert_eq!(
commands.len(),
13,
"debug group (group-local metadata exception) expected \
exactly 13 commands, got {}",
commands.len()
);
}
}
assert!(
has_config,
"config group not found (expected first command: /config)"
);
assert!(
has_debug,
"debug group not found (expected first command: /tokens)"
);
assert_eq!(
total_commands,
command_infos().len(),
"group-iterated command count must match registry infos count"
);
}
#[test]
fn command_groups_are_cached_once() {
let first_groups = groups::all_command_groups();
let second_groups = groups::all_command_groups();
assert!(
std::ptr::eq(first_groups.as_ptr(), second_groups.as_ptr()),
"command group list should be cached"
);
for &group in first_groups {
let first_commands = group.commands();
let second_commands = group.commands();
assert!(
std::ptr::eq(first_commands.as_ptr(), second_commands.as_ptr()),
"command list should be cached per group"
);
}
}
#[test]
fn command_registry_metadata_is_complete_and_palette_safe() {
for command in command_infos() {
assert!(!command.name.is_empty(), "command name must not be empty");
assert_eq!(
command.name.trim(),
command.name,
"/{} command name must not need trimming",
command.name
);
assert!(
is_palette_safe_command_name(command.name),
"/{} command names must stay lowercase ASCII kebab-case",
command.name
);
let expected_usage_prefix = format!("/{}", command.name);
assert!(
command.usage.starts_with(&expected_usage_prefix),
"/{} usage must start with its canonical slash command, got {:?}",
command.name,
command.usage
);
let description = command.description_for(Locale::En);
assert!(
!description.trim().is_empty(),
"/{} must have non-empty English help text",
command.name
);
assert!(
!description.contains(command.usage),
"/{} description embeds its usage string {:?}: {description:?}",
command.name,
command.usage
);
assert!(
!description.contains(&format!("/{}", command.name)),
"/{} description embeds slash-command syntax that usage already covers: {description:?}",
command.name
);
for banned_prefix in ["Toolbox:", "Reference:"] {
assert!(
!description.starts_with(banned_prefix),
"/{} description should not start with {banned_prefix:?}: {description:?}",
command.name
);
}
let palette_command = command.palette_command();
assert!(
palette_command.starts_with(&expected_usage_prefix),
"/{} palette command must use the canonical command, got {:?}",
command.name,
palette_command
);
assert_eq!(
palette_command.ends_with(' '),
command.requires_argument(),
"/{} palette command spacing must match argument requirement",
command.name
);
for &alias in command.aliases {
assert!(
!alias.trim().is_empty(),
"/{} alias must not be empty",
command.name
);
assert_eq!(
alias.trim(),
alias,
"/{} alias /{alias} must not need trimming",
command.name
);
assert!(
!alias.starts_with('/'),
"/{} alias /{alias} must be stored without a slash",
command.name
);
assert!(
!alias.chars().any(char::is_whitespace),
"/{} alias /{alias} must not contain whitespace",
command.name
);
assert!(
!alias.chars().any(|ch| ch.is_ascii_uppercase()),
"/{} alias /{alias} must not contain uppercase ASCII",
command.name
);
}
}
}
#[test]
fn command_discovery_tier_lists_use_canonical_registered_names() {
for (tier_name, names) in [
("advanced", traits::ADVANCED_DISCOVERY_COMMANDS),
("compatibility", traits::COMPATIBILITY_DISCOVERY_COMMANDS),
] {
for &name in names {
let info = registry()
.get_info(name)
.unwrap_or_else(|| panic!("{tier_name} discovery entry {name:?} must resolve"));
assert_eq!(
info.name, name,
"{tier_name} discovery entry {name:?} must be canonical, not an alias for /{}",
info.name
);
}
}
}
#[test]
fn command_info_resolves_canonical_names_and_aliases() {
for command in command_infos() {
for lookup in [command.name.to_string(), format!("/{}", command.name)] {
let resolved = get_command_info(&lookup)
.unwrap_or_else(|| panic!("{lookup:?} should resolve to /{}", command.name));
assert_eq!(resolved.name, command.name);
}
for &alias in command.aliases {
for lookup in [alias.to_string(), format!("/{alias}")] {
let resolved = get_command_info(&lookup).unwrap_or_else(|| {
panic!("{lookup:?} should resolve to /{}", command.name)
});
assert_eq!(resolved.name, command.name);
}
}
}
}
#[test]
fn every_registered_command_has_a_help_topic() {
let mut app = create_test_app();
for command in command_infos() {
let result = execute(&format!("/help {}", command.name), &mut app);
assert!(
!result.is_error,
"/help {} returned an error: {result:?}",
command.name
);
let message = result
.message
.unwrap_or_else(|| panic!("/help {} should return text", command.name));
assert!(
message.contains(command.name),
"/help {} should mention the command name, got {message:?}",
command.name
);
assert!(
message.contains(command.usage),
"/help {} should include usage {:?}, got {message:?}",
command.name,
command.usage
);
}
}
#[test]
fn context_command_opens_inspector_and_keeps_ctx_alias() {
let context = command_infos()
.into_iter()
.find(|cmd| cmd.name == "context")
.expect("context command should exist");
assert_eq!(context.aliases, &["ctx"]);
assert!(context.description_for(Locale::En).contains("inspector"));
let mut app = create_test_app();
let result = execute("/ctx", &mut app);
assert!(matches!(
result.action,
Some(AppAction::OpenContextInspector)
));
let report = execute("/context report", &mut app);
let message = report.message.expect("context report should return text");
assert!(message.contains("Context Source Map"));
}
#[test]
fn cache_inspect_dispatches_through_cache_command() {
let mut app = create_test_app();
let result = execute("/cache inspect", &mut app);
let msg = result.message.expect("cache inspect should return text");
assert!(msg.contains("Cache Inspect"));
assert!(msg.contains("Base static prefix hash:"));
assert!(msg.contains("Full request prefix hash:"));
assert!(result.action.is_none());
}
#[test]
fn cache_warmup_dispatches_action() {
let mut app = create_test_app();
let result = execute("/cache warmup", &mut app);
assert!(result.message.is_none());
assert!(matches!(result.action, Some(AppAction::CacheWarmup)));
}
#[test]
fn execute_config_opens_config_view_action() {
let mut app = create_test_app();
let result = execute("/config", &mut app);
assert!(result.message.is_none());
assert!(matches!(result.action, Some(AppAction::OpenConfigView)));
}
#[test]
fn execute_verbose_toggles_live_transcript_detail() {
let mut app = create_test_app();
assert!(!app.verbose_transcript);
let result = execute("/verbose on", &mut app);
assert!(!result.is_error);
assert!(app.verbose_transcript);
assert!(result.message.unwrap().contains("on"));
let result = execute("/verbose off", &mut app);
assert!(!result.is_error);
assert!(!app.verbose_transcript);
assert!(result.message.unwrap().contains("off"));
}
#[test]
fn voice_send_and_voice_control_commands_toggle_state() {
let mut app = create_test_app();
assert!(!app.voice_send_enabled);
assert!(!app.voice_control_enabled);
for invocation in ["/voicesend", "/voice-send", "/yuyinsend", "/语音发送"] {
let result = execute(invocation, &mut app);
assert!(!result.is_error, "{invocation} should toggle cleanly");
assert!(result.action.is_none());
assert!(result.message.is_some());
}
assert!(!app.voice_send_enabled);
let result = execute("/voicecontrol", &mut app);
assert!(!result.is_error);
assert!(app.voice_control_enabled);
let result = execute("/voice-control", &mut app);
assert!(!result.is_error);
assert!(!app.voice_control_enabled);
}
#[test]
fn voice_command_toggles_on_and_off_or_fails_gracefully() {
let mut app = create_test_app();
let result = execute("/voice", &mut app);
if app.voice_enabled {
assert!(!result.is_error);
assert!(matches!(result.action, Some(AppAction::VoiceCapture)));
let off = execute("/voice", &mut app);
assert!(!off.is_error);
assert!(off.action.is_none());
assert!(!app.voice_enabled);
} else {
assert!(result.is_error);
assert!(result.action.is_none());
}
}
#[test]
fn execute_rail_sets_placement_and_reports_actual_state() {
let mut app = create_test_app();
let result = execute("/rail off", &mut app);
assert!(!result.is_error);
assert_eq!(app.work_surface.placement, WorkSurfacePlacement::Off);
assert!(
result
.message
.as_deref()
.unwrap_or_default()
.contains("Rail is off")
);
let result = execute("/rail right", &mut app);
assert!(!result.is_error);
assert_eq!(app.work_surface.placement, WorkSurfacePlacement::Right);
assert!(
result
.message
.as_deref()
.unwrap_or_default()
.contains("right placement")
);
let result = execute("/sidebar left", &mut app);
assert!(!result.is_error);
assert_eq!(app.work_surface.placement, WorkSurfacePlacement::Left);
app.work_surface.placement = WorkSurfacePlacement::Off;
let result = execute("/rail", &mut app);
assert!(!result.is_error);
assert!(
result
.message
.as_deref()
.unwrap_or_default()
.contains("Rail is off")
);
}
#[test]
fn execute_rail_accepts_panel_targets_and_legacy_words() {
let mut app = create_test_app();
let result = execute("/rail agents", &mut app);
assert!(!result.is_error);
assert_eq!(app.work_surface.panel, RailPanel::Agents);
let result = execute("/sidebar context", &mut app);
assert!(!result.is_error);
assert_eq!(app.work_surface.panel, RailPanel::Context);
let result = execute("/rail activity", &mut app);
assert!(!result.is_error);
assert_eq!(
app.work_surface.panel,
RailPanel::Tasks,
"activity maps onto the Tasks panel"
);
let result = execute("/rail pinned", &mut app);
assert!(!result.is_error);
assert_eq!(app.work_surface.panel, RailPanel::Pinned);
let result = execute("/sidebar on", &mut app);
assert!(!result.is_error);
assert_eq!(
app.work_surface.placement,
WorkSurfacePlacement::Top,
"on restores the default top rail"
);
let result = execute("/sidebar none", &mut app);
assert!(!result.is_error);
assert_eq!(app.work_surface.placement, WorkSurfacePlacement::Off);
}
#[test]
fn execute_rail_rejects_invalid_args() {
let mut app = create_test_app();
let result = execute("/rail maybe", &mut app);
assert!(result.is_error);
assert!(
result
.message
.as_deref()
.unwrap_or_default()
.contains("Usage: /rail")
);
}
#[test]
fn execute_links_and_aliases_return_links_message() {
let mut app = create_test_app();
for cmd in ["/links", "/dashboard", "/api", "/lianjie"] {
let result = execute(cmd, &mut app);
let msg = result.message.expect("links commands should return text");
assert!(msg.contains("https://codewhale.net/en/docs"));
assert!(msg.contains("https://codewhale.net/en/community"));
assert!(msg.contains("https://github.com/Hmbown/CodeWhale"));
assert!(msg.contains("https://app.codewhale.net"));
assert!(msg.contains("separate sign-in"));
assert!(msg.contains("not connected to the current local session"));
assert!(msg.contains("https://platform.deepseek.com"));
assert!(result.action.is_none());
}
}
#[test]
fn execute_workspace_alias_switches_workspace() {
let dir = tempdir().expect("temp dir");
let mut app = create_test_app();
let result = execute(&format!("/cwd {}", dir.path().display()), &mut app);
assert!(matches!(
result.action,
Some(AppAction::SwitchWorkspace { workspace }) if workspace == dir.path().canonicalize().unwrap()
));
}
#[test]
fn removed_set_and_deepseek_commands_show_migration_hints() {
let mut app = create_test_app();
let set_result = execute("/set model deepseek-v4-pro", &mut app);
let set_msg = set_result
.message
.expect("legacy command should return an error message");
assert!(set_msg.contains("The /set command was retired"));
assert!(set_msg.contains("/config"));
assert!(set_msg.contains("/settings"));
assert!(set_result.action.is_none());
let deepseek_result = execute("/deepseek", &mut app);
let deepseek_msg = deepseek_result
.message
.expect("legacy command should return an error message");
assert!(deepseek_msg.contains("The /deepseek command was renamed"));
assert!(deepseek_msg.contains("/links"));
assert!(deepseek_msg.contains("/dashboard"));
assert!(deepseek_msg.contains("/api"));
assert!(deepseek_result.action.is_none());
}
struct ConfigPathGuard {
previous: Option<OsString>,
_lock: crate::test_support::TestEnvLock,
}
impl ConfigPathGuard {
fn new(config_path: &Path) -> Self {
let lock = crate::test_support::lock_test_env();
let previous = std::env::var_os("DEEPSEEK_CONFIG_PATH");
unsafe {
std::env::set_var("DEEPSEEK_CONFIG_PATH", config_path);
}
Self {
previous,
_lock: lock,
}
}
}
impl Drop for ConfigPathGuard {
fn drop(&mut self) {
unsafe {
if let Some(previous) = self.previous.take() {
std::env::set_var("DEEPSEEK_CONFIG_PATH", previous);
} else {
std::env::remove_var("DEEPSEEK_CONFIG_PATH");
}
}
}
}
fn create_isolated_test_app() -> (App, tempfile::TempDir, ConfigPathGuard) {
let tmpdir = tempfile::TempDir::new().expect("tempdir for smoke test");
let workspace = tmpdir.path().to_path_buf();
let config_path = workspace.join(".deepseek").join("config.toml");
std::fs::create_dir_all(config_path.parent().expect("config parent")).expect("config dir");
let guard = ConfigPathGuard::new(&config_path);
let options = TuiOptions {
config_path: Some(config_path),
skills_dir: workspace.join("skills"),
memory_path: workspace.join("memory.md"),
notes_path: workspace.join("notes.txt"),
mcp_config_path: workspace.join("mcp.json"),
..crate::test_support::test_tui_options(workspace.clone())
};
let app = App::new(options, &Config::default());
(app, tmpdir, guard)
}
fn invocation_for(command_name: &str, alias_or_name: &str, tmpdir: &std::path::Path) -> String {
match command_name {
"save" => format!("/{alias_or_name} {}", tmpdir.join("session.json").display()),
"export" => format!("/{alias_or_name} {}", tmpdir.join("chat.md").display()),
_ => format!("/{alias_or_name}"),
}
}
fn skip_in_dispatch_smoke(name: &str) -> bool {
name == "restore"
}
#[test]
fn slash_parser_preserves_arguments_after_the_command_name() {
let mut app = create_test_app();
let result = execute("/agent 2 review this carefully", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected /agent to send a model instruction");
};
assert!(message.contains(r#"prompt: "review this carefully""#));
assert!(message.contains("max_depth: 2"));
let mut app = create_test_app();
let result = execute(" /relay ship command harness ", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected /relay to send a model instruction");
};
assert!(message.contains("Requested relay focus: ship command harness"));
let mut app = create_test_app();
let result = execute("/rlm 3 inspect this corpus", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected /rlm to send a model instruction");
};
assert!(message.contains(r#"this text: "inspect this corpus""#));
assert!(message.contains("session-persistent working context"));
}
#[test]
fn representative_command_groups_keep_dispatch_surfaces() {
let mut app = create_test_app();
let help = execute("/help clear", &mut app)
.message
.expect("/help clear should return text");
assert!(help.contains("clear"));
assert!(help.contains("/clear"));
let mut app = create_test_app();
let result = execute("/config", &mut app);
assert!(matches!(result.action, Some(AppAction::OpenConfigView)));
let mut app = create_test_app();
let result = execute("/relay command boundary", &mut app);
assert!(!result.is_error);
assert!(matches!(
result.action,
Some(AppAction::SendMessage(message))
if message.contains("Requested relay focus: command boundary")
));
let mut app = create_test_app();
let note_help = execute("/note help", &mut app)
.message
.expect("/note help should return text");
assert!(note_help.contains("Usage: /note"));
let mut app = create_test_app();
let result = execute("/hunt ship layer 2 | budget: 100", &mut app);
assert!(!result.is_error);
assert_eq!(app.hunt.quarry.as_deref(), Some("ship layer 2"));
assert_eq!(app.hunt.token_budget, Some(100));
let (mut app, _tmpdir, _guard) = create_isolated_test_app();
let result = execute("/skills", &mut app);
assert!(matches!(result.action, Some(AppAction::OpenSkillsManager)));
let mut app = create_test_app();
let result = execute("/task list", &mut app);
assert!(matches!(result.action, Some(AppAction::TaskList)));
let mut app = create_test_app();
let tokens = execute("/tokens", &mut app)
.message
.expect("/tokens should return text");
assert!(tokens.contains("deepseek-v4-pro"));
}
#[test]
fn every_registered_command_dispatches_to_a_handler() {
for command in command_infos() {
if skip_in_dispatch_smoke(command.name) {
continue;
}
let (mut app, tmpdir, _guard) = create_isolated_test_app();
let invocation = invocation_for(command.name, command.name, tmpdir.path());
let result = execute(&invocation, &mut app);
if let Some(msg) = &result.message {
assert!(
!msg.contains("Unknown command"),
"/{} fell through to the unknown-command branch: {msg}",
command.name,
);
}
}
}
#[test]
fn every_command_alias_dispatches_to_a_handler() {
for command in command_infos() {
if skip_in_dispatch_smoke(command.name) {
continue;
}
for alias in command.aliases {
let (mut app, tmpdir, _guard) = create_isolated_test_app();
let invocation = invocation_for(command.name, alias, tmpdir.path());
let result = execute(&invocation, &mut app);
if let Some(msg) = &result.message {
assert!(
!msg.contains("Unknown command"),
"/{alias} (alias of /{}) fell through to unknown: {msg}",
command.name,
);
}
}
}
}
#[test]
fn balance_command_has_own_help_text() {
let info = get_command_info("balance").expect("balance command should be registered");
assert_eq!(info.description_id, MessageId::CmdBalanceDescription);
assert!(
info.description_for(Locale::En)
.contains("provider account balance")
);
}
#[test]
fn balance_command_reports_scaffold_without_claiming_dispatch() {
let mut app = create_test_app();
app.api_provider = ApiProvider::Deepseek;
let result = execute("/balance", &mut app);
let msg = result
.message
.expect("balance scaffold should explain current state");
assert!(!result.is_error);
assert!(msg.contains("DeepSeek"));
assert!(msg.contains("not wired"));
assert!(!msg.contains("sent"));
}
#[test]
fn balance_command_reports_unsupported_provider_clearly() {
let mut app = create_test_app();
app.api_provider = ApiProvider::Ollama;
let result = execute("/balance", &mut app);
let msg = result
.message
.expect("unsupported providers should return a clear message");
assert!(!result.is_error);
assert!(msg.contains("Ollama"));
assert!(msg.contains("not supported"));
assert!(msg.contains("dashboard"));
}
#[test]
fn unknown_command_suggests_nearest_match() {
let mut app = create_test_app();
let result = execute("/modle", &mut app);
let msg = result
.message
.expect("unknown command should return an error message");
assert!(msg.contains("Unknown command: /modle"));
assert!(msg.contains("Did you mean:"));
assert!(msg.contains("/model"));
}
#[test]
fn unknown_command_without_close_match_keeps_help_guidance() {
let mut app = create_test_app();
let result = execute("/zzzzzz", &mut app);
let msg = result
.message
.expect("unknown command should return an error message");
assert!(msg.contains("Unknown command: /zzzzzz"));
assert!(msg.contains("Type /help for available commands."));
}
#[test]
fn dollar_skill_prefix_with_no_name_shows_usage() {
let mut app = create_test_app();
let result = execute("$", &mut app);
assert!(result.is_error);
let msg = result.message.expect("should return error message");
assert!(msg.contains("Type a skill name after $"));
}
#[test]
fn dollar_skill_prefix_unknown_skill_reports_unknown_skill() {
let mut app = create_test_app();
let result = execute("$definitely-not-a-real-skill-12345", &mut app);
assert!(result.is_error);
let msg = result.message.expect("should return error message");
assert!(msg.contains("Unknown skill: $definitely-not-a-real-skill-12345"));
assert!(msg.contains("/skills"));
}
#[test]
fn dollar_skill_prefix_does_not_break_existing_slash_dispatch() {
let mut app = create_test_app();
let result = execute("/help", &mut app);
assert!(!result.is_error);
}
fn write_test_skill(root: &Path, name: &str) {
let skill_dir = root.join("skills").join(name);
std::fs::create_dir_all(&skill_dir).expect("skill directory");
std::fs::write(
skill_dir.join("SKILL.md"),
format!(
"---\nname: {name}\ndescription: Test {name} skill\n---\nFollow the test instructions."
),
)
.expect("skill fixture");
}
#[test]
fn task_bearing_skill_invocations_send_the_task_on_the_activated_turn() {
for invocation in ["$foo do X", "/foo do X", "/skill foo do X"] {
let (mut app, tmpdir, _guard) = create_isolated_test_app();
write_test_skill(tmpdir.path(), "foo");
let result = execute(invocation, &mut app);
assert!(!result.is_error, "{invocation}: {result:?}");
assert!(
result
.message
.as_deref()
.is_some_and(|message| message.contains("Skill 'foo' activated")),
"{invocation}: {result:?}"
);
assert!(
matches!(result.action, Some(AppAction::SendMessage(ref task)) if task == "do X"),
"{invocation}: {result:?}"
);
assert!(
app.active_skill
.as_deref()
.is_some_and(|instruction| instruction.contains("# Skill: foo")),
"{invocation} did not arm foo for the dispatched task"
);
}
}
#[test]
fn bare_dollar_skill_still_arms_the_next_message() {
let (mut app, tmpdir, _guard) = create_isolated_test_app();
write_test_skill(tmpdir.path(), "foo");
let result = execute("$foo", &mut app);
assert!(!result.is_error, "{result:?}");
assert!(result.action.is_none());
assert!(
app.active_skill
.as_deref()
.is_some_and(|instruction| instruction.contains("# Skill: foo"))
);
}
#[test]
fn shorthand_can_invoke_a_skill_named_install_without_stealing_management_commands() {
for invocation in ["$install do X", "/install do X"] {
let (mut app, tmpdir, _guard) = create_isolated_test_app();
write_test_skill(tmpdir.path(), "install");
let result = execute(invocation, &mut app);
assert!(!result.is_error, "{invocation}: {result:?}");
assert!(
matches!(result.action, Some(AppAction::SendMessage(ref task)) if task == "do X"),
"{invocation}: {result:?}"
);
assert!(
app.active_skill
.as_deref()
.is_some_and(|instruction| instruction.contains("# Skill: install")),
"{invocation} did not activate the install skill"
);
}
let (mut app, tmpdir, _guard) = create_isolated_test_app();
write_test_skill(tmpdir.path(), "install");
let result = execute("/skill install", &mut app);
assert!(result.is_error, "management subcommand should show usage");
assert!(
result
.message
.as_deref()
.is_some_and(|message| message.contains("/skill install"))
);
assert!(result.action.is_none());
assert!(app.active_skill.is_none());
}
}