harn-cli 0.10.47

CLI for the Harn programming language — run, test, REPL, format, and lint
Documentation
use super::super::template_lint::{
    collect_lint_targets, lint_prompt_file_inner, lint_prompt_fix_file,
};
use super::unique_temp_dir;

#[test]
fn flags_provider_identity_branch() {
    let dir = unique_temp_dir("harn-lint-prompt-identity");
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("sample.harn.prompt");
    std::fs::write(
        &file,
        "{{ if llm.provider == \"anthropic\" }}x{{ else }}y{{ end }}\n",
    )
    .unwrap();
    let outcome = lint_prompt_file_inner(&file, None, &[]);
    assert!(outcome.has_warning);
    assert!(!outcome.has_error);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn flags_unknown_filter() {
    let dir = unique_temp_dir("harn-lint-prompt-unknown-filter");
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("sample.harn.prompt");
    std::fs::write(&file, "{{ name | uppr }}\n").unwrap();
    let outcome = lint_prompt_file_inner(&file, None, &[]);
    assert!(outcome.has_error);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn fixes_unknown_filter() {
    let dir = unique_temp_dir("harn-lint-prompt-fix-unknown-filter");
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("sample.harn.prompt");
    std::fs::write(&file, "{{ name | uppr }}\n").unwrap();

    let outcome = lint_prompt_fix_file(&file, None, &[]);

    assert!(!outcome.has_error);
    assert_eq!(
        std::fs::read_to_string(&file).unwrap(),
        "{{ name | upper }}\n"
    );
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn leaves_unfixable_unknown_filter_as_an_error() {
    let dir = unique_temp_dir("harn-lint-prompt-unfixable-unknown-filter");
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("sample.harn.prompt");
    let source = "{{ name | completely_unknown }}\n";
    std::fs::write(&file, source).unwrap();

    let outcome = lint_prompt_fix_file(&file, None, &[]);

    assert!(outcome.has_error);
    assert_eq!(std::fs::read_to_string(&file).unwrap(), source);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn flags_variant_explosion_above_threshold() {
    let dir = unique_temp_dir("harn-lint-prompt-explosion");
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("sample.harn.prompt");
    let body: String = (0..4)
        .map(|i| {
            let flag = match i {
                0 => "native_tools",
                1 => "prefers_xml_scaffolding",
                2 => "supports_assistant_prefill",
                _ => "prefers_markdown_scaffolding",
            };
            format!("{{{{ if llm.capabilities.{flag} }}}}x{{{{ end }}}}\n")
        })
        .collect();
    std::fs::write(&file, body).unwrap();
    let outcome = lint_prompt_file_inner(&file, None, &[]);
    assert!(outcome.has_warning);
    let outcome_lifted = lint_prompt_file_inner(&file, Some(5), &[]);
    assert!(!outcome_lifted.has_warning);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn respects_disabled_rules() {
    let dir = unique_temp_dir("harn-lint-prompt-disabled");
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("sample.harn.prompt");
    std::fs::write(&file, "{{ if llm.provider == \"anthropic\" }}x{{ end }}\n").unwrap();
    let outcome = lint_prompt_file_inner(
        &file,
        None,
        &["template-provider-identity-branch".to_string()],
    );
    assert!(!outcome.has_warning);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn collects_prompt_targets_from_directories() {
    let dir = unique_temp_dir("harn-lint-prompt-collect");
    // The `.gitignore` below is only honored inside a project, so the tree
    // needs the repository marker `harn_vm::ignore_policy` looks for. See
    // `super::target_discovery`.
    std::fs::create_dir_all(dir.join(".git")).unwrap();
    std::fs::create_dir_all(dir.join("nested")).unwrap();
    std::fs::create_dir_all(dir.join("target").join("debug")).unwrap();
    std::fs::create_dir_all(dir.join(".harn").join("cache")).unwrap();
    std::fs::write(dir.join("a.harn.prompt"), "x").unwrap();
    std::fs::write(dir.join("nested").join("b.harn.prompt"), "y").unwrap();
    std::fs::write(dir.join("ignored_by_gitignore.prompt"), "ignored").unwrap();
    std::fs::write(dir.join(".gitignore"), "ignored_by_gitignore.prompt\n").unwrap();
    std::fs::write(
        dir.join("target").join("debug").join("ignored.harn.prompt"),
        "z",
    )
    .unwrap();
    std::fs::write(dir.join(".harn").join("cache").join("ignored.prompt"), "z").unwrap();
    std::fs::write(dir.join("c.txt"), "ignore").unwrap();
    let target = dir.display().to_string();
    let (_harn_files, files) = collect_lint_targets(&[target.as_str()]);
    assert_eq!(files.len(), 2);
    assert!(files.contains(&dir.join("a.harn.prompt")));
    assert!(files.contains(&dir.join("nested").join("b.harn.prompt")));
    assert!(!files.contains(&dir.join("ignored_by_gitignore.prompt")));

    let explicit = dir
        .join("ignored_by_gitignore.prompt")
        .display()
        .to_string();
    let (_harn_files, explicit_files) = collect_lint_targets(&[explicit.as_str()]);
    assert_eq!(
        explicit_files,
        vec![dir.join("ignored_by_gitignore.prompt")]
    );
    let _ = std::fs::remove_dir_all(&dir);
}