opencode-ralph-loop-cli 0.1.0

Scaffolder CLI for OpenCode Ralph Loop plugin — one command setup
Documentation
use assert_cmd::Command;
use tempfile::TempDir;

fn cmd() -> Command {
    Command::cargo_bin("opencode-ralph-loop-cli").unwrap()
}

fn setup_dir() -> TempDir {
    TempDir::new().unwrap()
}

#[test]
fn scenario_1_clean_scaffold_creates_7_files() {
    let dir = setup_dir();

    cmd()
        .args(["init", "--path", dir.path().to_str().unwrap()])
        .assert()
        .success();

    let opencode = dir.path().join(".opencode");
    assert!(
        opencode.join("plugins/ralph.ts").exists(),
        "ralph.ts missing"
    );
    assert!(opencode.join("commands/ralph-loop.md").exists());
    assert!(opencode.join("commands/cancel-ralph.md").exists());
    assert!(opencode.join("commands/ralph-help.md").exists());
    assert!(opencode.join("package.json").exists());
    assert!(opencode.join(".gitignore").exists());
    assert!(opencode.join(".manifest.json").exists());
}

#[test]
fn scenario_2_second_run_all_skipped() {
    let dir = setup_dir();
    let path = dir.path().to_str().unwrap();

    cmd().args(["init", "--path", path]).assert().success();

    let output = cmd()
        .args(["init", "--path", path, "--output", "json"])
        .output()
        .unwrap();

    assert!(output.status.success());
    let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    let files = json["files"].as_array().unwrap();
    for f in files {
        assert_eq!(
            f["action"].as_str().unwrap(),
            "skipped",
            "file {f} was not skipped"
        );
    }
}

#[test]
fn scenario_3_conflict_without_force_exits_3() {
    let dir = setup_dir();
    let path = dir.path().to_str().unwrap();
    let opencode = dir.path().join(".opencode");

    cmd().args(["init", "--path", path]).assert().success();

    std::fs::write(opencode.join("plugins/ralph.ts"), b"modified content").unwrap();

    cmd()
        .args(["init", "--path", path])
        .assert()
        .failure()
        .code(3);
}

#[test]
fn scenario_4_force_updates_files() {
    let dir = setup_dir();
    let path = dir.path().to_str().unwrap();
    let opencode = dir.path().join(".opencode");

    cmd().args(["init", "--path", path]).assert().success();

    std::fs::write(opencode.join("plugins/ralph.ts"), b"modified content").unwrap();

    let output = cmd()
        .args(["init", "--path", path, "--force", "--output", "json"])
        .output()
        .unwrap();

    assert!(output.status.success());
    let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    let files = json["files"].as_array().unwrap();
    let ralph = files
        .iter()
        .find(|f| f["path"] == "plugins/ralph.ts")
        .unwrap();
    assert_eq!(ralph["action"].as_str().unwrap(), "updated");
}

#[test]
fn scenario_5_json_output_is_valid() {
    let dir = setup_dir();
    let path = dir.path().to_str().unwrap();

    let output = cmd()
        .args(["init", "--path", path, "--output", "json"])
        .output()
        .unwrap();

    assert!(output.status.success());
    let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(json["status"].as_str().unwrap(), "ok");
    assert!(json["files"].is_array());
    assert!(json["summary"].is_object());
}

#[test]
fn scenario_6_dry_run_creates_no_files() {
    let dir = setup_dir();
    let path = dir.path().to_str().unwrap();

    cmd()
        .args(["init", "--path", path, "--dry-run"])
        .assert()
        .success();

    let opencode = dir.path().join(".opencode");
    assert!(
        !opencode.exists(),
        ".opencode should not exist after dry-run"
    );
}

#[test]
fn scenario_7_nonexistent_path_exits_4() {
    cmd()
        .args(["init", "--path", "/tmp/nao_existe_certamente_12345678"])
        .assert()
        .failure()
        .code(4);
}

#[test]
fn scenario_11_custom_plugin_version() {
    let dir = setup_dir();
    let path = dir.path().to_str().unwrap();

    cmd()
        .args(["init", "--path", path, "--plugin-version", "2.0.0"])
        .assert()
        .success();

    let opencode = dir.path().join(".opencode");
    let pkg = std::fs::read_to_string(opencode.join("package.json")).unwrap();
    assert!(
        pkg.contains("2.0.0"),
        "package.json does not contain custom version: {pkg}"
    );
    assert!(!pkg.contains("1.4.7"), "default version should not appear");
}