hey 0.1.0

Minimal terminal AI coding agent: kernel loop + MCP/Skills self-evolution
Documentation
//! REPL 命令层集成测试:spawn 真实二进制 + 管道输入,断言命令输出。
//!
//! 只测无副作用命令(/help /effort /thinking /model /quit)——不碰真实会话
//! (/sessions rm /resume 等涉及真实会话目录,底层已由 sessions 单元测试覆盖)。
//! REPL 启动不发请求(命令在 turn 前处理),故 mock server 无需有效响应。

mod common;

use std::process::Stdio;

/// 启动 REPL 子进程,喂命令,返回 stdout。
/// 会话目录经 --sessions-dir 重定向到临时目录(CLI 子进程可显式指定,无需环境变量)。
async fn run_repl(commands: &str) -> String {
    let server = common::MockServer::start(vec![common::MockResponse::sse("")]).await;
    // 会话目录每测试唯一(原子计数),并行测试互不污染(此前固定 {pid} 并行共享导致 flaky)
    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); // EOF → REPL 退出
    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;
    // /model 空参显示 provider/model + effort(此处 effort 默认 medium)
    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() {
    // 空会话目录(--sessions-dir 隔离):/sessions 空列表;/resume - 无会话;/resume <id> 未找到
    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() {
    // 终端不自动打印 usage——`/usage` 命令覆盖,避免每轮刷屏。
    let text = run_repl("hi\n/quit\n").await;
    assert!(
        !text.contains("[hey] usage:"),
        "终端不应自动打印 usage: {text}"
    );
}