fallow-cli 3.23.0

CLI for fallow, 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 common::{parse_json, run_fallow_raw, run_fallow_raw_with_env};

#[test]
fn help_only_advertises_supported_options() {
    for help_flag in ["-h", "--help"] {
        let output = run_fallow_raw(&["doctor", help_flag]);

        assert_eq!(output.code, 0, "doctor help failed: {}", output.stderr);
        assert!(output.stderr.is_empty());
        for supported in ["--root", "--config", "--format", "--pretty", "--quiet"] {
            assert!(
                output.stdout.contains(supported),
                "doctor {help_flag} omitted supported option {supported}"
            );
        }
        for unsupported in [
            "--output-file",
            "--allow-remote-extends",
            "--changed-since",
            "--workspace",
            "--ci",
            "--type-aware",
            "sarif",
            "compact",
        ] {
            assert!(
                !output.stdout.contains(unsupported),
                "doctor {help_flag} advertised unsupported option {unsupported}"
            );
        }
    }
}

#[test]
fn output_file_is_rejected_without_creating_or_modifying_a_target() {
    let root = tempfile::tempdir().expect("temp root");
    let output_path = root.path().join("doctor.json");
    let root_text = root.path().to_string_lossy();
    let output_text = output_path.to_string_lossy();
    let output = run_fallow_raw(&[
        "doctor",
        "--root",
        root_text.as_ref(),
        "--format",
        "json",
        "--output-file",
        output_text.as_ref(),
        "--quiet",
    ]);

    assert_eq!(output.code, 2);
    assert!(!output_path.exists());
    assert!(output.stderr.is_empty());
    let json = parse_json(&output);
    assert_eq!(json["error"], true);
    assert!(
        json["message"]
            .as_str()
            .is_some_and(|message| message.contains("writes its report to stdout"))
    );

    let original = b"important user content\n";
    std::fs::write(&output_path, original).expect("write existing file");
    let output = run_fallow_raw(&[
        "doctor",
        "--root",
        root_text.as_ref(),
        "--output-file",
        output_text.as_ref(),
        "--quiet",
    ]);
    assert_eq!(output.code, 2);
    assert_eq!(std::fs::read(&output_path).expect("read output"), original);
    assert!(output.stderr.contains("writes its report to stdout"));
}

#[test]
fn zero_config_json_is_stable_and_path_free() {
    let root = tempfile::tempdir().expect("temp root");
    let root_text = root.path().to_string_lossy();
    let output = run_fallow_raw(&[
        "doctor",
        "--root",
        root_text.as_ref(),
        "--format",
        "json",
        "--quiet",
    ]);

    assert_eq!(output.code, 0, "doctor failed: {}", output.stderr);
    assert!(output.stderr.is_empty());
    assert!(!output.stdout.contains(root_text.as_ref()));
    let json = parse_json(&output);
    assert_eq!(json["kind"], "doctor");
    assert_eq!(json["schema_version"], 1);
    assert_eq!(json["root"], ".");
    assert_eq!(json["status"], "pass");
    assert_eq!(
        json["checks"]
            .as_array()
            .expect("checks array")
            .iter()
            .map(|check| check["id"].as_str().expect("check id"))
            .collect::<Vec<_>>(),
        ["root", "config", "workspaces", "plugins", "type-aware"]
    );
}

#[test]
fn invalid_config_returns_complete_failed_json_report() {
    let root = tempfile::tempdir().expect("temp root");
    std::fs::write(root.path().join(".fallowrc.json"), "{").expect("write invalid config");
    let root_text = root.path().to_string_lossy();
    let output = run_fallow_raw(&[
        "doctor",
        "--root",
        root_text.as_ref(),
        "--format",
        "json",
        "--quiet",
    ]);

    assert_eq!(output.code, 2);
    assert!(output.stderr.is_empty());
    assert!(!output.stdout.contains(root_text.as_ref()));
    let json = parse_json(&output);
    assert_eq!(json["kind"], "doctor");
    assert_eq!(json["status"], "fail");
    assert_eq!(json["checks"].as_array().map(Vec::len), Some(5));
    assert_eq!(json["checks"][1]["id"], "config");
    assert_eq!(json["checks"][1]["status"], "fail");
}

#[test]
fn human_report_is_readable_and_path_free() {
    let root = tempfile::tempdir().expect("temp root");
    let root_text = root.path().to_string_lossy();
    let output = run_fallow_raw(&["doctor", "--root", root_text.as_ref(), "--quiet"]);

    assert_eq!(output.code, 0, "doctor failed: {}", output.stderr);
    assert!(output.stdout.starts_with("Fallow doctor (.)\n"));
    assert!(output.stdout.contains("[OK] root:"));
    assert!(output.stdout.contains("Status: ready"));
    assert!(!output.stdout.contains(root_text.as_ref()));
}

#[test]
fn invalid_root_uses_shared_failure_prefix_and_actionable_private_message() {
    let root = tempfile::tempdir().expect("temp root");
    let missing = root.path().join("missing");
    let missing_text = missing.to_string_lossy();
    let output = run_fallow_raw(&["doctor", "--root", missing_text.as_ref(), "--quiet"]);

    assert_eq!(output.code, 2);
    assert!(output.stderr.is_empty());
    assert!(output.stdout.contains("[X] root:"));
    assert!(
        output
            .stdout
            .contains("Set --root to an existing, readable directory.")
    );
    assert!(!output.stdout.contains(missing_text.as_ref()));
}

#[test]
fn analysis_flags_are_rejected_instead_of_ignored() {
    let output = run_fallow_raw(&["doctor", "--changed-since", "main", "--format", "json"]);

    assert_eq!(output.code, 2);
    let json = parse_json(&output);
    assert_eq!(json["error"], true);
    assert!(
        json["message"]
            .as_str()
            .is_some_and(|message| message.contains("--changed-since"))
    );
}

#[test]
fn type_aware_environment_requirement_controls_readiness() {
    let root = tempfile::tempdir().expect("temp root");
    let root_text = root.path().to_string_lossy();
    let output = run_fallow_raw_with_env(
        &[
            "doctor",
            "--root",
            root_text.as_ref(),
            "--format",
            "json",
            "--quiet",
        ],
        &[
            ("FALLOW_TYPE_AWARE", "true"),
            ("FALLOW_TYPE_AWARE_REQUIRE", "complete"),
            (
                "FALLOW_TYPE_AWARE_BIN",
                "/definitely/missing/fallow-type-aware",
            ),
        ],
    );

    assert_eq!(output.code, 2);
    let json = parse_json(&output);
    assert_eq!(json["status"], "fail");
    assert_eq!(json["checks"][4]["id"], "type-aware");
    assert_eq!(json["checks"][4]["status"], "fail");
    assert_eq!(json["checks"][4]["required"], true);
    assert_eq!(json["checks"][4]["remediation"]["cwd"], ".");
    assert_eq!(json["checks"][4]["remediation"]["mutating"], true);
}