main-spex 0.23.0

AI-first development framework that turns specifications into deterministic, verifiable code
use std::fs;

use tempfile::tempdir;

use crate::common::{
    assert_fails_mentioning, canonical_display, copy_project, dummy_project_fixture, listed_ids,
    run_spex, runners_project_fixture, temp_project,
};

const EXPECTED_GLOB_RULE: &str = r#"- id: glob-runner
  title: Glob runner
  sync_with_upstream: true
  description: Exercises the glob runner with a forbidden path and a required path.
  acceptance_criteria:
  - MUST provide a README.md in the package root
  - MUST NOT keep backup files in a package
  checks:
  - name: no-backup-files
    type: glob
    pattern: '**/*.bak'
    severity: warn
  - name: require-readme
    type: glob
    pattern: README.md
    negate: true
    severity: error
"#;

const EXPECTED_GLOB_RULE_FOR_AI: &str = r#"- id: glob-runner
  path: {spex_dir}/rules/glob-runner.yaml
  title: Glob runner
  description: Exercises the glob runner with a forbidden path and a required path.
  acceptance_criteria:
  - MUST provide a README.md in the package root
  - MUST NOT keep backup files in a package
"#;

#[test]
fn rules_lists_each_rule_once_however_many_packages_use_it() {
    let (_temp_dir, project) = temp_project(&runners_project_fixture());

    let rules = run_spex(&project, &["rules"], "");

    assert!(rules.status.success(), "rules should exit 0");
    assert_eq!(
        listed_ids(&rules.stdout),
        [
            "ast-grep-runner",
            "glob-runner",
            "regexp-runner",
            "script-failures",
            "script-runner"
        ]
    );
}

#[test]
fn rules_package_limits_output_to_the_rules_of_named_packages() {
    let (_temp_dir, project) = temp_project(&runners_project_fixture());

    let frontend = run_spex(&project, &["rules", "--package", "frontend"], "");
    let api = run_spex(&project, &["rules", "--package", "api"], "");
    let spex = run_spex(&project, &["rules", "--package", "spex"], "");

    assert_eq!(
        listed_ids(&frontend.stdout),
        [
            "ast-grep-runner",
            "glob-runner",
            "regexp-runner",
            "script-runner"
        ]
    );
    assert_eq!(
        listed_ids(&api.stdout),
        [
            "ast-grep-runner",
            "glob-runner",
            "regexp-runner",
            "script-failures",
            "script-runner"
        ]
    );
    assert!(spex.status.success());
    assert_eq!(String::from_utf8_lossy(&spex.stdout), "[]\n");
}

#[test]
fn rules_package_matches_a_package_by_short_or_full_name() {
    let (_temp_dir, project) = temp_project(&runners_project_fixture());

    let by_short_name = run_spex(&project, &["rules", "--package", "frontend"], "");
    let by_full_name = run_spex(
        &project,
        &["rules", "--package", "runners-project-frontend"],
        "",
    );

    assert!(by_full_name.status.success());
    assert_eq!(by_full_name.stdout, by_short_name.stdout);
}

#[test]
fn rules_id_limits_output_to_the_named_rules() {
    let (_temp_dir, project) = temp_project(&runners_project_fixture());

    let rules = run_spex(
        &project,
        &["rules", "--id", "script-failures", "glob-runner"],
        "",
    );

    assert!(rules.status.success());
    assert_eq!(
        listed_ids(&rules.stdout),
        ["glob-runner", "script-failures"]
    );
}

#[test]
fn rules_prints_the_full_rule_content_by_default() {
    let (_temp_dir, project) = temp_project(&runners_project_fixture());

    let rules = run_spex(&project, &["rules", "--id", "glob-runner"], "");

    assert!(rules.status.success());
    assert_eq!(String::from_utf8_lossy(&rules.stdout), EXPECTED_GLOB_RULE);
}

#[test]
fn rules_ai_drops_checks_and_the_sync_flag_and_adds_the_rule_path() {
    let (_temp_dir, project) = temp_project(&runners_project_fixture());

    let rules = run_spex(&project, &["rules", "--ai", "--id", "glob-runner"], "");

    assert!(rules.status.success());
    let spex_dir = canonical_display(&project.join("runners-project-spex"));
    assert_eq!(
        String::from_utf8_lossy(&rules.stdout),
        EXPECTED_GLOB_RULE_FOR_AI.replace("{spex_dir}", &spex_dir)
    );
}

#[test]
fn rules_rejects_an_unknown_rule_id() {
    let (_temp_dir, project) = temp_project(&runners_project_fixture());

    let rules = run_spex(&project, &["rules", "--id", "glob-runner", "bogus"], "");

    assert_fails_mentioning(&rules, "unknown rule id `bogus`");
    assert!(rules.stdout.is_empty());
}

#[test]
fn rules_rejects_an_unknown_package() {
    let (_temp_dir, project) = temp_project(&runners_project_fixture());

    let rules = run_spex(&project, &["rules", "--package", "bogus"], "");

    assert_fails_mentioning(&rules, "unknown package `bogus`");
    assert!(rules.stdout.is_empty());
}

#[test]
fn rules_output_flag_writes_file_matching_stdout() {
    let temp_dir = tempdir().expect("create temp dir");
    let project = temp_dir.path().join("proj");

    copy_project(&dummy_project_fixture(), &project);

    let stdout_run = run_spex(&project, &["rules"], "");
    assert!(
        stdout_run.status.success(),
        "rules failed (exit {:?})\nstderr:\n{}",
        stdout_run.status.code(),
        String::from_utf8_lossy(&stdout_run.stderr),
    );
    let stdout_yaml = String::from_utf8_lossy(&stdout_run.stdout).into_owned();
    assert!(!stdout_yaml.is_empty(), "expected rules on stdout");

    let output_path = temp_dir.path().join("rules.yaml");
    let file_run = run_spex(
        &project,
        &[
            "rules",
            "--output",
            output_path.to_str().expect("utf8 path"),
        ],
        "",
    );
    assert!(
        file_run.status.success(),
        "rules --output failed (exit {:?})\nstderr:\n{}",
        file_run.status.code(),
        String::from_utf8_lossy(&file_run.stderr),
    );
    assert!(
        file_run.stdout.is_empty(),
        "rules --output should not print to stdout"
    );

    let file_yaml = fs::read_to_string(&output_path).expect("read --output file");
    assert_eq!(
        file_yaml, stdout_yaml,
        "--output file contents should match stdout output"
    );
}