flowmark 0.3.0

A Markdown auto-formatter for clean diffs and semantic line breaks
Documentation
//! Tests for the skill installation module.
//!
//! Ported from Python: `test_skill.py` (9 tests)

use std::fs;

use flowmark::skills::{get_docs_content, get_skill_content, install_skill};

// --- Skill content loading (3 tests) ---

#[test]
fn test_skill_content_loads() {
    let content = get_skill_content();
    assert!(!content.is_empty(), "SKILL.md should be loadable and non-empty");
}

#[test]
fn test_skill_content_has_metadata() {
    let content = get_skill_content();
    assert!(content.contains("name: flowmark"), "should contain name: flowmark");
    assert!(content.contains("description:"), "should contain description:");
    assert!(content.contains("allowed-tools:"), "should contain allowed-tools:");
}

#[test]
fn test_skill_content_has_usage() {
    let content = get_skill_content();
    assert!(content.contains("# Flowmark"), "should contain # Flowmark heading");
    // Rust binary uses `flowmark` command directly (Python uses `uvx flowmark`)
    assert!(content.contains("flowmark --auto"), "should contain usage instructions");
}

#[test]
fn test_skill_content_has_vscode_cursor_setup() {
    let content = get_skill_content();
    assert!(
        content.contains("VS Code/Cursor"),
        "skill should include VS Code/Cursor setup section"
    );
    assert!(
        content.contains("emeraldwalk.runonsave"),
        "skill should include run-on-save configuration"
    );
}

// --- Docs content loading (2 tests) ---

#[test]
fn test_docs_content_loads() {
    let content = get_docs_content();
    assert!(!content.is_empty(), "docs content should be non-empty");
}

#[test]
fn test_docs_content_has_flowmark_reference() {
    let content = get_docs_content();
    let lower = content.to_lowercase();
    assert!(lower.contains("flowmark"), "docs content should reference flowmark");
}

#[test]
fn test_docs_content_has_vscode_cursor_setup() {
    let content = get_docs_content();
    assert!(
        content.contains("VSCode/Cursor") || content.contains("VS Code/Cursor"),
        "docs should include VS Code/Cursor section"
    );
    assert!(
        content.contains("emeraldwalk.runonsave"),
        "docs should include run-on-save settings snippet"
    );
}

// --- Skill installation (4 tests) ---

#[test]
fn test_install_skill_default() {
    // We can't easily mock home_dir() in Rust, so we test with custom base
    // which exercises the same code path minus home directory lookup.
    // The default path test is covered indirectly by the CLI --install-skill test.
    let dir = tempfile::tempdir().expect("create temp dir");
    let base = dir.path().join(".claude");

    install_skill(Some(base.to_str().expect("path to str"))).expect("install skill");

    let skill_file = base.join("skills").join("flowmark").join("SKILL.md");
    assert!(skill_file.exists(), "SKILL.md should be created");

    let content = fs::read_to_string(&skill_file).expect("read SKILL.md");
    assert!(content.contains("name: flowmark"), "should contain name: flowmark");
}

#[test]
fn test_install_skill_custom_base() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let custom_base = dir.path().join(".claude");

    install_skill(Some(custom_base.to_str().expect("path to str"))).expect("install skill");

    let skill_file = custom_base.join("skills").join("flowmark").join("SKILL.md");
    assert!(skill_file.exists(), "SKILL.md should be created at custom base");

    let content = fs::read_to_string(&skill_file).expect("read SKILL.md");
    assert!(content.contains("name: flowmark"), "should contain name: flowmark");
}

#[test]
fn test_install_skill_creates_directories() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let custom_base = dir.path().join("deep").join("nested").join("path");

    install_skill(Some(custom_base.to_str().expect("path to str"))).expect("install skill");

    let skill_file = custom_base.join("skills").join("flowmark").join("SKILL.md");
    assert!(skill_file.exists(), "SKILL.md should be created in deeply nested path");
}

#[test]
fn test_install_skill_overwrites_existing() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let custom_base = dir.path().join(".claude");
    let skill_dir = custom_base.join("skills").join("flowmark");
    fs::create_dir_all(&skill_dir).expect("create skill dir");

    // Write dummy content
    let skill_file = skill_dir.join("SKILL.md");
    fs::write(&skill_file, "old content").expect("write old content");

    install_skill(Some(custom_base.to_str().expect("path to str"))).expect("install skill");

    let content = fs::read_to_string(&skill_file).expect("read SKILL.md");
    assert!(!content.contains("old content"), "old content should be overwritten");
    assert!(content.contains("name: flowmark"), "should contain name: flowmark");
}

/// M5 regression test: `install_skill` with path traversal should be rejected.
#[test]
fn test_install_skill_rejects_path_traversal() {
    let result = install_skill(Some("../../tmp/evil"));
    assert!(result.is_err(), "path traversal should be rejected");
    let err = result.expect_err("should error");
    assert!(err.contains(".."), "error message should mention '..' traversal: {err}");
}

/// M5: `install_skill` with safe relative path should work.
#[test]
fn test_install_skill_safe_relative_path() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let safe_path = dir.path().join("project").join(".claude");
    install_skill(Some(safe_path.to_str().expect("path to str"))).expect("install skill");
    assert!(safe_path.join("skills/flowmark/SKILL.md").exists());
}