use super::support::*;
use super::*;
use crate::commands::BuiltInCommand;
use crate::output::ActivityStatus;
use crate::tui::input::submit_command::{
is_new_session_command, is_quit_command, slash_command_token, unsupported_slash_command,
};
#[test]
fn summary_commands_remain_local_during_primary_run() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
app.active_run = true;
let mut ui_state = state::MissionControlState::default();
for command in ["/summarize-start", "/summarize-stop"] {
assert!(!tui_submit_command(command, &app.commands).requires_idle());
assert!(!app.submit(command.into(), &mut ui_state, &receiver, test_area()));
assert!(ui_state.is_summary_focused());
assert!(app.active_run);
assert_eq!(app.steering.pending_count(), 0);
assert!(receiver.try_recv().is_err());
}
app.submit(
"/summarize-start unexpected".into(),
&mut ui_state,
&receiver,
test_area(),
);
assert!(ui_state.status.starts_with("usage:"));
}
#[test]
fn slash_command_recognizers_use_command_token_after_trim() {
assert_eq!(slash_command_token("/new"), Some("/new"));
assert_eq!(slash_command_token(" /new please "), Some("/new"));
assert_eq!(slash_command_token("/new\ntext"), Some("/new"));
assert_eq!(slash_command_token("not /new"), None);
assert!(is_new_session_command("/new"));
assert!(is_new_session_command(" /new "));
assert!(is_new_session_command("/new please"));
assert!(is_new_session_command("/new\ntext"));
assert!(!is_new_session_command("/new-session"));
assert!(!is_new_session_command("/setmodel"));
assert!(is_quit_command(" /quit "));
assert!(is_quit_command(" /quit now "));
}
#[test]
fn prune_sessions_tui_command_captures_args_and_requires_idle() {
assert_eq!(
tui_submit_command(" /prune-sessions 7 ", &CommandRegistry::mvp()),
TuiSubmitCommand::PruneSessions(Some("7"))
);
assert_eq!(
tui_submit_command("/prune-sessions", &CommandRegistry::mvp()),
TuiSubmitCommand::PruneSessions(None)
);
let command = tui_submit_command("/prune-sessions 7", &CommandRegistry::mvp());
assert!(command.requires_idle());
assert_eq!(
command.active_run_error(),
Some("cannot prune sessions while a prompt is running")
);
}
#[test]
fn unsupported_slash_command_recognizer_treats_all_builtins_as_supported() {
for command in BuiltInCommand::ALL {
let prompt = format!(" /{} ignored ", command.name());
assert_eq!(unsupported_slash_command(&prompt), None, "{prompt}");
assert!(
!matches!(
tui_submit_command(&prompt, &CommandRegistry::mvp()),
TuiSubmitCommand::Unsupported(_)
),
"{prompt}"
);
}
}
#[test]
fn unsupported_slash_command_recognizer_keeps_new_model_skills_system_prompt_and_quit_tokens() {
assert_eq!(unsupported_slash_command("/compact keep file paths"), None);
assert_eq!(unsupported_slash_command("/setmodel gpt-next"), None);
assert_eq!(unsupported_slash_command("/skills"), None);
assert_eq!(unsupported_slash_command("/subagents"), None);
assert_eq!(unsupported_slash_command("/system-prompt"), None);
assert_eq!(unsupported_slash_command("/system-prompt ignored"), None);
assert_eq!(
unsupported_slash_command("/skill:review"),
Some("/skill:review")
);
assert_eq!(
unsupported_slash_command(" /missing arg "),
Some("/missing")
);
assert_eq!(unsupported_slash_command("/new"), None);
assert_eq!(unsupported_slash_command("/new extra"), None);
assert_eq!(unsupported_slash_command("/quit"), None);
assert_eq!(unsupported_slash_command("/quit extra"), None);
assert_eq!(unsupported_slash_command("hello"), None);
}
#[test]
fn compact_command_uses_override_without_refreshing_missing_active_codex_auth() {
let _env = crate::test_support::env::env_lock();
let temp = tempfile::TempDir::new().unwrap();
let session = SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app_with_active_session(&temp, sender, Some(session.clone()));
app.config.provider = Some(crate::providers::OPENAI_CODEX_PROVIDER.to_string());
app.config.model = Some("active-model".to_string());
app.config.auth = None;
app.state.config = Some(app.config.clone());
app.state.model = "active-model".to_string();
app.state.auth_state = crate::config::AuthState::Missing {
provider: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
};
let base_url = start_sse_server("tui override summary");
write_compaction_override_settings(
&app.config.paths,
"local-provider",
"summary-model",
&base_url,
);
app.settings = crate::config::read_settings(&app.config.paths).unwrap();
let mut ui_state = state::MissionControlState {
provider: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
model: "active-model".to_string(),
provider_ready: false,
..Default::default()
};
app.start_compaction(None, &mut ui_state);
assert!(app.worker.is_some());
assert!(app.active_run);
assert!(!ui_state.provider_ready);
assert_eq!(ui_state.status, "compacting session…");
let worker = app.worker.take().unwrap();
worker.handle.join().unwrap();
let result = drain_tui_events(&receiver, &mut ui_state);
assert!(result.run_finished);
assert!(
ui_state.status.contains("compaction complete: session"),
"{}",
ui_state.status
);
let compaction_nodes = ui_state
.nodes
.values()
.filter(|node| node.kind == crate::output::ActivityKind::Compaction)
.collect::<Vec<_>>();
assert_eq!(compaction_nodes.len(), 1);
assert_eq!(compaction_nodes[0].status, ActivityStatus::Success);
assert_eq!(compaction_nodes[0].preview, "tui override summary");
let events = session.read_events().unwrap();
let compaction = events
.iter()
.find(|event| event.event_type == "compaction")
.unwrap();
assert_eq!(compaction.payload["summary"], "tui override summary");
assert_eq!(compaction.payload["provider"], "local-provider");
assert_eq!(compaction.payload["model"], "summary-model");
}
#[test]
fn compaction_runtime_refresh_applies_refreshed_auth_to_active_runtime() {
let temp = tempfile::TempDir::new().unwrap();
let session = SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let (sender, _receiver) = bounded::<TuiEvent>(2);
let mut app = test_app_with_active_session(&temp, sender, Some(session.clone()));
let provider = crate::providers::ANTHROPIC_PROVIDER.to_string();
let model = "claude-test".to_string();
let old_credential = crate::config::ProviderCredential::ApiKey {
key: "old-key".to_string(),
};
app.config.provider = Some(provider.clone());
app.config.model = Some(model.clone());
app.config.auth = Some(old_credential.clone());
app.state.config = Some(app.config.clone());
app.state.model = model.clone();
app.state.auth_state = crate::config::AuthState::Ready {
provider: provider.clone(),
credential: old_credential,
};
let mut ui_state = state::MissionControlState {
provider: provider.clone(),
model: model.clone(),
provider_ready: false,
..Default::default()
};
let new_credential = crate::config::ProviderCredential::ApiKey {
key: "new-key".to_string(),
};
let mut refreshed_config = app.config.clone();
refreshed_config.auth = Some(new_credential);
let changed = app.apply_compaction_runtime_refresh(
&mut ui_state,
CompactionRuntimeRefresh {
config: refreshed_config.clone(),
session_id: session.id().to_string(),
provider: provider.clone(),
model: model.clone(),
},
);
assert!(changed);
assert!(ui_state.provider_ready);
assert_eq!(app.config, refreshed_config);
assert_eq!(app.state.config.as_ref(), Some(&refreshed_config));
assert!(matches!(
&app.state.auth_state,
crate::config::AuthState::Ready {
provider: active_provider,
credential: crate::config::ProviderCredential::ApiKey { key },
} if active_provider == &provider && key == "new-key"
));
}
#[test]
fn compaction_settings_snapshot_keeps_tui_fast_mode_after_disk_change() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, _receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
app.settings.fast.enabled = true;
std::fs::create_dir_all(&app.config.paths.root).unwrap();
std::fs::write(
&app.config.paths.settings_file,
r#"{"fast":{"enabled":false}}"#,
)
.unwrap();
let settings = app.compaction_settings_snapshot();
assert!(settings.fast.enabled);
}
#[test]
fn submit_idle_only_commands_reject_while_provider_run_active() {
let cases = [
("/compact", "cannot compact while a prompt is running"),
("/login", "cannot start login while a prompt is running"),
("/logout", "cannot logout while a prompt is running"),
("/setmodel", "cannot change model while a prompt is running"),
(
"/mcp",
"cannot manage MCP servers while a prompt is running",
),
("/skills", "cannot manage skills while a prompt is running"),
(
"/subagents",
"cannot manage subagents while a prompt is running",
),
(
"/sessions",
"cannot switch sessions while a prompt is running",
),
(
"/new",
"cannot start a new session while a prompt is running",
),
];
for (prompt, expected) in cases {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
app.active_run = true;
let mut ui_state = state::MissionControlState::default();
app.submit(prompt.to_string(), &mut ui_state, &receiver, test_area());
assert_eq!(ui_state.status, expected, "{prompt}");
assert!(receiver.try_recv().is_err());
assert!(app.worker.is_none());
assert!(app.active_run, "{prompt}");
}
}