main-spex 0.21.0

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

use crate::common::{
    assert_fails_mentioning, dummy_project_fixture, run_spex, run_spex_with_env, temp_project,
};

const EXPECTED_INFO: &str = r#"{
  "spex_dir": {spex_dir},
  "project_name": "dummy-project",
  "cli_version": "0.8.8",
  "layout": "multi-repo",
  "agent_root": {spex_dir},
  "description": "Minimal dummy project used by spex's own test suite to exercise `spex check` and `spex coverage` on a project whose specs cover every lifecycle state.",
  "rules_unused": [],
  "packages": [
    {
      "package_name": "dummy-project-api",
      "package_short_name": "api",
      "path": {api_dir},
      "contracts": [
        {
          "path": "./contracts.md",
          "description": "Interface contract notes for the api."
        }
      ],
      "rules": [
        "no-todo-comments"
      ],
      "ignored_errors": []
    },
    {
      "package_name": "dummy-project-frontend",
      "package_short_name": "frontend",
      "path": {frontend_dir},
      "contracts": [],
      "rules": [],
      "ignored_errors": []
    },
    {
      "package_name": "dummy-project-spex",
      "package_short_name": "spex",
      "path": {spex_dir},
      "contracts": [],
      "rules": [],
      "ignored_errors": []
    }
  ],
  "coverage_ignore": []
}
"#;

fn with_project_dirs(template: &str, project: &Path) -> String {
    let as_json = |package_dir: &str| {
        let dir = project
            .join(package_dir)
            .canonicalize()
            .expect("canonical package dir");
        serde_json::to_string(&dir).expect("package dir as JSON")
    };
    template
        .replace("{spex_dir}", &as_json("dummy-project-spex"))
        .replace("{api_dir}", &as_json("dummy-project-api"))
        .replace("{frontend_dir}", &as_json("dummy-project-frontend"))
}

const EXPECTED_INFO_API: &str = r#"{
  "spex_dir": {spex_dir},
  "project_name": "dummy-project",
  "cli_version": "0.8.8",
  "layout": "multi-repo",
  "agent_root": {spex_dir},
  "description": "Minimal dummy project used by spex's own test suite to exercise `spex check` and `spex coverage` on a project whose specs cover every lifecycle state.",
  "rules_unused": [],
  "packages": [
    {
      "package_name": "dummy-project-api",
      "package_short_name": "api",
      "path": {api_dir},
      "contracts": [
        {
          "path": "./contracts.md",
          "description": "Interface contract notes for the api."
        }
      ],
      "rules": [
        "no-todo-comments"
      ],
      "ignored_errors": []
    }
  ],
  "coverage_ignore": []
}
"#;

fn listed_short_names(stdout: &[u8]) -> Vec<String> {
    let info: serde_json::Value = serde_json::from_slice(stdout).expect("info prints JSON");
    info["packages"]
        .as_array()
        .expect("packages list")
        .iter()
        .map(|package| {
            package["package_short_name"]
                .as_str()
                .expect("package_short_name")
                .to_string()
        })
        .collect()
}

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

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

    assert!(info.status.success(), "info should exit 0");
    assert_eq!(
        String::from_utf8_lossy(&info.stdout),
        with_project_dirs(EXPECTED_INFO, &project)
    );
}

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

    let info = run_spex(&project.join("dummy-project-api"), &["info"], "");

    assert!(info.status.success(), "info should exit 0");
    assert_eq!(
        String::from_utf8_lossy(&info.stdout),
        with_project_dirs(EXPECTED_INFO, &project)
    );
}

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

    let by_short_name = run_spex(&project, &["info", "--package", "api"], "");
    let by_full_name = run_spex(&project, &["info", "--package", "dummy-project-api"], "");
    let several = run_spex(&project, &["info", "--package", "frontend", "api"], "");

    let expected = with_project_dirs(EXPECTED_INFO_API, &project);

    assert!(by_short_name.status.success());
    assert_eq!(String::from_utf8_lossy(&by_short_name.stdout), expected);
    assert_eq!(
        String::from_utf8_lossy(&by_full_name.stdout),
        expected,
        "--package should match a package by full name as well as short name",
    );
    assert_eq!(listed_short_names(&several.stdout), ["api", "frontend"]);
}

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

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

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

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

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

    let stderr = String::from_utf8_lossy(&info.stderr);
    assert_eq!(info.status.code(), Some(2));
    assert!(
        stderr.ends_with("error: unknown package `bogus` — not listed in spex.yaml\n"),
        "unexpected stderr:\n{stderr}",
    );
    assert!(
        !stderr.contains('\u{1b}') && !stderr.contains("╰╴"),
        "stderr carries colour or a source location:\n{stderr}",
    );
}

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

    let info = run_spex_with_env(
        &project,
        &["info", "--package", "bogus"],
        "",
        &[("RUST_BACKTRACE", "1")],
    );

    let stderr = String::from_utf8_lossy(&info.stderr);
    assert_eq!(info.status.code(), Some(2));
    assert!(
        stderr.contains("error: unknown package `bogus` — not listed in spex.yaml\n\n"),
        "the error line is missing:\n{stderr}",
    );
    assert!(
        stderr.contains("[src/config/spex.rs:"),
        "the error location is missing:\n{stderr}",
    );
    assert!(
        stderr.contains("Backtrace (captured)"),
        "the backtrace is missing:\n{stderr}",
    );
}