fallow-cli 2.94.0

CLI for fallow, Rust-native codebase intelligence for TypeScript and JavaScript
Documentation
#![allow(
    clippy::unwrap_used,
    clippy::expect_used,
    reason = "tests use unwrap and expect to keep fixture setup concise"
)]

#[path = "common/mod.rs"]
mod common;

use std::fs;
use std::path::Path;

use common::{parse_json, run_fallow_raw, run_fallow_raw_with_env};
use tempfile::{TempDir, tempdir};

fn write_file(path: &Path, contents: &str) {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("create parent directories");
    }
    fs::write(path, contents).expect("write file");
}

fn create_branchy_project(name: &str) -> TempDir {
    let dir = tempdir().unwrap();
    write_file(
        &dir.path().join("package.json"),
        &format!(r#"{{"name":"{name}","type":"module"}}"#),
    );
    write_file(
        &dir.path().join("src/index.ts"),
        "export function branchy(n: number): number {
  if (n < 0) return -1;
  if (n === 0) return 0;
  if (n < 10) return 1;
  if (n < 100) return 2;
  if (n < 1000) return 3;
  if (n < 10000) return 4;
  return 5;
}
",
    );
    dir
}

fn write_config(root: &Path, body: &str) {
    write_file(&root.join(".fallowrc.json"), body);
}

fn write_branchy_istanbul_coverage(coverage_path: &Path, coverage_source_path: &str) {
    fs::create_dir_all(coverage_path.parent().unwrap()).unwrap();
    let mut coverage = serde_json::Map::new();
    coverage.insert(
        coverage_source_path.to_owned(),
        serde_json::json!({
            "path": coverage_source_path,
            "statementMap": {},
            "fnMap": {
                "0": {
                    "name": "branchy",
                    "line": 1,
                    "decl": {
                        "start": { "line": 1, "column": 16 },
                        "end": { "line": 1, "column": 23 }
                    },
                    "loc": {
                        "start": { "line": 1, "column": 43 },
                        "end": { "line": 9, "column": 1 }
                    }
                }
            },
            "branchMap": {},
            "s": {},
            "f": { "0": 1 },
            "b": {}
        }),
    );
    fs::write(coverage_path, serde_json::to_string(&coverage).unwrap()).unwrap();
}

fn combined_health_model(json: &serde_json::Value) -> Option<&str> {
    json.pointer("/health/summary/coverage_model")
        .and_then(serde_json::Value::as_str)
}

fn standalone_health_model(json: &serde_json::Value) -> Option<&str> {
    json.pointer("/summary/coverage_model")
        .and_then(serde_json::Value::as_str)
}

fn combined_health_args(root: &Path) -> Vec<&str> {
    vec![
        "--root",
        root.to_str().unwrap(),
        "--only",
        "health",
        "--format",
        "json",
        "--quiet",
    ]
}

#[test]
fn combined_cli_coverage_and_root_feed_health_crap_scoring() {
    let dir = create_branchy_project("combined-cli-coverage");
    write_config(dir.path(), r#"{"health":{"maxCrap":10}}"#);

    let without_coverage = run_fallow_raw(&combined_health_args(dir.path()));
    assert_eq!(
        without_coverage.code, 0,
        "static health run should still complete before Istanbul coverage is supplied. stderr: {}",
        without_coverage.stderr
    );
    let json = parse_json(&without_coverage);
    assert_eq!(combined_health_model(&json), Some("static_estimated"));

    let coverage_path = dir.path().join("artifacts/coverage-final.json");
    write_branchy_istanbul_coverage(&coverage_path, "/ci/workspace/src/index.ts");

    let mut args = combined_health_args(dir.path());
    args.extend([
        "--coverage",
        coverage_path.to_str().unwrap(),
        "--coverage-root",
        "/ci/workspace",
    ]);
    let with_coverage = run_fallow_raw(&args);
    assert_eq!(
        with_coverage.code, 0,
        "Istanbul coverage should lower CRAP below the combined health threshold. stderr: {}",
        with_coverage.stderr
    );
    let json = parse_json(&with_coverage);
    assert_eq!(combined_health_model(&json), Some("istanbul"));
}

#[test]
fn combined_env_coverage_fallback_feeds_health_crap_scoring() {
    let dir = create_branchy_project("combined-env-coverage");
    write_config(dir.path(), r#"{"health":{"maxCrap":10}}"#);
    let coverage_path = dir.path().join("artifacts/env-coverage.json");
    let source_path = dir.path().join("src/index.ts");
    write_branchy_istanbul_coverage(&coverage_path, &source_path.to_string_lossy());

    let output = run_fallow_raw_with_env(
        &combined_health_args(dir.path()),
        &[("FALLOW_COVERAGE", coverage_path.to_str().unwrap())],
    );
    assert_eq!(
        output.code, 0,
        "FALLOW_COVERAGE should feed combined health. stderr: {}",
        output.stderr
    );
    let json = parse_json(&output);
    assert_eq!(combined_health_model(&json), Some("istanbul"));
}

#[test]
fn combined_config_coverage_fallback_feeds_health_crap_scoring() {
    let dir = create_branchy_project("combined-config-coverage");
    let coverage_path = dir.path().join("artifacts/coverage-final.json");
    write_branchy_istanbul_coverage(&coverage_path, "/ci/workspace/src/index.ts");
    write_config(
        dir.path(),
        r#"{"health":{"maxCrap":10,"coverage":"artifacts/coverage-final.json","coverageRoot":"/ci/workspace"}}"#,
    );

    let output = run_fallow_raw(&combined_health_args(dir.path()));
    assert_eq!(
        output.code, 0,
        "health.coverage should feed combined health. stderr: {}",
        output.stderr
    );
    let json = parse_json(&output);
    assert_eq!(combined_health_model(&json), Some("istanbul"));
}

#[test]
fn mixed_precedence_resolves_coverage_and_root_independently() {
    let dir = create_branchy_project("combined-coverage-precedence");
    let coverage_path = dir.path().join("artifacts/coverage-final.json");
    write_branchy_istanbul_coverage(&coverage_path, "/ci/workspace/src/index.ts");
    write_config(
        dir.path(),
        r#"{"health":{"maxCrap":10,"coverageRoot":"/wrong/root"}}"#,
    );

    let mut args = combined_health_args(dir.path());
    args.extend(["--coverage", coverage_path.to_str().unwrap()]);
    let cli_coverage_env_root =
        run_fallow_raw_with_env(&args, &[("FALLOW_COVERAGE_ROOT", "/ci/workspace")]);
    assert_eq!(
        cli_coverage_env_root.code, 0,
        "env coverage root should pair with CLI coverage. stderr: {}",
        cli_coverage_env_root.stderr
    );
    let json = parse_json(&cli_coverage_env_root);
    assert_eq!(combined_health_model(&json), Some("istanbul"));

    let env_coverage_config_root = {
        write_config(
            dir.path(),
            r#"{"health":{"maxCrap":10,"coverageRoot":"/ci/workspace"}}"#,
        );
        run_fallow_raw_with_env(
            &combined_health_args(dir.path()),
            &[("FALLOW_COVERAGE", coverage_path.to_str().unwrap())],
        )
    };
    assert_eq!(
        env_coverage_config_root.code, 0,
        "config coverage root should pair with env coverage. stderr: {}",
        env_coverage_config_root.stderr
    );
    let json = parse_json(&env_coverage_config_root);
    assert_eq!(combined_health_model(&json), Some("istanbul"));

    write_config(
        dir.path(),
        r#"{"health":{"maxCrap":10,"coverageRoot":"/wrong/root"}}"#,
    );
    let mut args = combined_health_args(dir.path());
    args.extend([
        "--coverage",
        coverage_path.to_str().unwrap(),
        "--coverage-root",
        "/ci/workspace",
    ]);
    let cli_root_overrides_config = run_fallow_raw(&args);
    assert_eq!(
        cli_root_overrides_config.code, 0,
        "CLI coverage root should override config coverage root. stderr: {}",
        cli_root_overrides_config.stderr
    );
    let json = parse_json(&cli_root_overrides_config);
    assert_eq!(combined_health_model(&json), Some("istanbul"));
}

#[test]
fn health_config_coverage_fallback_feeds_standalone_health() {
    let dir = create_branchy_project("health-config-coverage");
    let coverage_path = dir.path().join("artifacts/coverage-final.json");
    write_branchy_istanbul_coverage(&coverage_path, "/ci/workspace/src/index.ts");
    write_config(
        dir.path(),
        r#"{"health":{"maxCrap":10,"coverage":"artifacts/coverage-final.json","coverageRoot":"/ci/workspace"}}"#,
    );

    let output = run_fallow_raw(&[
        "health",
        "--root",
        dir.path().to_str().unwrap(),
        "--format",
        "json",
        "--quiet",
    ]);
    assert_eq!(
        output.code, 0,
        "health.coverage should feed standalone health. stderr: {}",
        output.stderr
    );
    let json = parse_json(&output);
    assert_eq!(standalone_health_model(&json), Some("istanbul"));
}

#[test]
fn relative_config_coverage_root_is_structured_exit_two() {
    let dir = create_branchy_project("relative-config-coverage-root");
    write_config(dir.path(), r#"{"health":{"coverageRoot":"src"}}"#);

    let output = run_fallow_raw(&combined_health_args(dir.path()));
    assert_eq!(
        output.code, 2,
        "relative health.coverageRoot should be rejected before analysis. stderr: {}",
        output.stderr
    );
    let json = parse_json(&output);
    assert_eq!(json["error"], serde_json::json!(true));
    let message = json["message"].as_str().expect("message should be present");
    assert!(
        message.contains("--coverage-root expects an absolute path")
            && message.contains("got 'src'"),
        "unexpected error message: {message}"
    );
}

#[test]
fn bare_coverage_flags_before_subcommand_are_rejected() {
    let output = run_fallow_raw(&[
        "--coverage",
        "coverage/coverage-final.json",
        "dead-code",
        "--format",
        "json",
        "--quiet",
    ]);
    assert_eq!(
        output.code, 2,
        "pre-subcommand bare coverage should be rejected. stderr: {}",
        output.stderr
    );
    let json = parse_json(&output);
    assert_eq!(json["error"], serde_json::json!(true));
    let message = json["message"].as_str().expect("message should be present");
    assert!(
        message.contains("bare combined-mode flags"),
        "unexpected error message: {message}"
    );
}