fallow-cli 3.24.1

CLI for fallow, codebase intelligence for TypeScript and JavaScript
Documentation
#![allow(
    clippy::unwrap_used,
    clippy::expect_used,
    reason = "integration tests keep fixture setup concise"
)]

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

use common::{CommandOutput, parse_json, run_fallow_in_root};

fn run_guard(root: &std::path::Path, args: &[&str]) -> CommandOutput {
    run_fallow_in_root("guard", root, args)
}

fn write_base_project(root: &std::path::Path) {
    std::fs::create_dir_all(root.join("src/app")).expect("create app dir");
    std::fs::create_dir_all(root.join("src/domain")).expect("create domain dir");
    std::fs::create_dir_all(root.join("src/shared")).expect("create shared dir");
    std::fs::write(root.join("package.json"), "{\"name\":\"guard-fixture\"}\n")
        .expect("write package json");
    std::fs::write(root.join("src/domain/user.ts"), "export const user = 1;\n")
        .expect("write source");
}

fn write_guard_project(root: &std::path::Path) {
    write_base_project(root);
    std::fs::create_dir_all(root.join("rule-packs")).expect("create rule pack dir");
    std::fs::write(
        root.join(".fallowrc.json"),
        r#"{
  "rules": {
    "boundary-violation": "error",
    "policy-violation": "warn"
  },
  "boundaries": {
    "zones": [
      { "name": "domain", "patterns": ["src/domain/**"] },
      { "name": "app", "patterns": ["src/app/**"] },
      { "name": "shared", "patterns": ["src/shared/**"] }
    ],
    "rules": [
      { "from": "domain", "allow": ["shared"], "allowTypeOnly": ["shared"] }
    ],
    "calls": {
      "forbidden": [
        { "from": "domain", "callee": "child_process.*" }
      ]
    }
  },
  "rulePacks": ["rule-packs/team-policy.jsonc"]
}
"#,
    )
    .expect("write config");
    std::fs::write(
        root.join("rule-packs/team-policy.jsonc"),
        r#"{
  "version": 1,
  "name": "team-policy",
  "rules": [
    {
      "id": "pure-domain",
      "kind": "banned-effect",
      "effects": ["network"],
      "zones": ["domain"],
      "severity": "error",
      "message": "Domain code must inject network access through ports."
    }
  ]
}
"#,
    )
    .expect("write rule pack");
}

#[test]
fn guard_json_reports_zone_boundary_and_policy_rules() {
    let dir = tempfile::tempdir().expect("tempdir");
    write_guard_project(dir.path());

    let output = run_guard(
        dir.path(),
        &["src/domain/user.ts", "--format", "json", "--quiet"],
    );

    assert_eq!(output.code, 0, "stderr: {}", output.stderr);
    let json = parse_json(&output);
    let file = &json["files"][0];
    assert_eq!(json["kind"], "guard");
    assert_eq!(file["path"], "src/domain/user.ts");
    assert_eq!(file["zone"]["name"], "domain");
    assert_eq!(
        file["boundary"]["allowed_zones"],
        serde_json::json!(["domain", "shared"])
    );
    assert_eq!(
        file["boundary"]["forbidden_calls"],
        serde_json::json!(["child_process.*"])
    );
    assert_eq!(file["policy_rules"][0]["rule_id"], "pure-domain");
    assert_eq!(
        file["policy_rules"][0]["suppress_token"],
        "policy-violation:team-policy/pure-domain"
    );
}

#[test]
fn guard_json_honors_zone_scoped_policy_rules() {
    let dir = tempfile::tempdir().expect("tempdir");
    write_guard_project(dir.path());

    let output = run_guard(
        dir.path(),
        &["src/app/page.ts", "--format", "json", "--quiet"],
    );

    assert_eq!(output.code, 0, "stderr: {}", output.stderr);
    let json = parse_json(&output);
    let file = &json["files"][0];
    assert_eq!(file["zone"]["name"], "app");
    assert_eq!(file["policy_rules"], serde_json::json!([]));
}

#[test]
fn guard_json_reports_nonexistent_target_without_failing() {
    let dir = tempfile::tempdir().expect("tempdir");
    write_guard_project(dir.path());

    let output = run_guard(
        dir.path(),
        &["src/domain/new-file.ts", "--format", "json", "--quiet"],
    );

    assert_eq!(output.code, 0, "stderr: {}", output.stderr);
    let json = parse_json(&output);
    assert_eq!(json["files"][0]["exists"], false);
    assert_eq!(json["files"][0]["path"], "src/domain/new-file.ts");
}

#[test]
fn guard_rejects_path_outside_root() {
    let dir = tempfile::tempdir().expect("tempdir");
    write_guard_project(dir.path());

    let output = run_guard(
        dir.path(),
        &["../outside.ts", "--format", "json", "--quiet"],
    );

    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("outside project root"))
    );
}

#[test]
fn guard_human_reports_zone_and_suppress_hint() {
    let dir = tempfile::tempdir().expect("tempdir");
    write_guard_project(dir.path());

    let output = run_guard(dir.path(), &["src/domain/user.ts", "--quiet"]);

    assert_eq!(output.code, 0, "stderr: {}", output.stderr);
    assert!(output.stdout.contains("src/domain/user.ts (zone: domain)"));
    assert!(output.stdout.contains("policy rules:"));
    assert!(
        output
            .stdout
            .contains("fallow-ignore-next-line policy-violation:team-policy/pure-domain")
    );
}

fn write_required_coverage_project(root: &std::path::Path) {
    write_base_project(root);
    std::fs::create_dir_all(root.join("src/generated")).expect("create generated dir");
    std::fs::write(
        root.join(".fallowrc.json"),
        r#"{
  "boundaries": {
    "zones": [
      { "name": "domain", "patterns": ["src/domain/**"] }
    ],
    "coverage": {
      "requireAllFiles": true,
      "allowUnmatched": ["src/generated/**"]
    }
  }
}
"#,
    )
    .expect("write config");
}

#[test]
fn guard_human_states_coverage_requirement_for_unzoned_file() {
    let dir = tempfile::tempdir().expect("tempdir");
    write_required_coverage_project(dir.path());

    let required = run_guard(dir.path(), &["src/app/page.ts", "--quiet"]);

    assert_eq!(required.code, 0, "stderr: {}", required.stderr);
    assert!(
        required.stdout.contains("requireAllFiles")
            && required.stdout.contains("boundary-coverage"),
        "human output must state the coverage requirement: {}",
        required.stdout
    );
    assert!(
        !required.stdout.contains("unrestricted for boundary checks"),
        "the unrestricted note must not claim to cover the coverage check: {}",
        required.stdout
    );

    let exempt = run_guard(dir.path(), &["src/generated/client.ts", "--quiet"]);

    assert_eq!(exempt.code, 0, "stderr: {}", exempt.stderr);
    assert!(
        !exempt.stdout.contains("requireAllFiles"),
        "allowUnmatched paths must not state a coverage requirement: {}",
        exempt.stdout
    );
}

#[test]
fn guard_json_reports_unrestricted_when_no_boundaries_or_packs_configured() {
    let dir = tempfile::tempdir().expect("tempdir");
    write_base_project(dir.path());
    std::fs::write(dir.path().join(".fallowrc.json"), "{}\n").expect("write config");

    let output = run_guard(
        dir.path(),
        &["src/domain/user.ts", "--format", "json", "--quiet"],
    );

    assert_eq!(output.code, 0, "stderr: {}", output.stderr);
    let json = parse_json(&output);
    let file = &json["files"][0];
    assert_eq!(file["boundary"]["unrestricted"], true);
    assert!(
        file["notes"]
            .as_array()
            .is_some_and(|notes| !notes.is_empty())
    );
}