main-spex 0.25.0

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

    #[test]
    fn scan_collects_bare_layers() {
        let index =
            AnnotationIndex::scan("// @spec contract a\n// @spec impl a\n// @spec test a\n");
        assert!(index.has_bare(&testing::spec_id("a"), CoverageLayer::Contract));
        assert!(index.has_bare(&testing::spec_id("a"), CoverageLayer::Impl));
        assert!(index.has_bare(&testing::spec_id("a"), CoverageLayer::Test));
    }

    #[test]
    fn scan_splits_multiple_ids_in_one_annotation() {
        let index = AnnotationIndex::scan("// @spec test a, b, rate-limit\n");
        assert!(index.has_bare(&testing::spec_id("a"), CoverageLayer::Test));
        assert!(index.has_bare(&testing::spec_id("b"), CoverageLayer::Test));
        assert!(index.has_bare(&testing::spec_id("rate-limit"), CoverageLayer::Test));
    }

    #[test]
    fn scan_treats_vendor_extension_value_as_contract() {
        let index = AnnotationIndex::scan("      x-spec: \"@spec contract a\"\n");
        assert!(index.has_bare(&testing::spec_id("a"), CoverageLayer::Contract));
        assert!(!index.has_bare(&testing::spec_id("a"), CoverageLayer::Impl));
    }

    #[test]
    fn scan_collects_ac_qualified_annotations() {
        let index =
            AnnotationIndex::scan("// @spec impl a#empty-string\n// @spec test a#empty-string\n");
        assert!(index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("empty-string"),
            CoverageLayer::Impl
        ));
        assert!(index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("empty-string"),
            CoverageLayer::Test
        ));
        assert!(!index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("other-ac"),
            CoverageLayer::Impl
        ));
    }

    #[test]
    fn scan_supports_ac_qualified_ids_in_a_list() {
        let index = AnnotationIndex::scan("// @spec impl a#ac-one, b#ac-two\n");
        assert!(index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("ac-one"),
            CoverageLayer::Impl
        ));
        assert!(index.has_ac(
            &testing::spec_id("b"),
            &testing::ac_id("ac-two"),
            CoverageLayer::Impl
        ));
    }

    #[test]
    fn scan_treats_ac_qualified_contract_annotation_as_bare() {
        let index = AnnotationIndex::scan("// @spec contract a#empty-string\n");
        assert!(index.has_bare(&testing::spec_id("a"), CoverageLayer::Contract));
    }

    #[test]
    fn scan_collects_ac_qualified_contract_annotation_per_ac() {
        let index = AnnotationIndex::scan("// @spec contract a#empty-string\n");
        assert!(index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("empty-string"),
            CoverageLayer::Contract
        ));
        assert!(!index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("other-ac"),
            CoverageLayer::Contract
        ));
        assert!(!index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("empty-string"),
            CoverageLayer::Impl
        ));
    }

    #[test]
    fn bare_annotation_does_not_satisfy_ac_level_coverage() {
        let index = AnnotationIndex::scan("// @spec impl a\n// @spec test a\n");
        assert!(!index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("any-ac"),
            CoverageLayer::Impl
        ));
        assert!(!index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("any-ac"),
            CoverageLayer::Test
        ));
    }

    #[test]
    fn scan_project_reads_contract_files_inside_spex_dir() {
        let dir = tempfile::tempdir().expect("tempdir");
        let spex_dir = dir.path();
        std::fs::create_dir_all(spex_dir.join("contracts")).expect("contracts dir");
        std::fs::write(
            spex_dir.join("contracts/api.openapi.yaml"),
            "      x-spec: \"@spec contract a\"\n",
        )
        .expect("write contract");

        let config: crate::config::SpexConfig = serde_yaml_ng::from_str(
            "project_name: demo\ncli_version: 0.0.0\nlayout: multi-repo\nagent_root: .\npackages:\n  - package_name: demo-api\n    package_short_name: api\n    path: ../demo-api\n    contracts:\n      - path: contracts/api.openapi.yaml\n        description: REST API\n",
        )
        .expect("parse config");

        let spex = crate::config::spex::Spex::from_config(spex_dir.to_path_buf(), config);
        let index = AnnotationIndex::scan_project(&spex).expect("scan");
        assert!(index.has_bare(&testing::spec_id("a"), CoverageLayer::Contract));
    }

    #[test]
    fn scan_project_skips_the_spex_dir_nested_in_a_package() {
        let dir = tempfile::tempdir().expect("tempdir");
        let spex_dir = dir.path().join("spex");
        std::fs::create_dir_all(spex_dir.join("specs")).expect("specs dir");
        std::fs::create_dir_all(dir.path().join("src")).expect("src dir");
        std::fs::write(spex_dir.join("specs/a.yaml"), "# @spec impl a#some-ac\n")
            .expect("write spec");
        std::fs::write(dir.path().join("src/lib.rs"), "// @spec test b#other-ac\n")
            .expect("write source");

        let config: crate::config::SpexConfig = serde_yaml_ng::from_str(
            "project_name: demo\ncli_version: 0.0.0\nlayout: single-repo\nagent_root: ..\npackages:\n  - package_name: demo\n    package_short_name: demo\n    path: ..\n",
        )
        .expect("parse config");

        let spex = crate::config::spex::Spex::from_config(spex_dir, config);
        let index = AnnotationIndex::scan_project(&spex).expect("scan");

        assert!(!index.has_ac(
            &testing::spec_id("a"),
            &testing::ac_id("some-ac"),
            CoverageLayer::Impl
        ));
        assert!(index.has_ac(
            &testing::spec_id("b"),
            &testing::ac_id("other-ac"),
            CoverageLayer::Test
        ));
    }

    fn single_package_spex(
        spex_dir: std::path::PathBuf,
        package_entry: &str,
    ) -> crate::config::spex::Spex {
        let config: crate::config::SpexConfig = serde_yaml_ng::from_str(&format!(
            "project_name: demo\ncli_version: 0.0.0\nlayout: single-repo\nagent_root: ..\npackages:\n{package_entry}"
        ))
        .expect("parse config");
        crate::config::spex::Spex::from_config(spex_dir, config)
    }

    #[test]
    fn scan_project_skips_files_and_folders_a_package_excludes() {
        let dir = tempfile::tempdir().expect("tempdir");
        let spex_dir = dir.path().join("spex");
        std::fs::create_dir_all(&spex_dir).expect("spex dir");
        for (file, annotation) in [
            ("src/lib.rs", "// @spec impl kept#kept-ac\n"),
            ("vendor/dep.rs", "// @spec impl folder#folder-ac\n"),
            ("src/api.generated.ts", "// @spec impl glob#glob-ac\n"),
        ] {
            let path = dir.path().join(file);
            std::fs::create_dir_all(path.parent().expect("parent")).expect("parent dir");
            std::fs::write(path, annotation).expect("write source");
        }

        let spex = single_package_spex(
            spex_dir,
            "  - package_name: demo\n    package_short_name: demo\n    path: ..\n    exclude:\n      - vendor\n      - \"**/*.generated.ts\"\n",
        );
        let index = AnnotationIndex::scan_project(&spex).expect("scan");

        for (spec, ac, expected) in [
            ("kept", "kept-ac", true),
            ("folder", "folder-ac", false),
            ("glob", "glob-ac", false),
        ] {
            assert_eq!(
                index.has_ac(
                    &testing::spec_id(spec),
                    &testing::ac_id(ac),
                    CoverageLayer::Impl
                ),
                expected,
                "{spec}"
            );
        }
    }

    #[test]
    fn scan_project_still_reads_a_contract_file_the_package_excludes() {
        let dir = tempfile::tempdir().expect("tempdir");
        let spex_dir = dir.path().join("spex");
        std::fs::create_dir_all(&spex_dir).expect("spex dir");
        std::fs::create_dir_all(dir.path().join("contracts")).expect("contracts dir");
        std::fs::write(
            dir.path().join("contracts/api.openapi.yaml"),
            "      x-spec: \"@spec contract a\"\n",
        )
        .expect("write contract");

        let spex = single_package_spex(
            spex_dir,
            "  - package_name: demo\n    package_short_name: demo\n    path: ..\n    contracts:\n      - path: ../contracts/api.openapi.yaml\n        description: REST API\n    exclude:\n      - contracts\n",
        );
        let index = AnnotationIndex::scan_project(&spex).expect("scan");

        assert!(index.has_bare(&testing::spec_id("a"), CoverageLayer::Contract));
    }

    #[test]
    fn scan_project_does_not_apply_the_exclude_of_one_package_to_another() {
        let dir = tempfile::tempdir().expect("tempdir");
        let spex_dir = dir.path().join("spex");
        std::fs::create_dir_all(&spex_dir).expect("spex dir");
        for package in ["api", "web"] {
            let path = dir.path().join(package).join("vendor/dep.rs");
            std::fs::create_dir_all(path.parent().expect("parent")).expect("parent dir");
            std::fs::write(path, format!("// @spec impl {package}#vendored-ac\n"))
                .expect("write source");
        }

        let spex = single_package_spex(
            spex_dir,
            "  - package_name: demo-api\n    package_short_name: api\n    path: ../api\n    exclude: [vendor]\n  - package_name: demo-web\n    package_short_name: web\n    path: ../web\n",
        );
        let index = AnnotationIndex::scan_project(&spex).expect("scan");

        assert!(!index.has_ac(
            &testing::spec_id("api"),
            &testing::ac_id("vendored-ac"),
            CoverageLayer::Impl
        ));
        assert!(index.has_ac(
            &testing::spec_id("web"),
            &testing::ac_id("vendored-ac"),
            CoverageLayer::Impl
        ));
    }

    #[test]
    fn scan_project_fails_naming_the_package_when_an_exclude_is_not_a_valid_glob() {
        let dir = tempfile::tempdir().expect("tempdir");
        let spex_dir = dir.path().join("spex");
        std::fs::create_dir_all(&spex_dir).expect("spex dir");

        let spex = single_package_spex(
            spex_dir,
            "  - package_name: demo\n    package_short_name: demo\n    path: ..\n    exclude: [\"src/{a,b\"]\n",
        );
        let error = AnnotationIndex::scan_project(&spex).expect_err("invalid exclude");

        let message = crate::error::describe(&error);
        assert!(message.contains("`demo`"), "{message}");
        assert!(message.contains("src/{a,b"), "{message}");
    }
}