rigger 0.20.0

One seat for all your projects and tasks: a local record of what is done, what is next and when it ships - read by you and your coding assistant
//! A thin project skill, written from a template and the record.
//!
//! What the release claims: a skill is one template plus the record, so
//! changing the template changes every skill, and a file rigger wrote is
//! rewritten while a file a person wrote is not.

use std::path::Path;

use assert_cmd::Command;
use predicates::prelude::*;

fn rigger(data: &Path) -> Command {
    let mut cmd = Command::cargo_bin("rigger").unwrap();
    cmd.env("RIGGER_DATA_DIR", data);
    cmd.env("RIGGER_SKILLS_DIR", data.join("skills"));
    cmd
}

/// A project with a manifest that describes it, and a hub with a file only
/// that project could have written.
fn project(data: &Path) -> std::path::PathBuf {
    let root = data.join("sample");
    std::fs::create_dir_all(&root).unwrap();
    std::fs::write(
        root.join("Cargo.toml"),
        "[package]\nname = \"sample-cli\"\ndescription = \"A sample product\"\n",
    )
    .unwrap();
    rigger(data).args(["init"]).assert().success();
    rigger(data).args(["project", "add"]).arg(&root).assert().success();

    let hub = data.join("hub");
    std::fs::create_dir_all(&hub).unwrap();
    std::fs::write(hub.join("План.md"), "# План\n\n## v0.1.0 · First\n\n- [ ] one\n").unwrap();
    std::fs::write(hub.join("Rituals.md"), "Deploy to the box after the tag.\n").unwrap();
    rigger(data).args(["import", "sample", "--hub"]).arg(&hub).assert().success();
    hub
}

fn output(data: &Path, args: &[&str]) -> String {
    let out = rigger(data).args(args).assert().success();
    String::from_utf8(out.get_output().stdout.clone()).unwrap()
}

#[test]
fn the_skill_is_printed_from_the_built_in_template_with_the_record_filled_in() {
    let data = tempfile::tempdir().unwrap();
    let hub = project(data.path());

    let out = output(data.path(), &["skill", "sample"]);
    assert!(out.starts_with("---\nname: sample\n"), "{out}");
    assert!(out.contains("A sample product"), "the manifest's description: {out}");
    assert!(out.contains("rigger context sample"), "{out}");
    assert!(out.contains("rigger session end sample"), "{out}");
    // The record spells the hub path the way the platform does - not the
    // way the test's temporary directory was handed out (`/private/var`
    // on macOS, a short name on a Windows runner).
    let hub = dunce::canonicalize(&hub).unwrap();
    assert!(out.contains(&format!("Hub: {}", hub.display())), "{out}");
    assert!(out.contains("<!-- generated by rigger skill"), "{out}");
    // Nothing was installed by printing.
    assert!(!data.path().join("skills").exists());
}

#[test]
fn a_template_in_the_data_directory_is_what_every_skill_follows() {
    let data = tempfile::tempdir().unwrap();
    project(data.path());
    std::fs::write(data.path().join("skill.md"), "# {{name}}\n\n{{file:Rituals.md}}\n").unwrap();

    let out = output(data.path(), &["skill", "sample"]);
    assert!(out.contains("# sample\n\nDeploy to the box after the tag.\n"), "{out}");
    assert!(!out.contains("rigger context"), "the built-in template is not used when one is written: {out}");
}

#[test]
fn install_writes_rewrites_and_leaves_an_unchanged_file_alone() {
    let data = tempfile::tempdir().unwrap();
    project(data.path());
    let path = data.path().join("skills").join("sample").join("SKILL.md");

    let out = output(data.path(), &["skill", "sample", "--install"]);
    assert!(out.starts_with("Wrote"), "{out}");
    assert!(out.contains("the built-in template"), "{out}");
    assert!(path.is_file());

    let again = output(data.path(), &["skill", "sample", "--install"]);
    assert!(again.contains("already what the template says"), "{again}");

    // The template changed, so the skill follows it.
    std::fs::write(data.path().join("skill.md"), "# {{name}} again\n").unwrap();
    let rewritten = output(data.path(), &["skill", "sample", "--install"]);
    assert!(rewritten.starts_with("Rewrote"), "{rewritten}");
    assert!(std::fs::read_to_string(&path).unwrap().contains("# sample again"));
}

#[test]
fn a_skill_written_by_hand_is_not_replaced_unless_asked() {
    let data = tempfile::tempdir().unwrap();
    project(data.path());
    let dir = data.path().join("skills").join("sample");
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("SKILL.md"), "# sample\n\nSixteen kilobytes of ritual.\n").unwrap();

    rigger(data.path())
        .args(["skill", "sample", "--install"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("written by hand").and(predicate::str::contains("--replace")));
    assert!(std::fs::read_to_string(dir.join("SKILL.md")).unwrap().contains("Sixteen kilobytes"));

    output(data.path(), &["skill", "sample", "--install", "--replace"]);
    assert!(std::fs::read_to_string(dir.join("SKILL.md")).unwrap().contains("rigger context sample"));
}

#[test]
fn a_placeholder_rigger_does_not_know_is_refused_by_name() {
    let data = tempfile::tempdir().unwrap();
    project(data.path());
    std::fs::write(data.path().join("skill.md"), "{{name}} {{tier}}\n").unwrap();

    rigger(data.path())
        .args(["skill", "sample"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("{{tier}}").and(predicate::str::contains("file:<name>")));
}

#[test]
fn dir_and_template_are_taken_from_the_command_line() {
    let data = tempfile::tempdir().unwrap();
    project(data.path());
    let template = data.path().join("mine.md");
    std::fs::write(&template, "mine: {{name}}\n").unwrap();
    let dir = data.path().join("elsewhere");

    let out = output(
        data.path(),
        &["skill", "sample", "--dir", dir.to_str().unwrap(), "--template", template.to_str().unwrap()],
    );
    assert!(out.contains("mine.md"), "{out}");
    let written = std::fs::read_to_string(dir.join("sample").join("SKILL.md")).unwrap();
    assert!(written.ends_with("mine: sample\n"), "{written}");
}

#[test]
fn the_built_in_template_can_be_printed_to_start_from() {
    let data = tempfile::tempdir().unwrap();
    let out = output(data.path(), &["skill", "--print-template"]);
    assert!(out.starts_with("---\nname: {{name}}\n"), "{out}");
}