mod common;
use std::process::Stdio;
async fn run_repl(commands: &str) -> String {
let server = common::MockServer::start(vec![common::MockResponse::sse("")]).await;
static COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let uniq = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let sessions_dir =
std::env::temp_dir().join(format!("hey-repl-test-{}-{uniq}", std::process::id()));
let _ = std::fs::create_dir_all(&sessions_dir);
let mut child = tokio::process::Command::new(env!("CARGO_BIN_EXE_hey"))
.args([
"--base-url",
&format!("http://{}", server.addr),
"-m",
"test-model",
"--no-mcp",
"--no-skills",
"--sessions-dir",
sessions_dir.to_str().unwrap(),
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn hey binary");
let mut stdin = child.stdin.take().expect("stdin");
use tokio::io::AsyncWriteExt;
stdin
.write_all(commands.as_bytes())
.await
.expect("write commands");
drop(stdin); let out = child.wait_with_output().await.expect("wait");
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[tokio::test]
async fn effort_set_and_show() {
let text = run_repl("/effort high\n/effort\n/quit\n").await;
assert!(text.contains("effort set to 'high'"), "设置成功: {text}");
assert!(text.contains("current effort: high"), "空参查看: {text}");
}
#[tokio::test]
async fn effort_invalid_reports_levels() {
let text = run_repl("/effort turbo\n/quit\n").await;
assert!(
text.contains("use off|minimal|low|medium|high|xhigh|max"),
"非法值列出档位: {text}"
);
}
#[tokio::test]
async fn model_switch_shows_effort() {
let text = run_repl("/model\n/quit\n").await;
assert!(text.contains("test-model"), "模型显示: {text}");
assert!(text.contains("effort=medium"), "effort 显示: {text}");
}
#[tokio::test]
async fn thinking_toggle() {
let text = run_repl("/thinking\n/thinking\n/quit\n").await;
assert!(
text.contains("thinking display off"),
"第一次 toggle off: {text}"
);
assert!(
text.contains("thinking display on"),
"第二次 toggle on: {text}"
);
}
#[tokio::test]
async fn help_lists_all_commands() {
let text = run_repl("/help\n/quit\n").await;
for cmd in [
"/quit",
"/model",
"/effort",
"/thinking",
"/usage",
"/compact",
"/reload",
"/resume",
"/sessions",
"/sessions rm",
"/sessions prune",
"/skill:",
] {
assert!(text.contains(cmd), "help 含 {cmd}: {text}");
}
}
#[tokio::test]
async fn sessions_empty_and_resume_boundaries() {
let text = run_repl("/sessions\n/resume -\n/resume ghost\n/quit\n").await;
assert!(text.contains("(no sessions)"), "/sessions 空列表: {text}");
assert!(
text.contains("no sessions to resume"),
"/resume - 无会话: {text}"
);
assert!(
text.contains("session 'ghost' not found (starting fresh)"),
"/resume 未找到: {text}"
);
}
#[tokio::test]
async fn usage_silent_in_terminal() {
let text = run_repl("hi\n/quit\n").await;
assert!(
!text.contains("[hey] usage:"),
"终端不应自动打印 usage: {text}"
);
}