starweaver-cli 0.2.1

Command-line interface for Starweaver
Documentation
#![allow(missing_docs, clippy::unwrap_used)]

use std::process::{Command, Output};

fn cli(temp: &tempfile::TempDir) -> Command {
    let mut command = Command::new(env!("CARGO_BIN_EXE_starweaver-cli"));
    command.env("STARWEAVER_PROJECT_DIR", temp.path().join(".starweaver"));
    command.env("STARWEAVER_CONFIG_DIR", temp.path().join("global"));
    command
}

fn silent_value(stdout: &[u8], key: &str) -> String {
    String::from_utf8_lossy(stdout)
        .lines()
        .find_map(|line| line.strip_prefix(&format!("{key}=")))
        .unwrap()
        .to_string()
}

fn assert_pending_hitl_blocks_resume(output: &Output, expected_id: &str) {
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("cannot resume run"));
    assert!(stderr.contains(expected_id));
}

#[test]
fn tui_without_session_renders_ready_state() {
    let temp = tempfile::tempdir().unwrap();
    let tui = cli(&temp).arg("tui").output().unwrap();
    assert!(
        tui.status.success(),
        "stderr={}",
        String::from_utf8_lossy(&tui.stderr)
    );
    let stdout = String::from_utf8(tui.stdout).unwrap();
    assert!(stdout.contains("Welcome to Starweaver"));
    assert!(stdout.contains("status=ready"));
    assert!(stdout.contains("session=none"));
    assert!(stdout.contains("sw cli -p \"hello\""));
    assert!(!stdout.contains("make cli -- -p \"hello\""));
    assert!(!temp.path().join(".starweaver/starweaver.sqlite").exists());
    assert!(!temp.path().join(".starweaver/state.json").exists());
    assert!(!temp.path().join(".starweaver/store").exists());
}

#[test]
fn reset_removes_runtime_state_and_preserves_config() {
    let temp = tempfile::tempdir().unwrap();
    let run = cli(&temp)
        .args(["run", "reset me", "--output", "silent"])
        .output()
        .unwrap();
    assert!(run.status.success());
    assert!(temp.path().join(".starweaver/starweaver.sqlite").exists());
    assert!(temp.path().join(".starweaver/state.json").exists());
    assert!(temp.path().join(".starweaver/store").exists());

    let reset = cli(&temp)
        .args(["reset", "--yes", "--output", "silent"])
        .output()
        .unwrap();
    assert!(
        reset.status.success(),
        "stderr={}",
        String::from_utf8_lossy(&reset.stderr)
    );
    assert!(String::from_utf8(reset.stdout)
        .unwrap()
        .contains("status=reset"));
    assert!(!temp.path().join(".starweaver/starweaver.sqlite").exists());
    assert!(!temp.path().join(".starweaver/state.json").exists());
    assert!(!temp.path().join(".starweaver/store").exists());
    assert!(temp.path().join("global/config.toml").exists());
}

#[test]
fn tui_without_session_stays_clean_after_runs_exist() {
    let temp = tempfile::tempdir().unwrap();
    let run = cli(&temp)
        .args(["run", "do not auto replay", "--output", "silent"])
        .output()
        .unwrap();
    assert!(run.status.success());

    let tui = cli(&temp).arg("tui").output().unwrap();
    assert!(
        tui.status.success(),
        "stderr={}",
        String::from_utf8_lossy(&tui.stderr)
    );
    let stdout = String::from_utf8(tui.stdout).unwrap();
    assert!(stdout.contains("Welcome to Starweaver"));
    assert!(stdout.contains("session=none"));
    assert!(!stdout.contains("local echo: do not auto replay"));
    assert!(!stdout.contains("Starweaver CLI TUI snapshot"));
}

#[test]
fn tui_snapshot_renders_display_replay() {
    let temp = tempfile::tempdir().unwrap();
    let run = cli(&temp)
        .args(["run", "hello tui", "--output", "silent"])
        .output()
        .unwrap();
    assert!(run.status.success());
    let session_id = silent_value(&run.stdout, "session_id");

    let tui = cli(&temp)
        .args(["tui", "--session", &session_id])
        .output()
        .unwrap();
    assert!(
        tui.status.success(),
        "stderr={}",
        String::from_utf8_lossy(&tui.stderr)
    );
    let stdout = String::from_utf8(tui.stdout).unwrap();
    assert!(stdout.contains("Starweaver CLI TUI snapshot"));
    assert!(stdout.contains(&format!("session_id={session_id}")));
    assert!(stdout.contains("Assistant"));
    assert!(stdout.contains("local echo: hello tui"));
}

#[test]
fn approval_commands_list_show_and_decide_records() {
    let temp = tempfile::tempdir().unwrap();
    let run = cli(&temp)
        .args([
            "run",
            "needs approval",
            "--new-session",
            "--profile",
            "approval_model",
            "--hitl",
            "defer",
            "--output",
            "silent",
        ])
        .output()
        .unwrap();
    assert!(run.status.success());
    assert!(String::from_utf8_lossy(&run.stdout).contains("status=waiting"));
    let session_id = silent_value(&run.stdout, "session_id");

    let list = cli(&temp).args(["approval", "list"]).output().unwrap();
    assert!(list.status.success());
    let first: serde_json::Value = serde_json::from_str(
        String::from_utf8(list.stdout)
            .unwrap()
            .lines()
            .next()
            .unwrap(),
    )
    .unwrap();
    let approval_id = first["approval_id"].as_str().unwrap().to_string();
    assert_eq!(first["status"], "pending");

    let show = cli(&temp)
        .args(["approval", "show", &approval_id])
        .output()
        .unwrap();
    assert!(show.status.success());
    assert!(String::from_utf8(show.stdout)
        .unwrap()
        .contains(&approval_id));

    assert_pending_hitl_blocks_resume(
        &cli(&temp)
            .args([
                "resume",
                "--session",
                &session_id,
                "--prompt",
                "continue before approval",
                "--output",
                "text",
            ])
            .output()
            .unwrap(),
        &approval_id,
    );

    let implicit_continue = cli(&temp)
        .args([
            "run",
            "implicit continue before approval",
            "--session",
            &session_id,
            "--output",
            "text",
        ])
        .output()
        .unwrap();
    assert_pending_hitl_blocks_resume(&implicit_continue, &approval_id);

    let approve = cli(&temp)
        .args([
            "approval",
            "approve",
            &approval_id,
            "--reason",
            "ok",
            "--output",
            "silent",
        ])
        .output()
        .unwrap();
    assert!(approve.status.success());
    assert!(String::from_utf8(approve.stdout)
        .unwrap()
        .contains("status=approved"));

    let resume = cli(&temp)
        .args([
            "resume",
            "--session",
            &session_id,
            "--prompt",
            "continue after approval",
            "--output",
            "text",
        ])
        .output()
        .unwrap();
    assert!(
        resume.status.success(),
        "stderr={}",
        String::from_utf8_lossy(&resume.stderr)
    );
    let resume_stdout = String::from_utf8(resume.stdout).unwrap();
    assert!(resume_stdout.contains("approval_probe handled"));
}

#[test]
fn deferred_commands_complete_and_resume_waiting_session() {
    let temp = tempfile::tempdir().unwrap();
    let run = cli(&temp)
        .args([
            "run",
            "defer me",
            "--new-session",
            "--profile",
            "deferred_model",
            "--hitl",
            "defer",
            "--output",
            "silent",
        ])
        .output()
        .unwrap();
    assert!(run.status.success());
    let session_id = silent_value(&run.stdout, "session_id");

    let list = cli(&temp).args(["deferred", "list"]).output().unwrap();
    assert!(list.status.success());
    let first: serde_json::Value = serde_json::from_str(
        String::from_utf8(list.stdout)
            .unwrap()
            .lines()
            .next()
            .unwrap(),
    )
    .unwrap();
    let deferred_id = first["deferred_id"].as_str().unwrap().to_string();
    assert_eq!(first["status"], "waiting");

    assert_pending_hitl_blocks_resume(
        &cli(&temp)
            .args([
                "resume",
                "--session",
                &session_id,
                "--prompt",
                "continue before deferred completion",
                "--output",
                "text",
            ])
            .output()
            .unwrap(),
        &deferred_id,
    );

    let complete = cli(&temp)
        .args([
            "deferred",
            "complete",
            &deferred_id,
            "--result",
            r#"{"ok":true}"#,
            "--output",
            "silent",
        ])
        .output()
        .unwrap();
    assert!(
        complete.status.success(),
        "stderr={}",
        String::from_utf8_lossy(&complete.stderr)
    );
    assert!(String::from_utf8(complete.stdout)
        .unwrap()
        .contains("status=completed"));

    let resume = cli(&temp)
        .args([
            "resume",
            "--session",
            &session_id,
            "--prompt",
            "continue after deferred",
            "--output",
            "text",
        ])
        .output()
        .unwrap();
    assert!(
        resume.status.success(),
        "stderr={}",
        String::from_utf8_lossy(&resume.stderr)
    );
    let resume_stdout = String::from_utf8(resume.stdout).unwrap();
    assert!(resume_stdout.contains("deferred_probe handled"));
}