main-spex 0.21.0

AI-first development framework that turns specifications into deterministic, verifiable code
#[cfg(test)]
mod tests {
    use super::super::*;
    use crate::support::testing;
    use indoc::indoc;
    use tempfile::tempdir;

    const SPEX_REPO_DIR: &str = "demo-spex";

    fn project() -> tempfile::TempDir {
        let project = tempdir().expect("project");
        let spex = project.path().join(SPEX_REPO_DIR);
        std::fs::create_dir_all(project.path().join("demo-api/src")).expect("package");
        std::fs::create_dir_all(&spex).expect("spex dir");
        std::fs::write(spex.join(SPEX_CONFIG_FILE), "project_name: demo\n").expect("config");
        project
    }

    #[test]
    fn finds_from_project_root() {
        let project = project();
        let found = SpexConfig::find_file(project.path()).expect("found");
        assert_eq!(
            found,
            project.path().join(SPEX_REPO_DIR).join(SPEX_CONFIG_FILE)
        );
    }

    #[test]
    fn finds_from_spex_dir() {
        let project = project();
        let spex = project.path().join(SPEX_REPO_DIR);
        let found = SpexConfig::find_file(&spex).expect("found");
        assert_eq!(found, spex.join(SPEX_CONFIG_FILE));
    }

    #[test]
    fn finds_from_nested_package_subdirectory() {
        let project = project();
        let found = SpexConfig::find_file(&project.path().join("demo-api/src")).expect("found");
        assert_eq!(
            found,
            project.path().join(SPEX_REPO_DIR).join(SPEX_CONFIG_FILE)
        );
    }

    #[test]
    fn errors_when_not_in_a_project() {
        let dir = tempdir().expect("dir");
        assert!(SpexConfig::find_file(dir.path()).is_err());
    }

    #[test]
    fn ignores_child_directory_not_named_like_a_spex_repository() {
        let dir = tempdir().expect("dir");
        let templates = dir.path().join("templates");
        std::fs::create_dir_all(&templates).expect("templates dir");
        std::fs::write(templates.join(SPEX_CONFIG_FILE), "project_name: unknown\n")
            .expect("config");
        assert!(SpexConfig::find_file(dir.path()).is_err());
    }

    #[test]
    fn finds_child_directory_named_spex() {
        let dir = tempdir().expect("dir");
        let spex = dir.path().join(SPEX_DIR_NAME);
        std::fs::create_dir_all(&spex).expect("spex dir");
        std::fs::write(spex.join(SPEX_CONFIG_FILE), "project_name: demo\n").expect("config");
        assert_eq!(
            SpexConfig::find_file(dir.path()).expect("found"),
            spex.join(SPEX_CONFIG_FILE)
        );
    }

    fn config_with_version(version: &str) -> SpexConfig {
        SpexConfig {
            cli_version: version.to_string(),
            ..testing::spex_config()
        }
    }

    #[test]
    fn finds_spex_dir_directly_when_run_inside_it() {
        let dir = tempdir().expect("dir");
        std::fs::write(dir.path().join(SPEX_CONFIG_FILE), "project_name: demo\n").expect("config");
        assert_eq!(
            SpexConfig::find_file(dir.path()).expect("found"),
            dir.path().join(SPEX_CONFIG_FILE)
        );
    }

    #[test]
    fn version_warning_is_none_when_matching() {
        assert!(config_with_version(CLI_VERSION).version_warning().is_none());
    }

    #[test]
    fn version_warning_reports_mismatch() {
        let warning = config_with_version("0.0.1")
            .version_warning()
            .expect("warning");
        assert!(warning.contains("0.0.1"));
        assert!(warning.contains(CLI_VERSION));
    }

    #[test]
    fn serialization_keeps_descriptions_and_rules_unused() {
        let config: SpexConfig = serde_yaml_ng::from_str(indoc! {"
            project_name: demo
            cli_version: 0.0.0
            layout: multi-repo
            agent_root: .
            description: A demo project.
            rules_unused:
              - graphql-conventions
            packages:
              - package_name: demo-api
                package_short_name: api
                description: REST API.
                path: ../demo-api
                contracts:
                  - path: contracts/api.yaml
                    description: OpenAPI contract.
        "})
        .expect("parse config");

        let json = serde_json::to_value(&config).expect("serialize");

        assert_eq!(json["description"], "A demo project.");
        assert_eq!(json["rules_unused"][0], "graphql-conventions");
        assert_eq!(json["packages"][0]["description"], "REST API.");
        assert_eq!(
            json["packages"][0]["contracts"][0]["description"],
            "OpenAPI contract."
        );
    }

    #[test]
    fn serialization_omits_absent_descriptions_but_keeps_empty_lists() {
        let config: SpexConfig = serde_yaml_ng::from_str(indoc! {"
            project_name: demo
            cli_version: 0.0.0
            layout: multi-repo
            agent_root: .
            packages:
              - package_name: demo-api
                package_short_name: api
                path: ../demo-api
        "})
        .expect("parse config");

        let json = serde_json::to_value(&config).expect("serialize");

        assert!(json.get("description").is_none());
        assert_eq!(json["rules_unused"], serde_json::json!([]));
        assert!(json["packages"][0].get("description").is_none());
        assert_eq!(json["packages"][0]["contracts"], serde_json::json!([]));
    }

    #[test]
    fn parses_coverage_ignore_entries_naming_a_spec() {
        let config: SpexConfig = serde_yaml_ng::from_str(indoc! {"
            project_name: demo
            cli_version: 0.0.0
            layout: multi-repo
            agent_root: .
            coverage_ignore:
              - spec_id: place-order
                layer: contract
              - spec_id: place-order
                layer: impl
                ac: rejects-empty-cart
        "})
        .expect("parse config");

        let json = serde_json::to_value(&config).expect("serialize");

        assert_eq!(
            json["coverage_ignore"],
            serde_json::json!([
                { "spec_id": "place-order", "layer": "contract" },
                { "spec_id": "place-order", "layer": "impl", "ac": "rejects-empty-cart" },
            ])
        );
    }

    #[test]
    fn rejects_a_config_without_a_layout() {
        let parsed = serde_yaml_ng::from_str::<SpexConfig>(indoc! {"
            project_name: demo
            cli_version: 0.0.0
            agent_root: .
        "});

        assert!(parsed.is_err());
    }

    #[test]
    fn serialization_keeps_an_absent_coverage_ignore_as_an_empty_list() {
        let config: SpexConfig = serde_yaml_ng::from_str(indoc! {"
            project_name: demo
            cli_version: 0.0.0
            layout: multi-repo
            agent_root: .
        "})
        .expect("parse config");

        let json = serde_json::to_value(&config).expect("serialize");

        assert_eq!(json["coverage_ignore"], serde_json::json!([]));
    }
}