gram-data 0.3.11

Unified gram CLI and library for validating gram notation
Documentation
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;

fn gram() -> Command {
    Command::cargo_bin("gram").unwrap()
}

// --- install ---

#[test]
fn skill_install_no_agents_detected() {
    let dir = TempDir::new().unwrap();
    let home_dir = TempDir::new().unwrap();
    gram()
        .args(["skill", "install"])
        .current_dir(dir.path())
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("No gram-compatible agents detected"));
}

#[test]
fn skill_install_project_local_claude() {
    let dir = TempDir::new().unwrap();
    fs::create_dir(dir.path().join(".claude")).unwrap();

    gram()
        .args(["skill", "install"])
        .current_dir(dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("claude"));

    let installed = dir.path().join(".claude/skills/gram/SKILL.md");
    assert!(installed.exists(), "SKILL.md should be installed for claude");

    let content = fs::read_to_string(&installed).unwrap();
    assert!(content.contains("name: gram"), "installed SKILL.md should contain 'name: gram'");
}

#[test]
fn skill_install_overwrites_with_warning() {
    let dir = TempDir::new().unwrap();
    let skill_dir = dir.path().join(".claude/skills/gram");
    fs::create_dir_all(&skill_dir).unwrap();
    fs::write(skill_dir.join("SKILL.md"), "old content").unwrap();

    gram()
        .args(["skill", "install"])
        .current_dir(dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("overwritten"));

    let content = fs::read_to_string(skill_dir.join("SKILL.md")).unwrap();
    assert_ne!(content, "old content", "file should have been overwritten");
}

#[test]
fn skill_install_agent_flag_restricts_target() {
    let dir = TempDir::new().unwrap();
    fs::create_dir(dir.path().join(".claude")).unwrap();
    fs::create_dir(dir.path().join(".cursor")).unwrap();

    gram()
        .args(["skill", "install", "--agent", "claude"])
        .current_dir(dir.path())
        .assert()
        .success();

    assert!(dir.path().join(".claude/skills/gram/SKILL.md").exists());
    assert!(!dir.path().join(".cursor/skills/gram/SKILL.md").exists(),
        "cursor should not have been installed when --agent claude was specified");
}

#[test]
fn skill_install_unknown_agent_exits_2() {
    let dir = TempDir::new().unwrap();
    gram()
        .args(["skill", "install", "--agent", "bogus"])
        .current_dir(dir.path())
        .assert()
        .failure()
        .code(2)
        .stderr(predicate::str::contains("unknown agent"))
        .stderr(predicate::str::contains("claude"));
}

// --- list ---

#[test]
fn skill_list_nothing_installed() {
    let dir = TempDir::new().unwrap();
    let home_dir = TempDir::new().unwrap();
    gram()
        .args(["skill", "list"])
        .current_dir(dir.path())
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("No gram skill installations found"));
}

#[test]
fn skill_list_shows_installed() {
    let dir = TempDir::new().unwrap();
    let skill_dir = dir.path().join(".cursor/skills/gram");
    fs::create_dir_all(&skill_dir).unwrap();
    fs::write(skill_dir.join("SKILL.md"), "---\nname: gram\n---\n").unwrap();

    gram()
        .args(["skill", "list"])
        .current_dir(dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("cursor"));
}

// --- remove ---

#[test]
fn skill_remove_not_installed() {
    let dir = TempDir::new().unwrap();
    let home_dir = TempDir::new().unwrap();
    gram()
        .args(["skill", "remove"])
        .current_dir(dir.path())
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("not installed"));
}

#[test]
fn skill_list_and_remove_roundtrip() {
    let dir = TempDir::new().unwrap();
    let home_dir = TempDir::new().unwrap();
    fs::create_dir(dir.path().join(".claude")).unwrap();

    // install
    gram()
        .args(["skill", "install"])
        .current_dir(dir.path())
        .env("HOME", home_dir.path())
        .assert()
        .success();

    // list shows it
    gram()
        .args(["skill", "list"])
        .current_dir(dir.path())
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("claude"));

    // remove
    gram()
        .args(["skill", "remove"])
        .current_dir(dir.path())
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("removed"));

    // list shows nothing
    gram()
        .args(["skill", "list"])
        .current_dir(dir.path())
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("No gram skill installations found"));
}

// --- global flag ---

#[test]
fn skill_install_global_detects_home_dir_agent() {
    let home_dir = TempDir::new().unwrap();
    fs::create_dir(home_dir.path().join(".claude")).unwrap();

    gram()
        .args(["skill", "install", "--global"])
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("claude"));

    let installed = home_dir.path().join(".claude/skills/gram/SKILL.md");
    assert!(installed.exists(), "SKILL.md should be installed globally for claude");
    let content = fs::read_to_string(&installed).unwrap();
    assert!(content.contains("name: gram"), "installed SKILL.md should contain 'name: gram'");
}

#[test]
fn skill_list_global_shows_home_dir_installation() {
    let home_dir = TempDir::new().unwrap();
    let skill_dir = home_dir.path().join(".cursor/skills/gram");
    fs::create_dir_all(&skill_dir).unwrap();
    fs::write(skill_dir.join("SKILL.md"), "---\nname: gram\n---\n").unwrap();

    gram()
        .args(["skill", "list", "--global"])
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("cursor"));
}

#[test]
fn skill_global_install_list_remove_roundtrip() {
    let home_dir = TempDir::new().unwrap();
    fs::create_dir(home_dir.path().join(".claude")).unwrap();

    gram()
        .args(["skill", "install", "--global"])
        .env("HOME", home_dir.path())
        .assert()
        .success();

    gram()
        .args(["skill", "list", "--global"])
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("claude"));

    gram()
        .args(["skill", "remove", "--global"])
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("removed"));

    gram()
        .args(["skill", "list", "--global"])
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("No gram skill installations found"));
}

#[test]
fn skill_install_falls_back_to_global_when_no_project_local_agent() {
    // Project dir has no agent config dirs; home dir has .claude/.
    // Without --global, install should fall back and detect the global agent.
    let project_dir = TempDir::new().unwrap();
    let home_dir = TempDir::new().unwrap();
    fs::create_dir(home_dir.path().join(".claude")).unwrap();

    gram()
        .args(["skill", "install"])
        .current_dir(project_dir.path())
        .env("HOME", home_dir.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("claude"));

    let installed = home_dir.path().join(".claude/skills/gram/SKILL.md");
    assert!(installed.exists(), "should install to global ~/.claude when no project-local .claude exists");
}

#[test]
fn skill_remove_unknown_agent_exits_2() {
    let dir = TempDir::new().unwrap();
    gram()
        .args(["skill", "remove", "--agent", "bogus"])
        .current_dir(dir.path())
        .assert()
        .failure()
        .code(2)
        .stderr(predicate::str::contains("unknown agent"));
}